diff --git a/package-lock.json b/package-lock.json index 670d5d55c..5817a153c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "xrf-engine", "version": "1.0.0", "dependencies": { - "xray16": "^2.0.8" + "xray16": "^2.0.9" }, "bin": { "xrf": "cli/run.ts" @@ -9564,9 +9564,9 @@ "dev": true }, "node_modules/xray16": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/xray16/-/xray16-2.0.8.tgz", - "integrity": "sha512-a3iU8tnt0k7D+0VqNC4g2O/umLWkpdysxhyxQbARcXdibXAQS25ElLPP5VuXeA2RP9JT/j0beS3CLMfyxyDfiw==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/xray16/-/xray16-2.0.9.tgz", + "integrity": "sha512-R+9rNCyI9vQSWgLtbhIXeY/tGIGOqM1+n/ogXCkebcJYlATa1+sTlJpwuV2KlilWDAUFNAuuUlILHzWDeMT+OA==", "license": "MIT", "dependencies": { "fengari": "^0.1.5", diff --git a/package.json b/package.json index fa8b9a505..04128c456 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "help": "ts-node -P ./cli/tsconfig.json cli/run.ts -h" }, "dependencies": { - "xray16": "^2.0.8" + "xray16": "^2.0.9" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/src/engine/core/binders/creature/ActorBinder.test.ts b/src/engine/core/binders/creature/ActorBinder.test.ts index ee33174bb..6b03465ef 100644 --- a/src/engine/core/binders/creature/ActorBinder.test.ts +++ b/src/engine/core/binders/creature/ActorBinder.test.ts @@ -83,8 +83,9 @@ describe("ActorBinder", () => { expect(eventsManager.emitEvent).toHaveBeenCalledTimes(2); expect(eventsManager.emitEvent).toHaveBeenCalledWith(EGameEvent.ACTOR_GO_OFFLINE, binder); - expect(actor.set_callback).toHaveBeenCalledTimes(10); + expect(actor.set_callback).toHaveBeenCalledTimes(11); expect(actor.set_callback).toHaveBeenCalledWith(callback.inventory_info, null); + // todo: Add here check for info removal. expect(actor.set_callback).toHaveBeenCalledWith(callback.article_info, null); expect(actor.set_callback).toHaveBeenCalledWith(callback.on_item_take, null); expect(actor.set_callback).toHaveBeenCalledWith(callback.on_item_drop, null); @@ -130,8 +131,9 @@ describe("ActorBinder", () => { expect(state).not.toBeNull(); expect(state.portableStore).not.toBeNull(); - expect(actor.set_callback).toHaveBeenCalledTimes(8); + expect(actor.set_callback).toHaveBeenCalledTimes(9); expect(actor.set_callback).toHaveBeenCalledWith(callback.inventory_info, expect.any(Function)); + // todo: Add here check for info removal. expect(actor.set_callback).toHaveBeenCalledWith(callback.take_item_from_box, expect.any(Function)); expect(actor.set_callback).toHaveBeenCalledWith(callback.on_item_take, expect.any(Function)); expect(actor.set_callback).toHaveBeenCalledWith(callback.on_item_drop, expect.any(Function)); @@ -246,7 +248,9 @@ describe("ActorBinder", () => { binder.reinit(); MockGameObject.callCallback(actor, callback.inventory_info, actor, "test-info"); - expect(eventsManager.emitEvent).toHaveBeenCalledWith(EGameEvent.ACTOR_INFO_UPDATE, actor, "test-info"); + expect(eventsManager.emitEvent).toHaveBeenCalledWith(EGameEvent.ACTOR_INFO_ADDED, actor, "test-info"); + + // todo: Test info removal callback here. MockGameObject.callCallback(actor, callback.take_item_from_box, box, item); expect(eventsManager.emitEvent).toHaveBeenCalledWith(EGameEvent.ACTOR_TAKE_BOX_ITEM, box, item); diff --git a/src/engine/core/binders/creature/ActorBinder.ts b/src/engine/core/binders/creature/ActorBinder.ts index 490f3a1e2..6edf3eff3 100644 --- a/src/engine/core/binders/creature/ActorBinder.ts +++ b/src/engine/core/binders/creature/ActorBinder.ts @@ -22,6 +22,7 @@ import { EventsManager } from "@/engine/core/managers/events/EventsManager"; import { SaveManager } from "@/engine/core/managers/save/SaveManager"; import { alifeConfig } from "@/engine/core/managers/simulation/AlifeConfig"; import { setStableAlifeObjectsUpdate, setUnlimitedAlifeObjectsUpdate } from "@/engine/core/utils/alife"; +import { updateInfoPortionCache } from "@/engine/core/utils/info_portion"; import { LuaLogger } from "@/engine/core/utils/logging"; const logger: LuaLogger = new LuaLogger($filename); @@ -186,8 +187,14 @@ export class ActorBinder extends object_binder { const object: GameObject = this.object; const eventsManager: EventsManager = this.eventsManager; + // Covers engine-side grants (dialog XML, tasks, info chains) invisible to lua utils. object.set_callback(callback.inventory_info, (object: GameObject, info: string) => { - eventsManager.emitEvent(EGameEvent.ACTOR_INFO_UPDATE, object, info); + updateInfoPortionCache(info, true); + eventsManager.emitEvent(EGameEvent.ACTOR_INFO_ADDED, object, info); + }); + object.set_callback(callback.inventory_info_removed, (object: GameObject, info: string) => { + updateInfoPortionCache(info, false); + eventsManager.emitEvent(EGameEvent.ACTOR_INFO_REMOVED, object, info); }); object.set_callback(callback.take_item_from_box, (box: GameObject, item: GameObject) => { eventsManager.emitEvent(EGameEvent.ACTOR_TAKE_BOX_ITEM, box, item); @@ -224,6 +231,7 @@ export class ActorBinder extends object_binder { const object: GameObject = this.object; object.set_callback(callback.inventory_info, null); + object.set_callback(callback.inventory_info_removed, null); object.set_callback(callback.article_info, null); object.set_callback(callback.on_item_take, null); object.set_callback(callback.on_item_drop, null); diff --git a/src/engine/core/database/actor.ts b/src/engine/core/database/actor.ts index 92f4aac60..1fdf3a8f8 100644 --- a/src/engine/core/database/actor.ts +++ b/src/engine/core/database/actor.ts @@ -4,6 +4,7 @@ import { type IRegistryObjectState } from "@/engine/core/database/database_types import { registerObject, unregisterObject } from "@/engine/core/database/objects"; import { registry } from "@/engine/core/database/registry"; import { type Actor } from "@/engine/core/objects/creature/Actor"; +import { invalidateInfoPortionsCache } from "@/engine/core/utils/info_portion"; /** * Register new actor entry in db. @@ -15,6 +16,9 @@ import { type Actor } from "@/engine/core/objects/creature/Actor"; export function registerActor(object: GameObject): IRegistryObjectState { registry.actor = object; + // New actor instance means unknown info portions state. + invalidateInfoPortionsCache(); + return registerObject(object); } @@ -26,6 +30,8 @@ export function registerActor(object: GameObject): IRegistryObjectState { export function unregisterActor(): void { unregisterObject(registry.actor); registry.actor = null as unknown as GameObject; + + invalidateInfoPortionsCache(); } /** diff --git a/src/engine/core/database/registry.ts b/src/engine/core/database/registry.ts index d2ffd8ba4..a5d75e97e 100644 --- a/src/engine/core/database/registry.ts +++ b/src/engine/core/database/registry.ts @@ -63,15 +63,18 @@ export const registry = { */ schemes: new LuaTable(), /** - * Session-constant derived facts only: entries are computed once, never invalidated and always - * safe to read. Transient or coherence-managed caches (per-tick snapshots, scratch buffers) - * belong next to their mutation points in domain modules instead. + * Session-constant derived facts and engine-mirrored state kept coherent via engine callbacks. + * Nested containers may be replaced wholesale on invalidation, so always access them through ref. */ cache: { /** * Memoized condlist for parsing simplification, where key is string data and value is parsed descriptor. */ conditionLists: new LuaMap(), + /** + * Mirror of actor info portions state. + */ + infoPortions: new LuaMap(), /** * Memoized game graph vertex -> level id lookups, constant for the loaded game graph. */ diff --git a/src/engine/core/managers/events/EventsManager.test.ts b/src/engine/core/managers/events/EventsManager.test.ts index 004c9e539..d23151916 100644 --- a/src/engine/core/managers/events/EventsManager.test.ts +++ b/src/engine/core/managers/events/EventsManager.test.ts @@ -13,7 +13,7 @@ describe("EventsManager", () => { it("should correctly initialize", () => { const manager: EventsManager = getManager(EventsManager); - expect(table.size(manager.callbacks)).toBe(129); + expect(table.size(manager.callbacks)).toBe(130); Object.keys(manager.callbacks).forEach((it) => { expect(table.size(manager.callbacks[it as unknown as EGameEvent])).toBe(0); diff --git a/src/engine/core/managers/events/events_types.ts b/src/engine/core/managers/events/events_types.ts index 638788fc1..306c2a8f3 100644 --- a/src/engine/core/managers/events/events_types.ts +++ b/src/engine/core/managers/events/events_types.ts @@ -63,9 +63,13 @@ export enum EGameEvent { */ ACTOR_DEATH, /** - * Actor info-portions update. + * Actor info-portion added. */ - ACTOR_INFO_UPDATE, + ACTOR_INFO_ADDED, + /** + * Actor info-portion removed. + */ + ACTOR_INFO_REMOVED, /** * Actor take item from box. */ diff --git a/src/engine/core/schemes/stalker/smartcover/actions/ActionSmartCoverUse.test.ts b/src/engine/core/schemes/stalker/smartcover/actions/ActionSmartCoverUse.test.ts index c253914b3..45a9f0ba4 100644 --- a/src/engine/core/schemes/stalker/smartcover/actions/ActionSmartCoverUse.test.ts +++ b/src/engine/core/schemes/stalker/smartcover/actions/ActionSmartCoverUse.test.ts @@ -13,6 +13,7 @@ import { SmartCover } from "@/engine/core/objects/smart_cover"; import { ISchemeSmartCoverState } from "@/engine/core/schemes/stalker/smartcover"; import { ActionSmartCoverUse } from "@/engine/core/schemes/stalker/smartcover/actions/ActionSmartCoverUse"; import { EScheme } from "@/engine/core/schemes/types"; +import { giveInfoPortion } from "@/engine/core/utils/info_portion"; import { mockRegisteredActor, mockSchemeState, resetRegistry } from "@/fixtures/engine"; jest.mock("@/engine/core/database/stalker"); @@ -257,7 +258,8 @@ describe("ActionSmartCoverUse", () => { }); it("should correctly check smart cover target with path", () => { - const { actorGameObject } = mockRegisteredActor(); + mockRegisteredActor(); + const object: GameObject = MockGameObject.mock(); const state: ISchemeSmartCoverState = mockSchemeState(EScheme.SMARTCOVER, {}); const action: ActionSmartCoverUse = new ActionSmartCoverUse(state); @@ -271,7 +273,7 @@ describe("ActionSmartCoverUse", () => { expect(action.firePosition).toEqual(MockVector.mock(1, 1, 1)); expect(action.object.set_smart_cover_target).toHaveBeenCalledWith(action.firePosition); - actorGameObject.give_info_portion("a"); + giveInfoPortion("a"); expect(() => action.updateSmartCoverTarget()).toThrow("There is no patrol path 'not-existing'."); }); diff --git a/src/engine/core/utils/info_portion.test.ts b/src/engine/core/utils/info_portion.test.ts index 93f0ee04f..11d27baf2 100644 --- a/src/engine/core/utils/info_portion.test.ts +++ b/src/engine/core/utils/info_portion.test.ts @@ -9,6 +9,7 @@ import { hasFewInfoPortions, hasInfoPortion, hasInfoPortions, + invalidateInfoPortionsCache, } from "@/engine/core/utils/info_portion"; import { mockRegisteredActor, resetRegistry } from "@/fixtures/engine"; @@ -46,7 +47,9 @@ describe("disableInfoPortion", () => { expect(hasInfoPortion(infoPortions.info_up_ac_mp5)).toBe(false); expect(registry.actor.disable_info_portion).toHaveBeenCalledTimes(1); - expect(registry.actor.has_info).toHaveBeenCalledTimes(3); + + // Gives / disables update the mirror granularly - no engine reads were needed at all. + expect(registry.actor.has_info).toHaveBeenCalledTimes(0); }); }); @@ -143,3 +146,53 @@ describe("hasFewInfoPortions", () => { ).toBe(false); }); }); + +describe("info portions mirror", () => { + beforeEach(() => { + resetRegistry(); + }); + + it("should memoize checks and re-read from engine after invalidation", () => { + const { actorGameObject } = mockRegisteredActor(); + + expect(hasInfoPortion(infoPortions.info_up_ac_mp5)).toBe(false); + + // Raw mock grant is invisible while memoized (engine grants update the mirror via actor binder callback). + actorGameObject.give_info_portion(infoPortions.info_up_ac_mp5); + + expect(hasInfoPortion(infoPortions.info_up_ac_mp5)).toBe(false); + expect(registry.actor.has_info).toHaveBeenCalledTimes(1); + + invalidateInfoPortionsCache(); + + expect(hasInfoPortion(infoPortions.info_up_ac_mp5)).toBe(true); + }); + + it("should update exact entries on lua-side mutations without dropping the mirror", () => { + mockRegisteredActor(); + + expect(hasInfoPortion(infoPortions.pri_a15_lights_off)).toBe(false); + + giveInfoPortion(infoPortions.info_up_ac_mp5); + + // Mutated entry is visible without engine reads, unrelated memoized entry survives. + expect(hasInfoPortion(infoPortions.info_up_ac_mp5)).toBe(true); + expect(hasInfoPortion(infoPortions.pri_a15_lights_off)).toBe(false); + expect(registry.actor.has_info).toHaveBeenCalledTimes(1); + }); + + it("should invalidate when the actor is re-registered", () => { + const { actorGameObject: firstActor } = mockRegisteredActor(); + + firstActor.give_info_portion(infoPortions.info_up_ac_mp5); + invalidateInfoPortionsCache(); + + expect(hasInfoPortion(infoPortions.info_up_ac_mp5)).toBe(true); + + // New actor instance - registerActor replaces the mirror, state re-read from the fresh object. + resetRegistry(); + mockRegisteredActor(); + + expect(hasInfoPortion(infoPortions.info_up_ac_mp5)).toBe(false); + }); +}); diff --git a/src/engine/core/utils/info_portion.ts b/src/engine/core/utils/info_portion.ts index 9638e394b..cd22b6880 100644 --- a/src/engine/core/utils/info_portion.ts +++ b/src/engine/core/utils/info_portion.ts @@ -1,5 +1,5 @@ -import { TCount, TName } from "xray16/lib"; -import { $filename, $isNil } from "xray16/macros"; +import { Nillable, TCount, TName } from "xray16/lib"; +import { $filename, $isNil, $isNotNil } from "xray16/macros"; import { TInfoPortion } from "@/engine/constants/info_portions/info_portions"; import { registry } from "@/engine/core/database/registry"; @@ -7,6 +7,27 @@ import { ELuaLoggerMode, LuaLogger } from "@/engine/core/utils/logging"; const logger: LuaLogger = new LuaLogger($filename, { file: "info_portions", mode: ELuaLoggerMode.DUAL }); +/** + * Replace the actor info portions mirror when the whole state becomes unknown. + * Called on actor lifecycle changes (register / unregister) - granular mutations are applied + * with `updateInfoPortionCache` instead, engine callbacks make every mutation observable. + */ +export function invalidateInfoPortionsCache(): void { + registry.cache.infoPortions = new LuaMap(); +} + +/** + * Update state of a single info portion in the mirror cache. + * Called from lua-side give / disable utils and from actor binder engine callbacks + * (`inventory_info` grants, `inventory_info_removed` disables). + * + * @param name - Info portion name to update. + * @param isActive - Whether the info portion is known to be active. + */ +export function updateInfoPortionCache(name: TName, isActive: boolean): void { + registry.cache.infoPortions.set(name, isActive); +} + /** * Give info portion to actor. * @@ -16,6 +37,7 @@ export function giveInfoPortion(name: TName): void { logger.info("Give info portion: %s", name); registry.actor.give_info_portion(name); + updateInfoPortionCache(name, true); } /** @@ -26,8 +48,9 @@ export function giveInfoPortion(name: TName): void { export function disableInfoPortion(name: TName): void { logger.info("Disable info portion: %s", name); - if (registry.actor?.has_info(name)) { + if (hasInfoPortion(name)) { registry.actor.disable_info_portion(name); + updateInfoPortionCache(name, false); } } @@ -35,10 +58,28 @@ export function disableInfoPortion(name: TName): void { * Whether actor has info portion set. * Fallbacks to false if actor is not registered. * + * Checks hit the mirror cache first - condlists re-check the same handful of infos many times + * per frame and each miss crosses into an engine O(n) vector scan, so steady-state checks are + * plain table reads. + * * @returns Whether actor has info portion set already. */ export function hasInfoPortion(name: TName): name is TInfoPortion { - return $isNil(registry.actor) ? false : registry.actor.has_info(name); + if ($isNil(registry.actor)) { + return false; + } + + const existing: Nillable = registry.cache.infoPortions.get(name); + + if ($isNotNil(existing)) { + return existing; + } + + const isActive: boolean = registry.actor.has_info(name); + + registry.cache.infoPortions.set(name, isActive); + + return isActive; } /** @@ -72,7 +113,7 @@ export function hasFewInfoPortions(names: Array, count: TCount): boolean let activeInfos: TCount = 0; for (let it = 0; it < names.length; it++) { - if (registry.actor.has_info(names[it])) { + if (hasInfoPortion(names[it])) { activeInfos += 1; if (activeInfos >= count) { diff --git a/src/engine/declarations/dialogs/zaton/zat_b106/soroka.test.ts b/src/engine/declarations/dialogs/zaton/zat_b106/soroka.test.ts index a6c30cb1b..00fdf61cf 100644 --- a/src/engine/declarations/dialogs/zaton/zat_b106/soroka.test.ts +++ b/src/engine/declarations/dialogs/zaton/zat_b106/soroka.test.ts @@ -9,6 +9,7 @@ import { infoPortions } from "@/engine/constants/info_portions"; import { weapons } from "@/engine/constants/items/weapons"; import { registry } from "@/engine/core/database"; import { TreasureManager } from "@/engine/core/managers/treasures"; +import { giveInfoPortion } from "@/engine/core/utils/info_portion"; import { giveMoneyToActor, transferItemsToActor } from "@/engine/core/utils/reward"; import { callBinding, mockRegisteredActor, resetRegistry } from "@/fixtures/engine"; @@ -101,7 +102,7 @@ describe("zat_b106_soroka_reward", () => { ]) { mockActorWith([]); resetFunctionMock(giveMoneyToActor); - registry.actor.give_info_portion(portion); + giveInfoPortion(portion); callDialogsBinding("zat_b106_soroka_reward"); @@ -136,7 +137,7 @@ describe("zat_b106_soroka_gone", () => { infoPortions.jup_b25_flint_blame_done_to_freedom, ]) { mockActorWith([]); - registry.actor.give_info_portion(portion); + giveInfoPortion(portion); expect(callDialogsBinding("zat_b106_soroka_gone")).toBe(true); } }); @@ -145,7 +146,7 @@ describe("zat_b106_soroka_not_gone", () => { it("should invert the Flint blame outcome", () => { expect(callDialogsBinding("zat_b106_soroka_not_gone", [registry.actor, MockGameObject.mock()])).toBe(true); - registry.actor.give_info_portion(infoPortions.jup_b25_flint_blame_done_to_duty); + giveInfoPortion(infoPortions.jup_b25_flint_blame_done_to_duty); expect(callDialogsBinding("zat_b106_soroka_not_gone", [registry.actor, MockGameObject.mock()])).toBe(false); }); }); diff --git a/src/engine/declarations/dialogs/zaton/zat_b29/advanced-artefacts-variants.test.ts b/src/engine/declarations/dialogs/zaton/zat_b29/advanced-artefacts-variants.test.ts index d08783398..bfaa89074 100644 --- a/src/engine/declarations/dialogs/zaton/zat_b29/advanced-artefacts-variants.test.ts +++ b/src/engine/declarations/dialogs/zaton/zat_b29/advanced-artefacts-variants.test.ts @@ -4,6 +4,7 @@ import { AnyArgs, AnyObject, TName, TSection } from "xray16/lib"; import { MockAlifeSimulator, MockGameObject } from "xray16/mocks"; import { registry } from "@/engine/core/database"; +import { giveInfoPortion } from "@/engine/core/utils/info_portion"; import { zatB29AfTable, zatB29InfopTable } from "@/engine/scripts/quests/zaton/zat_b29/advanced_artefacts_data"; import { callBinding, mockRegisteredActor, resetRegistry } from "@/fixtures/engine"; @@ -39,7 +40,7 @@ describe.each([1, 2, 3, 4, 5, 6, 7, 8])("advanced artefact variant %i", (index: mockActorWith([artefact]); expect(callDialogsBinding(`zat_b29_actor_has_adv_task_af_${index}`)).toBe(false); - registry.actor.give_info_portion(zatB29InfopTable.get(tableIndex)); + giveInfoPortion(zatB29InfopTable.get(tableIndex)); expect(callDialogsBinding(`zat_b29_actor_has_adv_task_af_${index}`)).toBe(true); }); @@ -48,11 +49,11 @@ describe.each([1, 2, 3, 4, 5, 6, 7, 8])("advanced artefact variant %i", (index: const artefact = zatB29AfTable.get(tableIndex); expect(callDialogsBinding(`zat_b29_actor_do_not_has_adv_task_af_${index}`)).toBe(false); - registry.actor.give_info_portion(zatB29InfopTable.get(tableIndex)); + giveInfoPortion(zatB29InfopTable.get(tableIndex)); expect(callDialogsBinding(`zat_b29_actor_do_not_has_adv_task_af_${index}`)).toBe(true); mockActorWith([artefact]); - registry.actor.give_info_portion(zatB29InfopTable.get(tableIndex)); + giveInfoPortion(zatB29InfopTable.get(tableIndex)); expect(callDialogsBinding(`zat_b29_actor_do_not_has_adv_task_af_${index}`)).toBe(false); }); }); diff --git a/src/engine/declarations/dialogs/zaton/zat_b29/advanced-artefacts.test.ts b/src/engine/declarations/dialogs/zaton/zat_b29/advanced-artefacts.test.ts index 80137b2ab..dfcf27427 100644 --- a/src/engine/declarations/dialogs/zaton/zat_b29/advanced-artefacts.test.ts +++ b/src/engine/declarations/dialogs/zaton/zat_b29/advanced-artefacts.test.ts @@ -8,6 +8,7 @@ import { TInfoPortion } from "@/engine/constants/info_portions"; import { weapons } from "@/engine/constants/items/weapons"; import { AnomalyZoneBinder } from "@/engine/core/binders/zones/AnomalyZoneBinder"; import { registry } from "@/engine/core/database"; +import { giveInfoPortion } from "@/engine/core/utils/info_portion"; import { giveMoneyToActor, transferItemsFromActor, transferItemsToActor } from "@/engine/core/utils/reward"; import { zatB29AfTable, @@ -49,7 +50,7 @@ describe("zat_b29_create_af_in_anomaly", () => { const zone = { setForcedSpawnOverride: jest.fn() } as unknown as AnomalyZoneBinder; // Index 16 maps to the `gravi` anomaly type, whose first zone is zat_b14. - registry.actor.give_info_portion(zatB29InfopBringTable.get(16) as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(16) as TInfoPortion); registry.anomalyZones.set("zat_b14_anomal_zone", zone); jest.spyOn(math, "random").mockImplementation(() => 1); @@ -62,7 +63,7 @@ describe("zat_b29_create_af_in_anomaly", () => { const zone = { setForcedSpawnOverride: jest.fn() } as unknown as AnomalyZoneBinder; // Index 19 maps to the `electra` anomaly type, whose second zone is zat_b100. - registry.actor.give_info_portion(zatB29InfopBringTable.get(19) as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(19) as TInfoPortion); registry.anomalyZones.set("zat_b100_anomal_zone", zone); jest.spyOn(math, "random").mockImplementation(() => 2); @@ -73,9 +74,9 @@ describe("zat_b29_create_af_in_anomaly", () => { }); describe("zat_b29_linker_give_adv_task", () => { it("should list every requested artefact and clear the bring markers", () => { - registry.actor.give_info_portion(zatB29InfopTable.get(16)); - registry.actor.give_info_portion(zatB29InfopTable.get(17)); - registry.actor.give_info_portion(zatB29InfopBringTable.get(16) as TInfoPortion); + giveInfoPortion(zatB29InfopTable.get(16)); + giveInfoPortion(zatB29InfopTable.get(17)); + giveInfoPortion(zatB29InfopBringTable.get(16) as TInfoPortion); const result: string = callDialogsBinding("zat_b29_linker_give_adv_task", [ registry.actor, @@ -100,7 +101,7 @@ describe("zat_b29_actor_has_adv_task_af", () => { mockActorWith([zatB29AfTable.get(16)]); expect(callDialogsBinding("zat_b29_actor_has_adv_task_af")).toBe(false); - registry.actor.give_info_portion(zatB29InfopBringTable.get(16) as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(16) as TInfoPortion); expect(callDialogsBinding("zat_b29_actor_has_adv_task_af")).toBe(true); }); }); @@ -109,7 +110,7 @@ describe("zat_b29_actor_do_not_has_adv_task_af", () => { expect(callDialogsBinding("zat_b29_actor_do_not_has_adv_task_af")).toBe(true); mockActorWith([zatB29AfTable.get(16)]); - registry.actor.give_info_portion(zatB29InfopBringTable.get(16) as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(16) as TInfoPortion); expect(callDialogsBinding("zat_b29_actor_do_not_has_adv_task_af")).toBe(false); }); }); @@ -118,7 +119,7 @@ describe("zat_b29_linker_get_adv_task_af", () => { const npc: GameObject = MockGameObject.mock(); mockActorWith([zatB29AfTable.get(16)]); - registry.actor.give_info_portion(zatB29InfopBringTable.get(16) as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(16) as TInfoPortion); callDialogsBinding("zat_b29_linker_get_adv_task_af", [registry.actor, npc]); @@ -130,7 +131,7 @@ describe("zat_b29_linker_get_adv_task_af", () => { const npc: GameObject = MockGameObject.mock(); mockActorWith([zatB29AfTable.get(20)]); - registry.actor.give_info_portion(zatB29InfopBringTable.get(20) as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(20) as TInfoPortion); callDialogsBinding("zat_b29_linker_get_adv_task_af", [registry.actor, npc]); @@ -139,14 +140,14 @@ describe("zat_b29_linker_get_adv_task_af", () => { it("should reduce both rewards when the artefact came from a rival", () => { mockActorWith([zatB29AfTable.get(16)]); - registry.actor.give_info_portion(zatB29InfopBringTable.get(16) as TInfoPortion); - registry.actor.give_info_portion("zat_b29_linker_take_af_from_rival" as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(16) as TInfoPortion); + giveInfoPortion("zat_b29_linker_take_af_from_rival" as TInfoPortion); callDialogsBinding("zat_b29_linker_get_adv_task_af", [registry.actor, MockGameObject.mock()]); expect(giveMoneyToActor).toHaveBeenLastCalledWith(12000); mockActorWith([zatB29AfTable.get(20)]); - registry.actor.give_info_portion(zatB29InfopBringTable.get(20) as TInfoPortion); - registry.actor.give_info_portion("zat_b29_linker_take_af_from_rival" as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(20) as TInfoPortion); + giveInfoPortion("zat_b29_linker_take_af_from_rival" as TInfoPortion); callDialogsBinding("zat_b29_linker_get_adv_task_af", [registry.actor, MockGameObject.mock()]); expect(giveMoneyToActor).toHaveBeenLastCalledWith(18000); }); @@ -172,7 +173,7 @@ describe("zat_b29_actor_exchange", () => { const npc: GameObject = MockGameObject.mock(); mockActorWith([weapons.wpn_groza]); - registry.actor.give_info_portion(zatB29InfopBringTable.get(16) as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(16) as TInfoPortion); (registry.actor as AnyObject).goodGun = weapons.wpn_groza; callDialogsBinding("zat_b29_actor_exchange", [registry.actor, npc]); @@ -184,7 +185,7 @@ describe("zat_b29_actor_exchange", () => { it("should do nothing without a remembered weapon", () => { (registry.actor as AnyObject).goodGun = null; - registry.actor.give_info_portion(zatB29InfopBringTable.get(16) as TInfoPortion); + giveInfoPortion(zatB29InfopBringTable.get(16) as TInfoPortion); callDialogsBinding("zat_b29_actor_exchange", [registry.actor, MockGameObject.mock()]); diff --git a/src/engine/declarations/dialogs/zaton/zat_b3/technician.test.ts b/src/engine/declarations/dialogs/zaton/zat_b3/technician.test.ts index 6e502804a..0a969d58c 100644 --- a/src/engine/declarations/dialogs/zaton/zat_b3/technician.test.ts +++ b/src/engine/declarations/dialogs/zaton/zat_b3/technician.test.ts @@ -9,6 +9,7 @@ import { ammo } from "@/engine/constants/items/ammo"; import { food } from "@/engine/constants/items/food"; import { misc } from "@/engine/constants/items/misc"; import { registry } from "@/engine/core/database"; +import { giveInfoPortion } from "@/engine/core/utils/info_portion"; import { transferItemsFromActor, transferItemsToActor, transferMoneyFromActor } from "@/engine/core/utils/reward"; import { callBinding, mockRegisteredActor, resetRegistry } from "@/fixtures/engine"; @@ -99,7 +100,7 @@ describe("zat_b3_actor_got_toolkit", () => { it("should ignore a toolkit that was already brought", () => { mockActorWith([misc.toolkit_1]); - registry.actor.give_info_portion(infoPortions.zat_b3_tech_instrument_1_brought); + giveInfoPortion(infoPortions.zat_b3_tech_instrument_1_brought); expect(callDialogsBinding("zat_b3_actor_got_toolkit")).toBe(false); }); @@ -138,16 +139,16 @@ describe("zat_b3_tech_drinks_precond", () => { it("should open while the tech has not seen the produce", () => { expect(callDialogsBinding("zat_b3_tech_drinks_precond")).toBe(true); - registry.actor.give_info_portion(infoPortions.zat_b3_tech_see_produce_62); + giveInfoPortion(infoPortions.zat_b3_tech_see_produce_62); expect(callDialogsBinding("zat_b3_tech_drinks_precond")).toBe(false); }); it("should reopen after the gauss repair until the tech stops drinking", () => { - registry.actor.give_info_portion(infoPortions.zat_b3_tech_see_produce_62); - registry.actor.give_info_portion(infoPortions.zat_b3_gauss_repaired); + giveInfoPortion(infoPortions.zat_b3_tech_see_produce_62); + giveInfoPortion(infoPortions.zat_b3_gauss_repaired); expect(callDialogsBinding("zat_b3_tech_drinks_precond")).toBe(true); - registry.actor.give_info_portion(infoPortions.zat_b3_tech_drink_no_more); + giveInfoPortion(infoPortions.zat_b3_tech_drink_no_more); expect(callDialogsBinding("zat_b3_tech_drinks_precond")).toBe(false); }); }); diff --git a/src/engine/declarations/dialogs/zaton/zat_b30/owl-items.test.ts b/src/engine/declarations/dialogs/zaton/zat_b30/owl-items.test.ts index f23f0d712..0eaac0798 100644 --- a/src/engine/declarations/dialogs/zaton/zat_b30/owl-items.test.ts +++ b/src/engine/declarations/dialogs/zaton/zat_b30/owl-items.test.ts @@ -8,6 +8,7 @@ import { artefacts } from "@/engine/constants/items/artefacts"; import { detectors } from "@/engine/constants/items/detectors"; import { questItems } from "@/engine/constants/items/quest_items"; import { registry } from "@/engine/core/database"; +import { giveInfoPortion } from "@/engine/core/utils/info_portion"; import { callBinding, mockRegisteredActor, resetRegistry } from "@/fixtures/engine"; function callDialogsBinding(name: TName, args: AnyArgs = []): T { @@ -71,7 +72,7 @@ describe("zat_b30_owl_stalker_trader_actor_has_item_to_sell", () => { mockActorWith([section]); expect(callDialogsBinding("zat_b30_owl_stalker_trader_actor_has_item_to_sell")).toBe(true); - registry.actor.give_info_portion(askedPortion); + giveInfoPortion(askedPortion); expect(callDialogsBinding("zat_b30_owl_stalker_trader_actor_has_item_to_sell")).toBe(false); } }); @@ -80,7 +81,7 @@ describe("zat_b30_owl_stalker_trader_actor_has_item_to_sell", () => { mockActorWith([detectors.detector_scientific]); expect(callDialogsBinding("zat_b30_owl_stalker_trader_actor_has_item_to_sell")).toBe(false); - registry.actor.give_info_portion(infoPortions.zat_b30_second_detector); + giveInfoPortion(infoPortions.zat_b30_second_detector); expect(callDialogsBinding("zat_b30_owl_stalker_trader_actor_has_item_to_sell")).toBe(true); }); }); @@ -94,7 +95,7 @@ describe("zat_b30_owl_can_say_about_heli", () => { for (const [searched] of pairs) { expect(callDialogsBinding("zat_b30_owl_can_say_about_heli")).toBe(true); - registry.actor.give_info_portion(searched); + giveInfoPortion(searched); } expect(callDialogsBinding("zat_b30_owl_can_say_about_heli")).toBe(false); @@ -106,7 +107,7 @@ describe("zat_b30_owl_can_say_about_heli", () => { infoPortions.zat_b30_owl_scat_2, infoPortions.zat_b30_owl_scat_3, ]) { - registry.actor.give_info_portion(portion); + giveInfoPortion(portion); } expect(callDialogsBinding("zat_b30_owl_can_say_about_heli")).toBe(false); diff --git a/src/fixtures/engine/mocks/registry.mock.ts b/src/fixtures/engine/mocks/registry.mock.ts index e21b4d61e..8ad173be9 100644 --- a/src/fixtures/engine/mocks/registry.mock.ts +++ b/src/fixtures/engine/mocks/registry.mock.ts @@ -37,6 +37,14 @@ export function mockRegisteredActor( */ export function resetRegistry(): void { registry.actor = null as unknown as GameObject; + registry.cache = { + conditionLists: new LuaMap(), + gameVertexLevelIds: new LuaMap(), + graphDistances: new LuaMap(), + graphDistancesCount: 0, + infoPortions: new LuaMap(), + levelNames: new LuaMap(), + }; registry.actorCombat = new LuaTable(); registry.artefacts.parentZones = new LuaTable(); registry.artefacts.points = new LuaTable();