Merge branch 'worktree-agent-abadd094'
This commit is contained in:
@@ -14,6 +14,13 @@ export class World {
|
||||
return id;
|
||||
}
|
||||
|
||||
createEntityWithId(id: EntityId): void {
|
||||
this.entities.add(id);
|
||||
if (id >= this.nextId) {
|
||||
this.nextId = id + 1;
|
||||
}
|
||||
}
|
||||
|
||||
removeEntity(entity: EntityId): void {
|
||||
this.entities.delete(entity);
|
||||
for (const store of this.components.values()) {
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { World } from '../../ecs/World.js';
|
||||
import { openDatabase, closeDatabase } from '../database.js';
|
||||
import { saveEntities, loadEntities } from '../entitySerializer.js';
|
||||
import type {
|
||||
Position, Needs, Stats, Movement, NPCBrain, SocialState,
|
||||
StatModifiers, Appearance, RelationshipData, EntityId,
|
||||
} from '@dflike/shared';
|
||||
import type { StructureData } from '../../systems/buildingSystem.js';
|
||||
import type { BondRegistry } from '../../systems/bondRegistry.js';
|
||||
import { createBondRegistry } from '../../systems/bondRegistry.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
function tempDbPath(): string {
|
||||
return path.join(os.tmpdir(), `dflike-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
||||
}
|
||||
|
||||
describe('entitySerializer', () => {
|
||||
const dbPaths: string[] = [];
|
||||
let world: World;
|
||||
let dbPath: string;
|
||||
|
||||
function createTempDb(): string {
|
||||
const p = tempDbPath();
|
||||
dbPaths.push(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
dbPath = createTempDb();
|
||||
openDatabase(dbPath);
|
||||
world = new World();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { closeDatabase(); } catch { /* ignore */ }
|
||||
for (const p of dbPaths) {
|
||||
try { fs.unlinkSync(p); } catch { /* ignore */ }
|
||||
try { fs.unlinkSync(p + '-wal'); } catch { /* ignore */ }
|
||||
try { fs.unlinkSync(p + '-shm'); } catch { /* ignore */ }
|
||||
}
|
||||
dbPaths.length = 0;
|
||||
});
|
||||
|
||||
it('round-trips an NPC with all components', () => {
|
||||
const e = world.createEntity();
|
||||
world.addComponent<Position>(e, 'position', { x: 10.5, y: 20.5 });
|
||||
world.addComponent<string>(e, 'name', 'Gwendolyn');
|
||||
world.addComponent<Needs>(e, 'needs', { hunger: 75, energy: 60, productivity: 40 });
|
||||
world.addComponent<Stats>(e, 'stats', {
|
||||
strength: 12, dexterity: 14, constitution: 10, intelligence: 8, perception: 11,
|
||||
sociability: 15, courage: 9, curiosity: 13, empathy: 16, temperament: 7,
|
||||
});
|
||||
world.addComponent<Map<string, number>>(e, 'inventory', new Map([['log', 3], ['stone', 1]]));
|
||||
world.addComponent<Appearance>(e, 'appearance', {
|
||||
skinId: 'shape00_skin02',
|
||||
accessories: { torso: 'shirt01' },
|
||||
portraitFeatures: { eyes: 'eyes01' },
|
||||
});
|
||||
world.addComponent<Movement>(e, 'movement', {
|
||||
state: 'walking', target: { x: 5, y: 5 }, path: [{ x: 5, y: 5 }], direction: 2, moveProgress: 0.5,
|
||||
});
|
||||
world.addComponent<NPCBrain>(e, 'npcBrain', {
|
||||
currentGoal: 'gather', goalQueue: ['wander'],
|
||||
gatherTarget: { x: 3, y: 4, resourceType: 'tree' },
|
||||
});
|
||||
world.addComponent<SocialState>(e, 'socialState', {
|
||||
phase: 'facing', partnerId: 99, phaseTimer: 5,
|
||||
outcome: 'positive', globalCooldown: 10,
|
||||
pairCooldowns: new Map([[2, 50], [3, 30]]),
|
||||
lastOutcome: { partnerId: 99, outcome: 'positive', tick: 100 },
|
||||
proposalCooldown: 0,
|
||||
pendingProposal: { targetId: 99, type: 'partner' },
|
||||
isProposalInteraction: true,
|
||||
});
|
||||
world.addComponent<StatModifiers>(e, 'statModifiers', {
|
||||
modifiers: [{ stat: 'strength', value: 2, remaining: 10 }],
|
||||
});
|
||||
world.addComponent<string>(e, 'backstory', 'A brave adventurer from the north.');
|
||||
|
||||
saveEntities(world);
|
||||
|
||||
// Load into a fresh world
|
||||
const world2 = new World();
|
||||
const maxId = loadEntities(world2);
|
||||
|
||||
const entities = world2.getAllEntities();
|
||||
expect(entities).toHaveLength(1);
|
||||
expect(entities[0]).toBe(e);
|
||||
|
||||
// Position
|
||||
const pos = world2.getComponent<Position>(e, 'position')!;
|
||||
expect(pos.x).toBe(10.5);
|
||||
expect(pos.y).toBe(20.5);
|
||||
|
||||
// Name
|
||||
expect(world2.getComponent<string>(e, 'name')).toBe('Gwendolyn');
|
||||
|
||||
// Needs
|
||||
const needs = world2.getComponent<Needs>(e, 'needs')!;
|
||||
expect(needs.hunger).toBe(75);
|
||||
expect(needs.energy).toBe(60);
|
||||
|
||||
// Stats
|
||||
const stats = world2.getComponent<Stats>(e, 'stats')!;
|
||||
expect(stats.strength).toBe(12);
|
||||
expect(stats.empathy).toBe(16);
|
||||
|
||||
// Inventory (Map)
|
||||
const inv = world2.getComponent<Map<string, number>>(e, 'inventory')!;
|
||||
expect(inv).toBeInstanceOf(Map);
|
||||
expect(inv.get('log')).toBe(3);
|
||||
expect(inv.get('stone')).toBe(1);
|
||||
|
||||
// Appearance
|
||||
const app = world2.getComponent<Appearance>(e, 'appearance')!;
|
||||
expect(app.skinId).toBe('shape00_skin02');
|
||||
expect(app.accessories.torso).toBe('shirt01');
|
||||
|
||||
// Movement (reset on load)
|
||||
const mov = world2.getComponent<Movement>(e, 'movement')!;
|
||||
expect(mov.state).toBe('idle');
|
||||
expect(mov.target).toBeNull();
|
||||
expect(mov.path).toEqual([]);
|
||||
expect(mov.moveProgress).toBe(0);
|
||||
|
||||
// NPCBrain (reset on load)
|
||||
const brain = world2.getComponent<NPCBrain>(e, 'npcBrain')!;
|
||||
expect(brain.currentGoal).toBeNull();
|
||||
expect(brain.goalQueue).toEqual([]);
|
||||
|
||||
// SocialState (reset on load)
|
||||
const social = world2.getComponent<SocialState>(e, 'socialState')!;
|
||||
expect(social.phase).toBe('none');
|
||||
expect(social.partnerId).toBeNull();
|
||||
expect(social.phaseTimer).toBe(0);
|
||||
expect(social.outcome).toBeNull();
|
||||
expect(social.lastOutcome).toBeNull();
|
||||
expect(social.pendingProposal).toBeNull();
|
||||
expect(social.isProposalInteraction).toBe(false);
|
||||
// pairCooldowns should be preserved
|
||||
expect(social.pairCooldowns).toBeInstanceOf(Map);
|
||||
expect(social.pairCooldowns.get(2)).toBe(50);
|
||||
// globalCooldown preserved
|
||||
expect(social.globalCooldown).toBe(10);
|
||||
|
||||
// StatModifiers
|
||||
const mods = world2.getComponent<StatModifiers>(e, 'statModifiers')!;
|
||||
expect(mods.modifiers).toHaveLength(1);
|
||||
expect(mods.modifiers[0].stat).toBe('strength');
|
||||
|
||||
// Backstory
|
||||
expect(world2.getComponent<string>(e, 'backstory')).toBe('A brave adventurer from the north.');
|
||||
|
||||
expect(maxId).toBe(e);
|
||||
});
|
||||
|
||||
it('round-trips a structure entity', () => {
|
||||
const e = world.createEntity();
|
||||
world.addComponent<Position>(e, 'position', { x: 5, y: 10 });
|
||||
world.addComponent<string>(e, 'name', 'Stockpile');
|
||||
world.addComponent<StructureData>(e, 'structure', {
|
||||
type: 'stockpile',
|
||||
subtype: 'general',
|
||||
inventory: new Map([['log', 10], ['stone', 5]]),
|
||||
buildProgress: undefined,
|
||||
});
|
||||
|
||||
saveEntities(world);
|
||||
|
||||
const world2 = new World();
|
||||
loadEntities(world2);
|
||||
|
||||
const entities = world2.getAllEntities();
|
||||
expect(entities).toHaveLength(1);
|
||||
|
||||
const struct = world2.getComponent<StructureData>(e, 'structure')!;
|
||||
expect(struct.type).toBe('stockpile');
|
||||
expect(struct.subtype).toBe('general');
|
||||
expect(struct.inventory).toBeInstanceOf(Map);
|
||||
expect(struct.inventory.get('log')).toBe(10);
|
||||
expect(struct.inventory.get('stone')).toBe(5);
|
||||
});
|
||||
|
||||
it('round-trips relationships between entities', () => {
|
||||
const e1 = world.createEntity();
|
||||
const e2 = world.createEntity();
|
||||
world.addComponent<Position>(e1, 'position', { x: 0, y: 0 });
|
||||
world.addComponent<Position>(e2, 'position', { x: 1, y: 1 });
|
||||
world.addComponent<string>(e1, 'name', 'Alice');
|
||||
world.addComponent<string>(e2, 'name', 'Bob');
|
||||
|
||||
const rels1 = new Map<EntityId, RelationshipData>();
|
||||
rels1.set(e2, { value: 50, interactions: 10, lastInteractionTick: 100, status: 'active' });
|
||||
world.addComponent(e1, 'relationships', rels1);
|
||||
|
||||
const rels2 = new Map<EntityId, RelationshipData>();
|
||||
rels2.set(e1, { value: 30, interactions: 10, lastInteractionTick: 100, status: 'active' });
|
||||
world.addComponent(e2, 'relationships', rels2);
|
||||
|
||||
saveEntities(world);
|
||||
|
||||
const world2 = new World();
|
||||
loadEntities(world2);
|
||||
|
||||
const loadedRels1 = world2.getComponent<Map<EntityId, RelationshipData>>(e1, 'relationships')!;
|
||||
expect(loadedRels1).toBeInstanceOf(Map);
|
||||
expect(loadedRels1.get(e2)!.value).toBe(50);
|
||||
expect(loadedRels1.get(e2)!.interactions).toBe(10);
|
||||
expect(loadedRels1.get(e2)!.status).toBe('active');
|
||||
|
||||
const loadedRels2 = world2.getComponent<Map<EntityId, RelationshipData>>(e2, 'relationships')!;
|
||||
expect(loadedRels2.get(e1)!.value).toBe(30);
|
||||
});
|
||||
|
||||
it('round-trips bond registry', () => {
|
||||
const e1 = world.createEntity();
|
||||
const e2 = world.createEntity();
|
||||
world.addComponent<Position>(e1, 'position', { x: 0, y: 0 });
|
||||
world.addComponent<Position>(e2, 'position', { x: 1, y: 1 });
|
||||
|
||||
const registry = createBondRegistry();
|
||||
registry.set(`${e1}:${e2}`, [
|
||||
{ type: 'partner', formedAtTick: 50, status: 'active' },
|
||||
{ type: 'friend', formedAtTick: 20, status: 'former' },
|
||||
]);
|
||||
world.setSingleton('bondRegistry', registry);
|
||||
|
||||
saveEntities(world);
|
||||
|
||||
const world2 = new World();
|
||||
loadEntities(world2);
|
||||
|
||||
const loadedRegistry = world2.getSingleton<BondRegistry>('bondRegistry')!;
|
||||
expect(loadedRegistry).toBeInstanceOf(Map);
|
||||
|
||||
const bonds = loadedRegistry.get(`${e1}:${e2}`)!;
|
||||
expect(bonds).toHaveLength(2);
|
||||
expect(bonds[0].type).toBe('partner');
|
||||
expect(bonds[0].formedAtTick).toBe(50);
|
||||
expect(bonds[0].status).toBe('active');
|
||||
expect(bonds[1].type).toBe('friend');
|
||||
expect(bonds[1].status).toBe('former');
|
||||
});
|
||||
|
||||
it('skips playerControlled entities', () => {
|
||||
const npc = world.createEntity();
|
||||
world.addComponent<Position>(npc, 'position', { x: 0, y: 0 });
|
||||
world.addComponent<string>(npc, 'name', 'NPC');
|
||||
|
||||
const player = world.createEntity();
|
||||
world.addComponent<Position>(player, 'position', { x: 5, y: 5 });
|
||||
world.addComponent(player, 'playerControlled', { playerId: 'p1', mode: 'avatar' });
|
||||
world.addComponent<string>(player, 'name', 'Player');
|
||||
|
||||
saveEntities(world);
|
||||
|
||||
const world2 = new World();
|
||||
loadEntities(world2);
|
||||
|
||||
const entities = world2.getAllEntities();
|
||||
expect(entities).toHaveLength(1);
|
||||
expect(entities[0]).toBe(npc);
|
||||
expect(world2.getComponent<string>(npc, 'name')).toBe('NPC');
|
||||
});
|
||||
|
||||
it('sets maxId correctly after loading', () => {
|
||||
const e1 = world.createEntity(); // 1
|
||||
const e2 = world.createEntity(); // 2
|
||||
const e3 = world.createEntity(); // 3
|
||||
world.addComponent<Position>(e1, 'position', { x: 0, y: 0 });
|
||||
world.addComponent<Position>(e2, 'position', { x: 1, y: 1 });
|
||||
world.addComponent<Position>(e3, 'position', { x: 2, y: 2 });
|
||||
|
||||
saveEntities(world);
|
||||
|
||||
const world2 = new World();
|
||||
const maxId = loadEntities(world2);
|
||||
expect(maxId).toBe(3);
|
||||
|
||||
// New entity should get id 4
|
||||
const newEntity = world2.createEntity();
|
||||
expect(newEntity).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
import type { EntityId, RelationshipData } from '@dflike/shared';
|
||||
import type { World } from '../ecs/World.js';
|
||||
import type { Bond, BondRegistry } from '../systems/bondRegistry.js';
|
||||
import { createBondRegistry } from '../systems/bondRegistry.js';
|
||||
import { getDatabase } from './database.js';
|
||||
|
||||
/** Components to serialize (excluding name, relationships, position which are handled separately) */
|
||||
const SERIALIZABLE_COMPONENTS = [
|
||||
'needs', 'stats', 'appearance', 'npcBrain', 'socialState',
|
||||
'statModifiers', 'backstory', 'inventory', 'structure', 'movement',
|
||||
] as const;
|
||||
|
||||
function serializeComponent(name: string, data: unknown): string {
|
||||
switch (name) {
|
||||
case 'inventory': {
|
||||
// Map<string, number> -> plain object
|
||||
const map = data as Map<string, number>;
|
||||
return JSON.stringify(Object.fromEntries(map));
|
||||
}
|
||||
case 'socialState': {
|
||||
const social = data as Record<string, unknown>;
|
||||
// Convert pairCooldowns Map, reset transient fields
|
||||
const serialized = {
|
||||
...social,
|
||||
phase: 'none',
|
||||
partnerId: null,
|
||||
phaseTimer: 0,
|
||||
outcome: null,
|
||||
lastOutcome: null,
|
||||
pendingProposal: null,
|
||||
isProposalInteraction: false,
|
||||
pairCooldowns: Object.fromEntries(
|
||||
(social.pairCooldowns as Map<EntityId, number>) ?? new Map(),
|
||||
),
|
||||
};
|
||||
return JSON.stringify(serialized);
|
||||
}
|
||||
case 'structure': {
|
||||
const struct = data as Record<string, unknown>;
|
||||
const serialized = {
|
||||
...struct,
|
||||
inventory: Object.fromEntries(
|
||||
(struct.inventory as Map<string, number>) ?? new Map(),
|
||||
),
|
||||
};
|
||||
return JSON.stringify(serialized);
|
||||
}
|
||||
case 'movement': {
|
||||
const mov = data as Record<string, unknown>;
|
||||
const serialized = {
|
||||
...mov,
|
||||
path: [],
|
||||
state: 'idle',
|
||||
target: null,
|
||||
moveProgress: 0,
|
||||
};
|
||||
return JSON.stringify(serialized);
|
||||
}
|
||||
case 'npcBrain': {
|
||||
const brain = data as Record<string, unknown>;
|
||||
const serialized = {
|
||||
...brain,
|
||||
currentGoal: null,
|
||||
goalQueue: [],
|
||||
};
|
||||
return JSON.stringify(serialized);
|
||||
}
|
||||
case 'backstory':
|
||||
// backstory is a string, wrap directly
|
||||
return JSON.stringify(data);
|
||||
default:
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
|
||||
function deserializeComponent(name: string, json: string): unknown {
|
||||
const parsed = JSON.parse(json);
|
||||
|
||||
switch (name) {
|
||||
case 'inventory': {
|
||||
return new Map(Object.entries(parsed));
|
||||
}
|
||||
case 'socialState': {
|
||||
return {
|
||||
...parsed,
|
||||
pairCooldowns: new Map(
|
||||
Object.entries(parsed.pairCooldowns ?? {}).map(
|
||||
([k, v]) => [Number(k), v] as [number, number],
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'structure': {
|
||||
return {
|
||||
...parsed,
|
||||
inventory: new Map(Object.entries(parsed.inventory ?? {})),
|
||||
};
|
||||
}
|
||||
case 'backstory':
|
||||
case 'name':
|
||||
// These are strings directly
|
||||
return parsed;
|
||||
default:
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveEntities(world: World): void {
|
||||
const db = getDatabase();
|
||||
|
||||
const transaction = db.transaction(() => {
|
||||
// Clear existing entity data
|
||||
db.exec('DELETE FROM entities');
|
||||
db.exec('DELETE FROM components');
|
||||
db.exec('DELETE FROM relationships');
|
||||
db.exec('DELETE FROM bonds');
|
||||
|
||||
const insertEntity = db.prepare(
|
||||
'INSERT INTO entities (id, type, name, x, y) VALUES (?, ?, ?, ?, ?)',
|
||||
);
|
||||
const insertComponent = db.prepare(
|
||||
'INSERT INTO components (entity_id, component_name, data) VALUES (?, ?, ?)',
|
||||
);
|
||||
const insertRelationship = db.prepare(
|
||||
'INSERT INTO relationships (entity_id, target_id, value, interactions, last_interaction_tick, status) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
);
|
||||
const insertBond = db.prepare(
|
||||
'INSERT INTO bonds (entity_a, entity_b, type, formed_tick, status) VALUES (?, ?, ?, ?, ?)',
|
||||
);
|
||||
|
||||
for (const entityId of world.getAllEntities()) {
|
||||
// Skip player-controlled entities
|
||||
if (world.getComponent(entityId, 'playerControlled')) continue;
|
||||
|
||||
const position = world.getComponent<{ x: number; y: number }>(entityId, 'position');
|
||||
if (!position) continue;
|
||||
|
||||
// Determine entity type
|
||||
const hasStructure = !!world.getComponent(entityId, 'structure');
|
||||
const entityType = hasStructure ? 'structure' : 'npc';
|
||||
const name = world.getComponent<string>(entityId, 'name') ?? null;
|
||||
|
||||
insertEntity.run(entityId, entityType, name, position.x, position.y);
|
||||
|
||||
// Serialize components
|
||||
for (const compName of SERIALIZABLE_COMPONENTS) {
|
||||
const compData = world.getComponent(entityId, compName);
|
||||
if (compData !== undefined) {
|
||||
insertComponent.run(entityId, compName, serializeComponent(compName, compData));
|
||||
}
|
||||
}
|
||||
|
||||
// Relationships go to dedicated table
|
||||
const relationships = world.getComponent<Map<EntityId, RelationshipData>>(
|
||||
entityId, 'relationships',
|
||||
);
|
||||
if (relationships) {
|
||||
for (const [targetId, relData] of relationships) {
|
||||
insertRelationship.run(
|
||||
entityId, targetId, relData.value,
|
||||
relData.interactions, relData.lastInteractionTick, relData.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save bond registry
|
||||
const bondRegistry = world.getSingleton<BondRegistry>('bondRegistry');
|
||||
if (bondRegistry) {
|
||||
for (const [key, bonds] of bondRegistry) {
|
||||
const [a, b] = key.split(':').map(Number);
|
||||
for (const bond of bonds) {
|
||||
insertBond.run(a, b, bond.type, bond.formedAtTick, bond.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
transaction();
|
||||
}
|
||||
|
||||
export function loadEntities(world: World): number {
|
||||
const db = getDatabase();
|
||||
let maxId = 0;
|
||||
|
||||
// Load entities
|
||||
const entityRows = db.prepare('SELECT id, type, name, x, y FROM entities ORDER BY id').all() as Array<{
|
||||
id: number; type: string; name: string | null; x: number; y: number;
|
||||
}>;
|
||||
|
||||
for (const row of entityRows) {
|
||||
world.createEntityWithId(row.id);
|
||||
world.addComponent(row.id, 'position', { x: row.x, y: row.y });
|
||||
if (row.name !== null) {
|
||||
world.addComponent(row.id, 'name', row.name);
|
||||
}
|
||||
if (row.id > maxId) maxId = row.id;
|
||||
}
|
||||
|
||||
// Load components
|
||||
const componentRows = db.prepare('SELECT entity_id, component_name, data FROM components').all() as Array<{
|
||||
entity_id: number; component_name: string; data: string;
|
||||
}>;
|
||||
|
||||
for (const row of componentRows) {
|
||||
const data = deserializeComponent(row.component_name, row.data);
|
||||
world.addComponent(row.entity_id, row.component_name, data);
|
||||
}
|
||||
|
||||
// Load relationships, grouped by entity_id
|
||||
const relRows = db.prepare(
|
||||
'SELECT entity_id, target_id, value, interactions, last_interaction_tick, status FROM relationships',
|
||||
).all() as Array<{
|
||||
entity_id: number; target_id: number; value: number;
|
||||
interactions: number; last_interaction_tick: number; status: string;
|
||||
}>;
|
||||
|
||||
const relMap = new Map<EntityId, Map<EntityId, RelationshipData>>();
|
||||
for (const row of relRows) {
|
||||
if (!relMap.has(row.entity_id)) {
|
||||
relMap.set(row.entity_id, new Map());
|
||||
}
|
||||
relMap.get(row.entity_id)!.set(row.target_id, {
|
||||
value: row.value,
|
||||
interactions: row.interactions,
|
||||
lastInteractionTick: row.last_interaction_tick,
|
||||
status: row.status as 'active' | 'memory',
|
||||
});
|
||||
}
|
||||
for (const [entityId, rels] of relMap) {
|
||||
world.addComponent(entityId, 'relationships', rels);
|
||||
}
|
||||
|
||||
// Load bonds
|
||||
const bondRows = db.prepare(
|
||||
'SELECT entity_a, entity_b, type, formed_tick, status FROM bonds',
|
||||
).all() as Array<{
|
||||
entity_a: number; entity_b: number; type: string;
|
||||
formed_tick: number; status: string;
|
||||
}>;
|
||||
|
||||
const bondRegistry: BondRegistry = createBondRegistry();
|
||||
for (const row of bondRows) {
|
||||
const key = `${row.entity_a}:${row.entity_b}`;
|
||||
if (!bondRegistry.has(key)) {
|
||||
bondRegistry.set(key, []);
|
||||
}
|
||||
bondRegistry.get(key)!.push({
|
||||
type: row.type,
|
||||
formedAtTick: row.formed_tick,
|
||||
status: row.status as 'active' | 'former',
|
||||
});
|
||||
}
|
||||
if (bondRows.length > 0) {
|
||||
world.setSingleton('bondRegistry', bondRegistry);
|
||||
}
|
||||
|
||||
return maxId;
|
||||
}
|
||||
Reference in New Issue
Block a user