From bac50f823eab5cc238169f7a8bc40597f02c3aba Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Thu, 5 Feb 2026 07:44:30 -0500 Subject: [PATCH 1/3] fix: fix dup returns in hash grid --- src/Entity/Misc/Mothership.ts | 2 +- src/Physics/HashGrid.ts | 51 +++++++++++++++-------------------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/Entity/Misc/Mothership.ts b/src/Entity/Misc/Mothership.ts index 145cf335..9c03ac6c 100644 --- a/src/Entity/Misc/Mothership.ts +++ b/src/Entity/Misc/Mothership.ts @@ -20,7 +20,7 @@ import { ClientInputs } from "../../Client"; import { tps } from "../../config"; import { Color, Tank, Stat, ColorsHexCode, ClientBound, TeamFlags } from "../../Const/Enums"; import GameServer from "../../Game"; -import ArenaEntity, { ArenaState } from "../../Native/Arena"; +import { ArenaState } from "../../Native/Arena"; import { CameraEntity } from "../../Native/Camera"; import { AI, AIState, Inputs } from "../AI"; import Live from "../Live"; diff --git a/src/Physics/HashGrid.ts b/src/Physics/HashGrid.ts index de444af5..12344abb 100644 --- a/src/Physics/HashGrid.ts +++ b/src/Physics/HashGrid.ts @@ -41,7 +41,7 @@ export default class HashGrid implements CollisionManager { private hashMap: number[][] = []; private gameLeftX: number = 0; private gameTopY: number = 0 - private collisionPairsSeen = new Uint32Array(MAX_ENTITY_COUNT * MAX_ENTITY_COUNT / 32) + private collisionPairsSeen = new Uint32Array(MAX_ENTITY_COUNT * (MAX_ENTITY_COUNT - 1) / 2 / 32) public constructor(game: GameServer) { this.game = game; @@ -50,7 +50,7 @@ export default class HashGrid implements CollisionManager { public preTick(tick: number): void { const widthInCells = (this.game.arena.width + (CELL_SIZE - 1)) >> CELL_SHIFT; const heightInCells = (this.game.arena.height + (CELL_SIZE - 1)) >> CELL_SHIFT; - this.hashMul = widthInCells >> CELL_SHIFT; + this.hashMul = widthInCells; this.hashMap = Array(widthInCells * heightInCells); this.queryIdMap.fill(0); this.lastQueryId = 0; @@ -65,7 +65,7 @@ export default class HashGrid implements CollisionManager { } public insert(entity: ObjectEntity) { - if (this.isLocked) throw new Error("HashGrid is locked! Cannot insert entity outside of tick"); + if (this.isLocked) throw new Error("HashGrid is locked! Cannot insert() entity outside of tick"); const { sides, size, width } = entity.physicsData.values; const { x, y } = entity.positionData.values; const isLine = sides === 2; @@ -98,7 +98,7 @@ export default class HashGrid implements CollisionManager { halfWidth: number, halfHeight: number ): PackedEntitySet { - if (this.isLocked) throw new Error("HashGrid is locked! Cannot insert entity outside of tick"); + if (this.isLocked) throw new Error("HashGrid is locked! Cannot retrieve() entity outside of tick"); const result = this.resultSet; result.clear(); @@ -140,7 +140,7 @@ export default class HashGrid implements CollisionManager { halfHeight: number, predicate: (entity: ObjectEntity) => boolean ): ObjectEntity | null { - if (this.isLocked) throw new Error("HashGrid is locked! Cannot insert entity outside of tick"); + if (this.isLocked) throw new Error("HashGrid is locked! Cannot getFirstMatch() outside of tick"); const startX = (centerX - halfWidth - this.gameLeftX) >> CELL_SHIFT; const startY = (centerY - halfHeight - this.gameTopY) >> CELL_SHIFT; @@ -180,9 +180,9 @@ export default class HashGrid implements CollisionManager { // No longer used public retrieveEntitiesByEntity(entity: ObjectEntity): PackedEntitySet { - if (this.isLocked) throw new Error("HashGrid is locked! Cannot insert entity outside of tick"); + if (this.isLocked) throw new Error("HashGrid is locked! Cannot retrieveEntitiesByEntity() outside of tick"); const { sides, size, width } = entity.physicsData.values; - const { x, y } = entity.positionData; + const { x, y } = entity.positionData.values; const isLine = sides === 2; const halfWidth = isLine ? size / 2 : size; const halfHeight = isLine ? width / 2 : size; @@ -192,7 +192,7 @@ export default class HashGrid implements CollisionManager { public forEachCollisionPair( callback: (entityA: ObjectEntity, entityB: ObjectEntity) => void ): void { - if (this.isLocked) throw new Error("HashGrid is locked! Cannot insert entity outside of tick"); + if (this.isLocked) throw new Error("HashGrid is locked! Cannot forEachCollisionPair() entity outside of tick"); const collisionsSeen = this.collisionPairsSeen; collisionsSeen.fill(0); @@ -213,27 +213,20 @@ export default class HashGrid implements CollisionManager { const entityB = this.game.entities.inner[eidB] as ObjectEntity; if (!entityB || entityB.hash === 0) continue; - if (eidA < eidB) { - // Prevent extra-cell duplicates - const pairHash = (eidA << MAX_ENTITY_ID_BITS) | eidB; - const pairHashIndex = pairHash >>> 5; - const pairHashBit = 1 << (pairHash & 31); - if ((collisionsSeen[pairHashIndex] & pairHashBit) !== 0) continue; - collisionsSeen[pairHashIndex] |= pairHashBit; - - // Ensure (x, y) -> x.id < y.id - callback(entityA, entityB); - } else { - // Prevent extra-cell duplicates - const pairHash = (eidB << MAX_ENTITY_ID_BITS) | eidA; - const pairHashIndex = pairHash >>> 5; - const pairHashBit = 1 << (pairHash & 31); - if ((collisionsSeen[pairHashIndex] & pairHashBit) !== 0) continue; - collisionsSeen[pairHashIndex] |= pairHashBit; - - // Ensure (x, y) -> x.id < y.id - callback(entityB, entityA); - } + // Ensure eidA < eidB for triangular matrix indexing + const [idA, idB] = eidA < eidB ? [eidA, eidB] : [eidB, eidA]; + const [entA, entB] = eidA < eidB ? [entityA, entityB] : [entityB, entityA]; + + // Triangular matrix index: row * (row - 1) / 2 + col, where row > col + const triangularIndex = (idB * (idB - 1) / 2 + idA); + const arrayIndex = triangularIndex >>> 5; + const bitIndex = triangularIndex & 31; + const bitMask = 1 << bitIndex; + + if ((collisionsSeen[arrayIndex] & bitMask) !== 0) continue; + collisionsSeen[arrayIndex] |= bitMask; + + callback(entA, entB); } } } From 60cd4a2084a54d4838350fe0fca1e992e6574a28 Mon Sep 17 00:00:00 2001 From: abcxff <79597906+abcxff@users.noreply.github.com> Date: Thu, 5 Feb 2026 07:52:47 -0500 Subject: [PATCH 2/3] fix: de-dup camera ticks --- src/Native/Manager.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Native/Manager.ts b/src/Native/Manager.ts index aa32cc11..61b6c47d 100644 --- a/src/Native/Manager.ts +++ b/src/Native/Manager.ts @@ -179,16 +179,23 @@ export default class EntityManager { this.collisionManager.forEachCollisionPair(this.handleCollision); for (let id = 0; id <= this.lastId; ++id) { - const entity = this.inner[id] as ObjectEntity; + const entity = this.inner[id]; // Alternatively, Entity.exists(entity), though this is probably faster. if (!entity || entity.hash === 0) continue; - if (entity.isPhysical) { - entity.applyPhysics(); - } - - if (!entity.isChild) { + if (entity instanceof CameraEntity) { + continue; + } else if (ObjectEntity.isObject(entity)) { + if (entity.isPhysical) { + entity.applyPhysics(); + } + + if (!entity.isChild) { + entity.tick(tick); + } + } else { + // Not an object entity (arena) entity.tick(tick); } } From a40c5fe0ff2e0aac1bbe052ea5f7dc92f4cfef37 Mon Sep 17 00:00:00 2001 From: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com> Date: Fri, 6 Feb 2026 03:16:14 +0200 Subject: [PATCH 3/3] No need for instanceof check --- src/Native/Manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Native/Manager.ts b/src/Native/Manager.ts index 61b6c47d..f1b3f852 100644 --- a/src/Native/Manager.ts +++ b/src/Native/Manager.ts @@ -184,7 +184,7 @@ export default class EntityManager { // Alternatively, Entity.exists(entity), though this is probably faster. if (!entity || entity.hash === 0) continue; - if (entity instanceof CameraEntity) { + if (entity.cameraData) { // Cameras are ticked later continue; } else if (ObjectEntity.isObject(entity)) { if (entity.isPhysical) {