From e550e038e2e8787278417d608ad94cea086f91c3 Mon Sep 17 00:00:00 2001 From: Neloreck Date: Mon, 19 Jun 2023 21:16:33 +0300 Subject: [PATCH] Separate file for scheme logics init. Tests for `initializeObjectSchemeLogic`, `initializeObjectSectionItems`. Rename state logics fields to camelcase. Use separate state interface to describe logics. More todos/comments. --- doc/TODO.md | 5 +- src/engine/configs/scripts/README.md | 5 + src/engine/core/database/ini.ts | 8 +- src/engine/core/database/logic.test.ts | 14 +- src/engine/core/database/logic.ts | 14 +- src/engine/core/database/types.ts | 78 +++--- .../core/managers/debug/DebugManager.ts | 6 +- .../core/managers/interaction/TradeManager.ts | 22 +- .../managers/interface/MapDisplayManager.ts | 6 +- src/engine/core/managers/world/DropManager.ts | 2 +- .../core/managers/world/ReleaseBodyManager.ts | 2 +- .../core/objects/binders/HelicopterBinder.ts | 4 +- .../objects/binders/creature/StalkerBinder.ts | 2 +- .../binders/physic/PhysicObjectBinder.ts | 4 +- .../objects/binders/zones/RestrictorBinder.ts | 4 +- .../server/smart_terrain/SmartTerrain.ts | 22 +- .../server/smart_terrain/jobs_general.ts | 10 +- src/engine/core/schemes/death/SchemeDeath.ts | 2 +- src/engine/core/schemes/meet/SchemeMeet.ts | 2 +- .../schemes/mob_walker/SchemeMobWalker.ts | 8 +- .../core/schemes/wounded/SchemeWounded.ts | 2 +- src/engine/core/utils/check/is.ts | 6 +- src/engine/core/utils/ini/config.ts | 10 +- .../core/utils/object/object_general.ts | 6 +- src/engine/core/utils/scheme/index.ts | 1 + .../core/utils/scheme/initialization.test.ts | 123 +++++++++ .../core/utils/scheme/initialization.ts | 236 ++++++++++++++++++ src/engine/core/utils/scheme/logic.test.ts | 34 +-- src/engine/core/utils/scheme/logic.ts | 179 +------------ src/engine/core/utils/scheme/switch.ts | 2 +- src/engine/core/utils/spawn.ts | 29 +-- src/engine/lib/configs/LogicsConfig.ts | 3 + .../xray/mocks/objects/AlifeSimulator.mock.ts | 15 +- 33 files changed, 532 insertions(+), 334 deletions(-) create mode 100644 src/engine/configs/scripts/README.md create mode 100644 src/engine/core/utils/scheme/initialization.test.ts create mode 100644 src/engine/core/utils/scheme/initialization.ts diff --git a/doc/TODO.md b/doc/TODO.md index 64db25232..ad314d575 100644 --- a/doc/TODO.md +++ b/doc/TODO.md @@ -3,10 +3,7 @@ ## 🧰 Main todos - Create xrf.ltx config and place extended game configs in it -- Scripts to unpack raw_gamedata for observation / usage -- Rework acdc perl script and add all.spawn editing utils -- Interop with level editor tools etc -- Add linux/windows custom CLI script +- Update statistics measurements to be more generic and less hardcoded ## 🧰 Tech diff --git a/src/engine/configs/scripts/README.md b/src/engine/configs/scripts/README.md new file mode 100644 index 000000000..67d1ae76d --- /dev/null +++ b/src/engine/configs/scripts/README.md @@ -0,0 +1,5 @@ +## todo: scripts logic ini fields descriptions + +## Logic fields + +`spawn` - name of section describing items to spawn on section activation diff --git a/src/engine/core/database/ini.ts b/src/engine/core/database/ini.ts index 0b1c1c76d..82ae0654d 100644 --- a/src/engine/core/database/ini.ts +++ b/src/engine/core/database/ini.ts @@ -8,9 +8,12 @@ import { ClientObject, IniFile, Optional, TName } from "@/engine/lib/types"; /** * Create dynamic ini file representation or get existing one from cache. + * Used to create in-memory ini files based on string content. + * Usually describes smart terrain job logic for an object based on assigned smart terrain. * * @param name - dynamic ini file name * @param content - dynamic ini file content to initialize, if it does not exist + * @returns multi return of file and filename */ export function loadDynamicIni(name: TName, content: Optional = null): LuaMultiReturn<[IniFile, TName]> { const nameKey: TName = DYNAMIC_LTX_PREFIX + name; @@ -30,7 +33,10 @@ export function loadDynamicIni(name: TName, content: Optional = null): L } /** - * Get ini file based on active object logic. + * Get ini file describing object script logic. + * In case of `customdata` get spawn ini. + * In case of dynamic LTX load it according to object job descriptor or from database. + * As fallback just load LTX file from configs by name. * * @param object - game object to get matching ini config * @param filename - ini file name diff --git a/src/engine/core/database/logic.test.ts b/src/engine/core/database/logic.test.ts index 86f8cd328..b970ba831 100644 --- a/src/engine/core/database/logic.test.ts +++ b/src/engine/core/database/logic.test.ts @@ -32,10 +32,10 @@ describe("'logic' database module", () => { } as IBaseSchemeState; state.job_ini = "test.ltx"; - state.ini_filename = "test2.ltx"; - state.section_logic = "section_ex"; + state.iniFilename = "test2.ltx"; + state.sectionLogic = "section_ex"; state.activeSection = "active_sect_ex"; - state.gulag_name = "gulag_name_ex"; + state.smartTerrainName = "gulag_name_ex"; state.activeScheme = EScheme.COMBAT; state.activationTime = 15_000; state.activationGameTime = time; @@ -124,10 +124,10 @@ describe("'logic' database module", () => { ]); expect(nextState.job_ini).toBe("test.ltx"); - expect(nextState.loaded_ini_filename).toBe("test2.ltx"); - expect(nextState.loaded_section_logic).toBe("section_ex"); - expect(nextState.loaded_active_section).toBe("active_sect_ex"); - expect(nextState.loaded_gulag_name).toBe("gulag_name_ex"); + expect(nextState.loadedIniFilename).toBe("test2.ltx"); + expect(nextState.loadedSectionLogic).toBe("section_ex"); + expect(nextState.loadedActiveSection).toBe("active_sect_ex"); + expect(nextState.loadedSmartTerrainName).toBe("gulag_name_ex"); expect(nextState.activationTime).toBe(15_000); expect(nextState.activationGameTime.toString()).toBe(time.toString()); diff --git a/src/engine/core/database/logic.ts b/src/engine/core/database/logic.ts index 313f64a33..1d5eeae91 100644 --- a/src/engine/core/database/logic.ts +++ b/src/engine/core/database/logic.ts @@ -38,10 +38,10 @@ export function saveObjectLogic(object: ClientObject, packet: NetPacket): void { openSaveMarker(packet, "object" + object.name()); packet.w_stringZ(state.job_ini ? state.job_ini : ""); - packet.w_stringZ(state.ini_filename ? state.ini_filename : ""); - packet.w_stringZ(state.section_logic ? state.section_logic : ""); + packet.w_stringZ(state.iniFilename ? state.iniFilename : ""); + packet.w_stringZ(state.sectionLogic ? state.sectionLogic : ""); packet.w_stringZ(state.activeSection ? state.activeSection : ""); - packet.w_stringZ(state.gulag_name ? state.gulag_name : ""); + packet.w_stringZ(state.smartTerrainName ? state.smartTerrainName : ""); packet.w_s32((state.activationTime || 0) - time_global()); writeTimeToPacket(packet, state.activationGameTime); @@ -73,10 +73,10 @@ export function loadObjectLogic(object: ClientObject, reader: NetProcessor): voi const gulagName: TName = reader.r_stringZ(); state.job_ini = jobIni === "" ? null : jobIni; - state.loaded_ini_filename = iniFilename === "" ? null : iniFilename; - state.loaded_section_logic = sectionLogic === "" ? null : sectionLogic; - state.loaded_active_section = activeSection === "" ? NIL : activeSection; - state.loaded_gulag_name = gulagName; + state.loadedIniFilename = iniFilename === "" ? null : iniFilename; + state.loadedSectionLogic = sectionLogic === "" ? null : sectionLogic; + state.loadedActiveSection = activeSection === "" ? NIL : activeSection; + state.loadedSmartTerrainName = gulagName; state.activationTime = reader.r_s32() + time_global(); state.activationGameTime = readTimeFromPacket(reader) as Time; diff --git a/src/engine/core/database/types.ts b/src/engine/core/database/types.ts index 16c7f0340..6f4c76674 100644 --- a/src/engine/core/database/types.ts +++ b/src/engine/core/database/types.ts @@ -18,13 +18,9 @@ import { } from "@/engine/lib/types"; /** - * Client-side registry of game objects logics and states. + * Client objects registry state logics descriptor. */ -export interface IRegistryObjectState extends Record> { - /** - * Client object reference to owner of the registry state. - */ - object: ClientObject; +export interface IRegistryObjectStateLogic { /** * todo; */ @@ -32,11 +28,7 @@ export interface IRegistryObjectState extends Record; - /** - * Dynamically stored flags / variables. - */ - portableStore: Optional>; + iniFilename: Optional; /** * Based on object type, marks compatible scheme types. */ @@ -52,11 +44,13 @@ export interface IRegistryObjectState extends Record; + sectionLogic: Optional; /** - * todo; + * Object smart terrain name. + * Used as base for schemes to pick up logic when smart terrains are capturing objects/squads. + * Having smart terrain allows selecting generic schemes defined for smart terrain. */ - gulag_name: Optional; + smartTerrainName: Optional; /** * todo; */ @@ -69,7 +63,40 @@ export interface IRegistryObjectState extends Record; + /** + * Describes last active logic file name when game was saved. + */ + loadedIniFilename: Optional; + /** + * Describes last active section logic section when game was saved. + */ + loadedSectionLogic: Optional; + /** + * Describes last active logic section when game was saved. + */ + loadedActiveSection: Optional; + /** + * Describes last active smart terrain name when game was saved. + */ + loadedSmartTerrainName: Optional; +} +/** + * Client objects registry state describing logics and state. + */ +export interface IRegistryObjectState extends Record>, IRegistryObjectStateLogic { + /** + * Client object reference to owner of the registry state. + */ + object: ClientObject; + /** + * Dynamically stored flags / variables. + */ + portableStore: Optional>; /** * todo; */ @@ -130,30 +157,11 @@ export interface IRegistryObjectState extends Record; - /** - * todo; - */ - job_ini: Optional; - /** - * todo; - */ - loaded_ini_filename: Optional; - /** - * todo; - */ - loaded_section_logic: Optional; - /** - * todo; - */ - loaded_active_section: Optional; - /** - * todo; - */ - loaded_gulag_name: Optional; } /** - * todo; + * Offline object state descriptor. + * Remember object active section when object switched offline. */ export interface IStoredOfflineObject { levelVertexId: Optional; diff --git a/src/engine/core/managers/debug/DebugManager.ts b/src/engine/core/managers/debug/DebugManager.ts index 19bd3fa8d..56f3a4b60 100644 --- a/src/engine/core/managers/debug/DebugManager.ts +++ b/src/engine/core/managers/debug/DebugManager.ts @@ -173,12 +173,12 @@ export class DebugManager extends AbstractCoreManager { const state: IRegistryObjectState = registry.objects.get(object.id()); logger.info("Object section:", object.section()); - logger.info("Ini file name:", state.ini_filename); - logger.info("Section logic:", state.section_logic); + logger.info("Ini file name:", state.iniFilename); + logger.info("Section logic:", state.sectionLogic); logger.info("Scheme type:", ESchemeType[state.schemeType]); logger.info("Active scheme:", state.activeScheme); logger.info("Active section:", state.activeSection); - logger.info("Active gulag name:", state.gulag_name); + logger.info("Smart terrain name:", state.smartTerrainName); logger.info("Activation time:", state.activationTime); logger.info("Activation game time:", gameTimeToString(state.activationGameTime)); logger.info("Portable store:", toJSON(state.portableStore)); diff --git a/src/engine/core/managers/interaction/TradeManager.ts b/src/engine/core/managers/interaction/TradeManager.ts index 0e46b1a9b..498663fb2 100644 --- a/src/engine/core/managers/interaction/TradeManager.ts +++ b/src/engine/core/managers/interaction/TradeManager.ts @@ -3,7 +3,7 @@ import { ini_file, time_global } from "xray16"; import { closeLoadMarker, closeSaveMarker, openSaveMarker, registry } from "@/engine/core/database"; import { openLoadMarker } from "@/engine/core/database/save_markers"; import { AbstractCoreManager } from "@/engine/core/managers/base/AbstractCoreManager"; -import { abort } from "@/engine/core/utils/assertion"; +import { abort, assert } from "@/engine/core/utils/assertion"; import { pickSectionFromCondList } from "@/engine/core/utils/ini/config"; import { parseConditionsList } from "@/engine/core/utils/ini/parse"; import { readIniNumber, readIniString } from "@/engine/core/utils/ini/read"; @@ -41,16 +41,25 @@ export interface ITradeManagerDescriptor { /** * todo; + * todo: Move periods to logicsConfig.TRADE */ export class TradeManager extends AbstractCoreManager { public static readonly TRADE_UPDATE_PERIOD: TDuration = 3_600_000; public static readonly TRADE_RESUPPLY_PERIOD: TDuration = 24 * 3_600_000; + /** + * todo; + */ + public static initializeForObject(object: ClientObject, iniFilePath: TPath): void { + TradeManager.getInstance().initializeForObject(object, iniFilePath); + } + /** * todo + * todo: Do not reuse data variable. */ - public initForObject(object: ClientObject, configFilePath: TPath): void { - logger.info("Init trade for:", object.name(), configFilePath); + public initializeForObject(object: ClientObject, configFilePath: TPath): void { + logger.info("Initialize trade for:", object.name(), configFilePath); const objectId: TNumberId = object.id(); @@ -60,9 +69,7 @@ export class TradeManager extends AbstractCoreManager { let data = readIniString(registry.trade.get(objectId).config, "trader", "buy_condition", true, ""); - if (data === null) { - abort("Incorrect trader settings. Cannot find buy_condition. [%s]->[%s]", object.name(), configFilePath); - } + assert(data, "Incorrect trader settings. Cannot find buy_condition. [%s]->[%s].", object.name(), configFilePath); registry.trade.get(objectId).buy_condition = parseConditionsList(data); @@ -155,6 +162,9 @@ export class TradeManager extends AbstractCoreManager { } } + /** + * todo: Description. + */ public getBuyDiscountForObject(objectId: TNumberId): number { const tradeDescriptor: ITradeManagerDescriptor = registry.trade.get(objectId); const data: string = readIniString(tradeDescriptor.config, "trader", "discounts", false, "", ""); diff --git a/src/engine/core/managers/interface/MapDisplayManager.ts b/src/engine/core/managers/interface/MapDisplayManager.ts index 36006ed62..274d34399 100644 --- a/src/engine/core/managers/interface/MapDisplayManager.ts +++ b/src/engine/core/managers/interface/MapDisplayManager.ts @@ -82,7 +82,7 @@ export class MapDisplayManager extends AbstractCoreManager { let spotSection; if (scheme === null || scheme === NIL) { - spotSection = readIniString(state.ini, state.section_logic, "show_spot", false, ""); + spotSection = readIniString(state.ini, state.sectionLogic, "show_spot", false, ""); } else { spotSection = readIniString(state.ini, section, "show_spot", false, ""); } @@ -94,7 +94,7 @@ export class MapDisplayManager extends AbstractCoreManager { const actor: ClientObject = registry.actor; let mapSpot: Optional = readIniString( state.ini, - state.section_logic, + state.sectionLogic, "level_spot", false, "" @@ -152,7 +152,7 @@ export class MapDisplayManager extends AbstractCoreManager { const objectId: Maybe = simulator.object(object.id())?.id; let mapSpot: Optional = readIniString( state.ini, - state.section_logic, + state.sectionLogic, "level_spot", false, "" diff --git a/src/engine/core/managers/world/DropManager.ts b/src/engine/core/managers/world/DropManager.ts index 514fb2925..dcb4046d8 100644 --- a/src/engine/core/managers/world/DropManager.ts +++ b/src/engine/core/managers/world/DropManager.ts @@ -184,7 +184,7 @@ export class DropManager extends AbstractCoreManager { const state: Optional = registry.objects.get(object.id()); - if (state.ini?.line_exist(state.section_logic, DropManager.DONT_SPAWN_LOOT_LTX_SECTION)) { + if (state.ini?.line_exist(state.sectionLogic, DropManager.DONT_SPAWN_LOOT_LTX_SECTION)) { return; } diff --git a/src/engine/core/managers/world/ReleaseBodyManager.ts b/src/engine/core/managers/world/ReleaseBodyManager.ts index 08b1e7dfa..0f6b431cc 100644 --- a/src/engine/core/managers/world/ReleaseBodyManager.ts +++ b/src/engine/core/managers/world/ReleaseBodyManager.ts @@ -175,7 +175,7 @@ export class ReleaseBodyManager extends AbstractCoreManager { const state: IRegistryObjectState = registry.objects.get(object.id()); const knownInfo: TSection = - readIniString(characterIni, state.section_logic, "known_info", false, "", null) || "known_info"; + readIniString(characterIni, state.sectionLogic, "known_info", false, "", null) || "known_info"; return characterIni.section_exist(knownInfo); } diff --git a/src/engine/core/objects/binders/HelicopterBinder.ts b/src/engine/core/objects/binders/HelicopterBinder.ts index e45fe31eb..bd9e21360 100644 --- a/src/engine/core/objects/binders/HelicopterBinder.ts +++ b/src/engine/core/objects/binders/HelicopterBinder.ts @@ -19,7 +19,7 @@ import { HeliCombat } from "@/engine/core/schemes/heli_move/HeliCombat"; import { getHeliFirer, HeliFire } from "@/engine/core/schemes/heli_move/HeliFire"; import { readIniNumber, readIniString } from "@/engine/core/utils/ini/read"; import { LuaLogger } from "@/engine/core/utils/logging"; -import { emitSchemeEvent, initializeObjectSchemeLogic } from "@/engine/core/utils/scheme/logic"; +import { emitSchemeEvent, initializeObjectSchemeLogic } from "@/engine/core/utils/scheme"; import { ClientObject, ESchemeType, @@ -97,7 +97,7 @@ export class HelicopterBinder extends object_binder { if (!this.initialized && actor) { this.initialized = true; - initializeObjectSchemeLogic(this.object, this.state, this.loaded, actor, ESchemeType.HELI); + initializeObjectSchemeLogic(this.object, this.state, this.loaded, ESchemeType.HELI); } if (this.state.activeSection !== null) { diff --git a/src/engine/core/objects/binders/creature/StalkerBinder.ts b/src/engine/core/objects/binders/creature/StalkerBinder.ts index 366d7500d..e80184f3d 100644 --- a/src/engine/core/objects/binders/creature/StalkerBinder.ts +++ b/src/engine/core/objects/binders/creature/StalkerBinder.ts @@ -427,7 +427,7 @@ export class StalkerBinder extends object_binder { statisticsManager.updateBestMonsterKilled(npc); } - const knownInfo: Optional = readIniString(state.ini!, state.section_logic, "known_info", false, "", null); + const knownInfo: Optional = readIniString(state.ini!, state.sectionLogic, "known_info", false, "", null); this.initializeInfoPortions(state.ini!, knownInfo); diff --git a/src/engine/core/objects/binders/physic/PhysicObjectBinder.ts b/src/engine/core/objects/binders/physic/PhysicObjectBinder.ts index f795fcba2..c9bdd7094 100644 --- a/src/engine/core/objects/binders/physic/PhysicObjectBinder.ts +++ b/src/engine/core/objects/binders/physic/PhysicObjectBinder.ts @@ -18,7 +18,7 @@ import { ESchemeEvent } from "@/engine/core/schemes"; import { pickSectionFromCondList } from "@/engine/core/utils/ini/config"; import { TConditionList } from "@/engine/core/utils/ini/types"; import { LuaLogger } from "@/engine/core/utils/logging"; -import { emitSchemeEvent, initializeObjectSchemeLogic } from "@/engine/core/utils/scheme/logic"; +import { emitSchemeEvent, initializeObjectSchemeLogic } from "@/engine/core/utils/scheme"; import { ClientObject, EScheme, @@ -142,7 +142,7 @@ export class PhysicObjectBinder extends object_binder { if (!this.initialized) { this.initialized = true; - initializeObjectSchemeLogic(this.object, this.state, this.loaded, registry.actor, ESchemeType.ITEM); + initializeObjectSchemeLogic(this.object, this.state, this.loaded, ESchemeType.ITEM); } const spawnIni: Optional = this.object.spawn_ini(); diff --git a/src/engine/core/objects/binders/zones/RestrictorBinder.ts b/src/engine/core/objects/binders/zones/RestrictorBinder.ts index 3e80d9ff6..9bd05144a 100644 --- a/src/engine/core/objects/binders/zones/RestrictorBinder.ts +++ b/src/engine/core/objects/binders/zones/RestrictorBinder.ts @@ -15,7 +15,7 @@ import { openLoadMarker } from "@/engine/core/database/save_markers"; import { GlobalSoundManager } from "@/engine/core/managers/sounds/GlobalSoundManager"; import { ESchemeEvent } from "@/engine/core/schemes"; import { LuaLogger } from "@/engine/core/utils/logging"; -import { emitSchemeEvent, initializeObjectSchemeLogic } from "@/engine/core/utils/scheme/logic"; +import { emitSchemeEvent, initializeObjectSchemeLogic } from "@/engine/core/utils/scheme"; import { NetPacket, Reader, ServerObject, TDuration, TNumberId } from "@/engine/lib/types"; import { ESchemeType } from "@/engine/lib/types/scheme"; @@ -75,7 +75,7 @@ export class RestrictorBinder extends object_binder { if (!this.isInitialized) { this.isInitialized = true; - initializeObjectSchemeLogic(this.object, this.state, this.isLoaded, registry.actor, ESchemeType.RESTRICTOR); + initializeObjectSchemeLogic(this.object, this.state, this.isLoaded, ESchemeType.RESTRICTOR); } if (this.state.activeSection !== null) { diff --git a/src/engine/core/objects/server/smart_terrain/SmartTerrain.ts b/src/engine/core/objects/server/smart_terrain/SmartTerrain.ts index 1357eb00a..c7429f9b7 100644 --- a/src/engine/core/objects/server/smart_terrain/SmartTerrain.ts +++ b/src/engine/core/objects/server/smart_terrain/SmartTerrain.ts @@ -67,8 +67,8 @@ import { configureObjectSchemes, getSectionToActivate, initializeObjectSchemeLogic, -} from "@/engine/core/utils/scheme/logic"; -import { switchObjectSchemeToSection } from "@/engine/core/utils/scheme/switch"; + switchObjectSchemeToSection, +} from "@/engine/core/utils/scheme"; import { getTableSize, isEmpty } from "@/engine/core/utils/table"; import { readTimeFromPacket, writeTimeToPacket } from "@/engine/core/utils/time"; import { toJSON } from "@/engine/core/utils/transform/json"; @@ -291,7 +291,6 @@ export class SmartTerrain extends cse_alife_smart_zone implements ISimulationTar registryState.object, registryState, false, - registry.actor, isStalker(object) ? ESchemeType.STALKER : ESchemeType.MONSTER ); } @@ -882,32 +881,33 @@ export class SmartTerrain extends cse_alife_smart_zone implements ISimulationTar objectJobDescriptor.begin_job = false; objectJobDescriptor.job_link = selectedJobLink; - const objectState: Optional = registry.objects.get(objectJobDescriptor.serverObject.id); + const state: Optional = registry.objects.get(objectJobDescriptor.serverObject.id); - if (objectState !== null) { - switchObjectSchemeToSection(objectState.object!, this.ltxConfig, NIL); + if (state !== null) { + switchObjectSchemeToSection(state.object, this.ltxConfig, NIL); } } if (!objectJobDescriptor.begin_job) { - const job_data = this.jobsData.get(objectJobDescriptor.job_id); + const jobData: ISmartTerrainJob = this.jobsData.get(objectJobDescriptor.job_id); - logger.info("Begin job in smart", this.name(), objectJobDescriptor.serverObject.name(), job_data.section); + logger.info("Begin job in smart", this.name(), objectJobDescriptor.serverObject.name(), jobData.section); hardResetOfflineObject(objectJobDescriptor.serverObject.id); objectJobDescriptor.begin_job = true; - const objectState: Optional = registry.objects.get(objectJobDescriptor.serverObject.id); + const state: Optional = registry.objects.get(objectJobDescriptor.serverObject.id); - if (objectState !== null) { - this.setupObjectLogic(objectState.object!); + if (state !== null) { + this.setupObjectLogic(state.object!); } } } /** * todo: Description. + * todo: Move to scheme utils as separate function, it is not method of smart terrain. */ public setupObjectLogic(object: ClientObject): void { logger.info("Setup logic:", this.name(), object.name()); diff --git a/src/engine/core/objects/server/smart_terrain/jobs_general.ts b/src/engine/core/objects/server/smart_terrain/jobs_general.ts index 5536e6373..e2e7a1554 100644 --- a/src/engine/core/objects/server/smart_terrain/jobs_general.ts +++ b/src/engine/core/objects/server/smart_terrain/jobs_general.ts @@ -9,7 +9,7 @@ import { getSchemeFromSection, parseConditionsList, parseWaypointData } from "@/ import { readIniBoolean, readIniNumber, readIniString } from "@/engine/core/utils/ini/read"; import { TConditionList } from "@/engine/core/utils/ini/types"; import { LuaLogger } from "@/engine/core/utils/logging"; -import { initializeObjectSchemeLogic } from "@/engine/core/utils/scheme/logic"; +import { initializeObjectSchemeLogic } from "@/engine/core/utils/scheme"; import { isInTimeInterval } from "@/engine/core/utils/time"; import { communities } from "@/engine/lib/constants/communities"; import { MAX_U16 } from "@/engine/lib/constants/memory"; @@ -55,7 +55,7 @@ export function loadSmartTerrainJobs(smartTerrain: SmartTerrain): LuaMultiReturn logger.info("Load job for smart:", smartTerrainName); - let ltx = + let ltx: string = "[meet@generic_lager]\n" + "close_distance = {=is_wounded} 0, 2\n" + "close_anim = {=is_wounded} nil, {!is_squad_commander} nil, {=actor_has_weapon} threat_na, talk_default\n" + @@ -1300,12 +1300,12 @@ export function setupSmartJobsAndLogicOnSpawn( if (needSetupLogic) { smartTerrain.setupObjectLogic(object); } else { - initializeObjectSchemeLogic(object, state, isLoaded, registry.actor, schemeType); + initializeObjectSchemeLogic(object, state, isLoaded, schemeType); } } else { - initializeObjectSchemeLogic(object, state, isLoaded, registry.actor, schemeType); + initializeObjectSchemeLogic(object, state, isLoaded, schemeType); } } else { - initializeObjectSchemeLogic(object, state, isLoaded, registry.actor, schemeType); + initializeObjectSchemeLogic(object, state, isLoaded, schemeType); } } diff --git a/src/engine/core/schemes/death/SchemeDeath.ts b/src/engine/core/schemes/death/SchemeDeath.ts index bccd60cb6..9f9423cac 100644 --- a/src/engine/core/schemes/death/SchemeDeath.ts +++ b/src/engine/core/schemes/death/SchemeDeath.ts @@ -48,7 +48,7 @@ export class SchemeDeath extends AbstractScheme { ): void { const deathSection: Optional = readIniString( objectState.ini!, - objectState.section_logic, + objectState.sectionLogic, "on_death", false, "", diff --git a/src/engine/core/schemes/meet/SchemeMeet.ts b/src/engine/core/schemes/meet/SchemeMeet.ts index e6bf1bb63..8bed3e00f 100644 --- a/src/engine/core/schemes/meet/SchemeMeet.ts +++ b/src/engine/core/schemes/meet/SchemeMeet.ts @@ -104,7 +104,7 @@ export class SchemeMeet extends AbstractScheme { ): void { const meetSection: TSection = scheme === null || scheme === NIL - ? readIniString(state.ini, state.section_logic, SchemeMeet.SCHEME_SECTION, false, "") + ? readIniString(state.ini, state.sectionLogic, SchemeMeet.SCHEME_SECTION, false, "") : readIniString(state.ini, section, SchemeMeet.SCHEME_SECTION, false, ""); SchemeMeet.initializeMeetScheme(object, state.ini, meetSection, state.meet as ISchemeMeetState, scheme); diff --git a/src/engine/core/schemes/mob_walker/SchemeMobWalker.ts b/src/engine/core/schemes/mob_walker/SchemeMobWalker.ts index 7f781e2e9..9841b51e2 100644 --- a/src/engine/core/schemes/mob_walker/SchemeMobWalker.ts +++ b/src/engine/core/schemes/mob_walker/SchemeMobWalker.ts @@ -6,7 +6,7 @@ import { abort } from "@/engine/core/utils/assertion"; import { getConfigSwitchConditions } from "@/engine/core/utils/ini/config"; import { readIniBoolean, readIniString } from "@/engine/core/utils/ini/read"; import { LuaLogger } from "@/engine/core/utils/logging"; -import { ClientObject, EScheme, ESchemeType, IniFile, TSection } from "@/engine/lib/types"; +import { ClientObject, EScheme, ESchemeType, IniFile, TName, TSection } from "@/engine/lib/types"; const logger: LuaLogger = new LuaLogger($filename); @@ -22,15 +22,15 @@ export class SchemeMobWalker extends AbstractScheme { ini: IniFile, scheme: EScheme, section: TSection, - gulag_name: string + smartTerrainName: TName ): void { const state: ISchemeMobWalkerState = AbstractScheme.assign(object, ini, scheme, section); state.logic = getConfigSwitchConditions(ini, section); state.state = getMonsterState(ini, section); state.no_reset = readIniBoolean(ini, section, "no_reset", false); - state.path_walk = readIniString(ini, section, "path_walk", true, gulag_name); - state.path_look = readIniString(ini, section, "path_look", false, gulag_name); + state.path_walk = readIniString(ini, section, "path_walk", true, smartTerrainName); + state.path_look = readIniString(ini, section, "path_look", false, smartTerrainName); if (state.path_walk === state.path_look) { abort( diff --git a/src/engine/core/schemes/wounded/SchemeWounded.ts b/src/engine/core/schemes/wounded/SchemeWounded.ts index 729228873..45f36415e 100644 --- a/src/engine/core/schemes/wounded/SchemeWounded.ts +++ b/src/engine/core/schemes/wounded/SchemeWounded.ts @@ -90,7 +90,7 @@ export class SchemeWounded extends AbstractScheme { ): void { const woundedSection: TSection = scheme === null || scheme === EScheme.NIL - ? readIniString(state.ini, state.section_logic, "wounded", false, "") + ? readIniString(state.ini, state.sectionLogic, "wounded", false, "") : readIniString(state.ini, section, "wounded", false, ""); SchemeWounded.initialize(object, state.ini, woundedSection, state.wounded as ISchemeWoundedState, scheme); diff --git a/src/engine/core/utils/check/is.ts b/src/engine/core/utils/check/is.ts index d7852e944..aab329e32 100644 --- a/src/engine/core/utils/check/is.ts +++ b/src/engine/core/utils/check/is.ts @@ -113,21 +113,21 @@ export function isStoryObject(object: ServerObject): boolean { * @returns whether object can be looted by stalkers from corpses. */ export function isLootableItem(object: ClientObject): boolean { - return lootableTable[object.section()] !== null; + return object.section() in lootableTable; } /** * @returns whether object is ammo-defined section item. */ export function isAmmoItem(object: ClientObject): boolean { - return ammo[object.section()] !== null; + return object.section() in ammo; } /** * @returns whether section is ammo-defined. */ export function isAmmoSection(section: TSection): section is TAmmoItem { - return ammo[section as TAmmoItem] !== null; + return section in ammo; } /** diff --git a/src/engine/core/utils/ini/config.ts b/src/engine/core/utils/ini/config.ts index 41fd12cca..5f3f49ccd 100644 --- a/src/engine/core/utils/ini/config.ts +++ b/src/engine/core/utils/ini/config.ts @@ -83,8 +83,6 @@ export function pickSectionFromCondList( object: Optional, condlist: TConditionList ): Optional { - assert(actor, "Trying to pick section from condlist when actor is not initialized."); - let randomValue: Optional = null; // -- math.random(100) for (const [, switchCondition] of condlist) { @@ -152,7 +150,7 @@ export function pickSectionFromCondList( } if (areInfoPortionConditionsMet) { - for (const [inum, infop] of pairs(switchCondition.infop_set)) { + for (const [, infop] of switchCondition.infop_set) { if (infop.func) { if (!getExtern("xr_effects")[infop.func]) { abort( @@ -238,10 +236,10 @@ export function getObjectConfigOverrides(ini: IniFile, section: TSection, object const state: IRegistryObjectState = registry.objects.get(object.id()); // todo: use ternary for state.section_logic - if (ini.line_exist(state.section_logic, "post_combat_time")) { + if (ini.line_exist(state.sectionLogic, "post_combat_time")) { const [minPostCombatTime, maxPostCombatTime] = readIniTwoNumbers( ini, - state.section_logic, + state.sectionLogic, "post_combat_time", logicsConfig.POST_COMBAT_IDLE.MIN / 1000, logicsConfig.POST_COMBAT_IDLE.MAX / 1000 @@ -266,7 +264,7 @@ export function getObjectConfigOverrides(ini: IniFile, section: TSection, object overrides.on_offline_condlist = parseConditionsList(readIniString(ini, section, "on_offline", false, "", NIL)); } else { overrides.on_offline_condlist = parseConditionsList( - readIniString(ini, state.section_logic, "on_offline", false, "", NIL) + readIniString(ini, state.sectionLogic, "on_offline", false, "", NIL) ); } diff --git a/src/engine/core/utils/object/object_general.ts b/src/engine/core/utils/object/object_general.ts index e70533fd6..705cb92da 100644 --- a/src/engine/core/utils/object/object_general.ts +++ b/src/engine/core/utils/object/object_general.ts @@ -312,7 +312,7 @@ export function initializeObjectTakeItemsEnabledState( ): void { const isTakeItemsEnabled: boolean = state.ini.line_exist(section, "take_items") ? readIniBoolean(state.ini, section, "take_items", false, true) - : readIniBoolean(state.ini, state.section_logic, "take_items", false, true); + : readIniBoolean(state.ini, state.sectionLogic, "take_items", false, true); object.take_items_enabled(isTakeItemsEnabled); } @@ -329,7 +329,7 @@ export function initializeObjectCanSelectWeaponState( let data: string = readIniString(state.ini, section, "can_select_weapon", false, "", ""); if (data === "") { - data = readIniString(state.ini, state.section_logic, "can_select_weapon", false, "", TRUE); + data = readIniString(state.ini, state.sectionLogic, "can_select_weapon", false, "", TRUE); } const conditionsList: TConditionList = parseConditionsList(data); @@ -399,7 +399,7 @@ export function resetObjectIgnoreThreshold( ): void { const thresholdSection: Optional = scheme === null || scheme === NIL - ? readIniString(state.ini, state.section_logic, "threshold", false, "") + ? readIniString(state.ini, state.sectionLogic, "threshold", false, "") : readIniString(state.ini, section, "threshold", false, ""); if (thresholdSection) { diff --git a/src/engine/core/utils/scheme/index.ts b/src/engine/core/utils/scheme/index.ts index 63cb0c9b3..6737d5249 100644 --- a/src/engine/core/utils/scheme/index.ts +++ b/src/engine/core/utils/scheme/index.ts @@ -1,3 +1,4 @@ +export * from "@/engine/core/utils/scheme/initialization"; export * from "@/engine/core/utils/scheme/logic"; export * from "@/engine/core/utils/scheme/setup"; export * from "@/engine/core/utils/scheme/switch"; diff --git a/src/engine/core/utils/scheme/initialization.test.ts b/src/engine/core/utils/scheme/initialization.test.ts new file mode 100644 index 000000000..72464f4f0 --- /dev/null +++ b/src/engine/core/utils/scheme/initialization.test.ts @@ -0,0 +1,123 @@ +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { alife } from "xray16"; + +import { IRegistryObjectState, registerObject, registry } from "@/engine/core/database"; +import { TAbstractSchemeConstructor } from "@/engine/core/schemes"; +import { SchemeCombatIgnore } from "@/engine/core/schemes/combat_ignore"; +import { SchemeDanger } from "@/engine/core/schemes/danger"; +import { SchemeDeath } from "@/engine/core/schemes/death"; +import { SchemeHear } from "@/engine/core/schemes/hear"; +import { SchemeMobCombat } from "@/engine/core/schemes/mob_combat"; +import { initializeObjectSchemeLogic, initializeObjectSectionItems } from "@/engine/core/utils/scheme/initialization"; +import { loadSchemeImplementations } from "@/engine/core/utils/scheme/setup"; +import { AnyObject, ClientObject, ESchemeType } from "@/engine/lib/types"; +import { resetFunctionMock } from "@/fixtures/utils"; +import { FILES_MOCKS, mockClientGameObject, mockIniFile } from "@/fixtures/xray"; + +describe("'scheme initialization' utils", () => { + beforeEach(() => { + registry.schemes = new LuaTable(); + registry.actor = null as unknown as ClientObject; + }); + + it("'configureObjectSchemes' should correctly configure scheme for objects", () => { + // todo; + }); + + it("'initializeObjectSchemeLogic' should correctly initialize scheme logic on init", () => {}); + + it("'initializeObjectSchemeLogic' should correctly initialize scheme logic on load", () => { + const object: ClientObject = mockClientGameObject(); + const state: IRegistryObjectState = registerObject(object); + + initializeObjectSchemeLogic(object, state, true, ESchemeType.MONSTER); + + expect(state.activeScheme).toBeUndefined(); + expect(state.activeSection).toBeUndefined(); + + state.loadedActiveSection = "mob_combat@test"; + state.loadedSectionLogic = "mob_combat@test"; + state.loadedIniFilename = "initializeObjectSchemeLogic-test.ltx"; + + (FILES_MOCKS as Record)[state.loadedIniFilename] = { + "mob_combat@test": {}, + }; + + const schemes: Array = [ + SchemeMobCombat, + SchemeCombatIgnore, + SchemeDeath, + SchemeDanger, + SchemeHear, + ]; + + loadSchemeImplementations($fromArray(schemes)); + + schemes.forEach((it) => { + jest.spyOn(it, "disable").mockImplementation(() => {}); + jest.spyOn(it, "activate").mockImplementation(() => {}); + jest.spyOn(it, "reset").mockImplementation(() => {}); + }); + + initializeObjectSchemeLogic(object, state, true, ESchemeType.MONSTER); + + expect(SchemeMobCombat.disable).toHaveBeenCalled(); + expect(SchemeCombatIgnore.disable).toHaveBeenCalled(); + + expect(SchemeCombatIgnore.activate).toHaveBeenCalled(); + expect(SchemeCombatIgnore.reset).toHaveBeenCalled(); + expect(SchemeHear.reset).toHaveBeenCalled(); + expect(SchemeMobCombat.activate).toHaveBeenCalled(); + }); + + it("'initializeObjectSectionItems' should correctly skip spawn if section does not exist", () => { + const object: ClientObject = mockClientGameObject(); + const state: IRegistryObjectState = registerObject(object); + + state.sectionLogic = "active@test"; + state.ini = mockIniFile("test.ltx", { + "active@test": {}, + }); + + resetFunctionMock(alife().create); + initializeObjectSectionItems(object, state); + expect(alife().create).not.toHaveBeenCalled(); + }); + + it("'initializeObjectSectionItems' should correctly spawn items on scheme activation", () => { + const object: ClientObject = mockClientGameObject(); + const state: IRegistryObjectState = registerObject(object); + + state.sectionLogic = "active@test"; + state.ini = mockIniFile("test.ltx", { + "active@test": { + spawn: "spawn@test", + }, + "spawn@test": { + augA3: 1, + AR15: 1, + }, + }); + + resetFunctionMock(alife().create); + initializeObjectSectionItems(object, state); + + expect(alife().create).toHaveBeenCalledTimes(2); + expect(alife().create).toHaveBeenNthCalledWith( + 1, + "augA3", + object.position(), + object.level_vertex_id(), + object.game_vertex_id(), + object.id() + ); + expect(alife().create).toHaveBeenNthCalledWith( + 2, + "AR15", + object.position(), + object.level_vertex_id(), + object.game_vertex_id(), + object.id() + ); + }); +}); diff --git a/src/engine/core/utils/scheme/initialization.ts b/src/engine/core/utils/scheme/initialization.ts new file mode 100644 index 000000000..12f1a09e4 --- /dev/null +++ b/src/engine/core/utils/scheme/initialization.ts @@ -0,0 +1,236 @@ +import { ini_file } from "xray16"; + +import { getObjectLogicIniConfig, IRegistryObjectState, registry } from "@/engine/core/database"; +import { TradeManager } from "@/engine/core/managers/interaction/TradeManager"; +import { SmartTerrain } from "@/engine/core/objects"; +import { ISmartTerrainJob } from "@/engine/core/objects/server/smart_terrain/types"; +import { ESchemeEvent, IBaseSchemeState } from "@/engine/core/schemes"; +import { assert } from "@/engine/core/utils/assertion"; +import { readIniNumber, readIniString } from "@/engine/core/utils/ini/read"; +import { LuaLogger } from "@/engine/core/utils/logging"; +import { getObjectSmartTerrain } from "@/engine/core/utils/object/object_general"; +import { + activateSchemeBySection, + emitSchemeEvent, + enableObjectBaseSchemes, + getSectionToActivate, +} from "@/engine/core/utils/scheme/logic"; +import { disableObjectBaseSchemes } from "@/engine/core/utils/scheme/setup"; +import { spawnItemsForObject } from "@/engine/core/utils/spawn"; +import { logicsConfig } from "@/engine/lib/configs/LogicsConfig"; +import { TInventoryItem } from "@/engine/lib/constants/items"; +import { ERelation } from "@/engine/lib/constants/relations"; +import { + ClientObject, + EClientObjectRelation, + EScheme, + ESchemeType, + IniFile, + Optional, + TCount, + TName, + TPath, + TSection, +} from "@/engine/lib/types"; + +const logger: LuaLogger = new LuaLogger($filename); + +/** + * todo; + * todo; + * todo; + */ +export function configureObjectSchemes( + object: ClientObject, + ini: IniFile, + iniName: TName, + schemeType: ESchemeType, + section: TSection, + smartTerrainName: Optional +): IniFile { + const state: IRegistryObjectState = registry.objects.get(object.id()); + + // Deactivate previous scheme section. + if (state.activeSection) { + emitSchemeEvent(object, state[state.activeScheme as EScheme] as IBaseSchemeState, ESchemeEvent.DEACTIVATE, object); + } + + let actualIni: IniFile; + let actualIniFilename: TName; + + if (!ini.section_exist(section)) { + assert( + smartTerrainName === "", + "Object '%s': unable to find section '%s' in '%s' and has no assigned smart terrain.", + object.name(), + section, + iniName + ); + + actualIniFilename = iniName; + actualIni = ini; + } else { + const filename: Optional = readIniString(ini, section, "cfg", false, ""); + + if (filename !== null) { + actualIniFilename = filename; + actualIni = new ini_file(filename); + + assert( + actualIni.section_exist(section), + "object '%s' configuration file [%s] !FOUND || section [logic] isn't assigned ", + object.name(), + filename + ); + + return configureObjectSchemes(object, actualIni, actualIniFilename, schemeType, section, smartTerrainName); + } else { + if (schemeType === ESchemeType.STALKER || schemeType === ESchemeType.MONSTER) { + const currentSmart: Optional = getObjectSmartTerrain(object); + + if (currentSmart !== null) { + const job: Optional = currentSmart.getJob(object.id()); + + state.job_ini = job ? (job.ini_path as TName) : null; + } + } + + actualIniFilename = iniName; + actualIni = ini; + } + } + + disableObjectBaseSchemes(object, schemeType); + enableObjectBaseSchemes(object, actualIni, schemeType, section); + + state.activeSection = null; + state.activeScheme = null; + state.smartTerrainName = smartTerrainName; + + state.schemeType = schemeType; + state.ini = actualIni; + state.iniFilename = actualIniFilename; + state.sectionLogic = section; + + // todo: Move to separate activation methods? + if (schemeType === ESchemeType.STALKER) { + const tradeIni: TPath = readIniString( + actualIni, + section, + "trade", + false, + "", + logicsConfig.TRADE.DEFAULT_TRADE_LTX_PATH + ); + + TradeManager.initializeForObject(object, tradeIni); + initializeObjectSectionItems(object, state); + } + + return state.ini; +} + +/** + * Initialize object scheme logics on object logics change/load/spawn. + * Called on first object update or when smart terrain assignments change and object has to get new logic. + * + * @param object - target client object + * @param state - target object registry state + * @param isLoading - whether initialization is happening on object load + * @param schemeType - type of object schemes applied + */ +export function initializeObjectSchemeLogic( + object: ClientObject, + state: IRegistryObjectState, + isLoading: boolean, + schemeType: ESchemeType +): void { + if (isLoading) { + const loadingIniFilename: Optional = state.loadedIniFilename; + + if (loadingIniFilename) { + const iniFile: IniFile = configureObjectSchemes( + object, + getObjectLogicIniConfig(object, loadingIniFilename), + loadingIniFilename, + schemeType, + state.loadedSectionLogic as TSection, + state.loadedSmartTerrainName + ); + + activateSchemeBySection( + object, + iniFile, + state.loadedActiveSection as TSection, + state.loadedSmartTerrainName, + true + ); + } + } else { + const iniFilename: TName = ""; + const iniFile: IniFile = configureObjectSchemes( + object, + getObjectLogicIniConfig(object, iniFilename), + iniFilename, + schemeType, + "logic", + "" + ); + + const section: TSection = getSectionToActivate(object, iniFile, "logic"); + + activateSchemeBySection(object, iniFile, section, state.smartTerrainName, false); + + const relation: Optional = readIniString(iniFile, "logic", "relation", false, "") as ERelation; + + if (relation !== null) { + switch (relation) { + case ERelation.NEUTRAL: + object.set_relation(EClientObjectRelation.NEUTRAL, registry.actor); + break; + case ERelation.ENEMY: + object.set_relation(EClientObjectRelation.ENEMY, registry.actor); + break; + case ERelation.FRIEND: + object.set_relation(EClientObjectRelation.FRIEND, registry.actor); + break; + } + } + + const sympathy: Optional = readIniNumber(iniFile, "logic", "sympathy", false); + + if (sympathy !== null) { + object.set_sympathy(sympathy); + } + } +} + +/** + * Spawn object items on logics section change for an object. + * todo: description + */ +export function initializeObjectSectionItems(object: ClientObject, state: IRegistryObjectState): void { + const spawnItemsSection: Optional = readIniString(state.ini, state.sectionLogic, "spawn", false, "", null); + + if (spawnItemsSection === null) { + return; + } + + logger.info("Initialize section spawn items for object:", object.name()); + + const itemsToSpawn: LuaTable = new LuaTable(); + const itemSectionsCount: TCount = state.ini.line_count(spawnItemsSection); + + // todo: Probably do everything in one loop? The only problem is duplicated sections in such case. + for (const it of $range(0, itemSectionsCount - 1)) { + const [, id, value] = state.ini.r_line(spawnItemsSection, it, "", ""); + + itemsToSpawn.set(id as TInventoryItem, value === "" ? 1 : (tonumber(value) as TCount)); + } + + for (const [id, count] of itemsToSpawn) { + if (object.object(id) === null) { + spawnItemsForObject(object, id, count); + } + } +} diff --git a/src/engine/core/utils/scheme/logic.test.ts b/src/engine/core/utils/scheme/logic.test.ts index d24c63b53..6cb624e18 100644 --- a/src/engine/core/utils/scheme/logic.test.ts +++ b/src/engine/core/utils/scheme/logic.test.ts @@ -42,7 +42,7 @@ import { disableInfo, giveInfo } from "@/engine/core/utils/info_portion"; import { activateSchemeBySection, emitSchemeEvent, - enableObjectGenericSchemes, + enableObjectBaseSchemes, getSectionToActivate, isSectionActive, resetObjectGenericSchemesOnSectionSwitch, @@ -353,7 +353,7 @@ describe("'scheme logic' utils", () => { }); }); - it("'enableObjectGenericSchemes' should correctly enables schemes for heli", () => { + it("'enableObjectBaseSchemes' should correctly enables schemes for heli", () => { const object: ClientObject = mockClientGameObject(); const ini: IniFile = mockIniFile("test.ltx", { "sr_idle@first": {}, @@ -366,14 +366,14 @@ describe("'scheme logic' utils", () => { loadSchemeImplementation(SchemeHit); - enableObjectGenericSchemes(object, ini, ESchemeType.HELI, "sr_idle@first"); + enableObjectBaseSchemes(object, ini, ESchemeType.HELI, "sr_idle@first"); expect(SchemeHit.activate).not.toHaveBeenCalled(); - enableObjectGenericSchemes(object, ini, ESchemeType.HELI, "sr_idle@second"); + enableObjectBaseSchemes(object, ini, ESchemeType.HELI, "sr_idle@second"); expect(SchemeHit.activate).toHaveBeenCalledWith(object, ini, EScheme.HIT, "hit@another"); }); - it("'enableObjectGenericSchemes' should correctly enables schemes for items", () => { + it("'enableObjectBaseSchemes' should correctly enables schemes for items", () => { const object: ClientObject = mockClientGameObject(); const ini: IniFile = mockIniFile("test.ltx", { "sr_idle@first": {}, @@ -386,14 +386,14 @@ describe("'scheme logic' utils", () => { loadSchemeImplementation(SchemePhysicalOnHit); - enableObjectGenericSchemes(object, ini, ESchemeType.ITEM, "sr_idle@first"); + enableObjectBaseSchemes(object, ini, ESchemeType.ITEM, "sr_idle@first"); expect(SchemePhysicalOnHit.activate).not.toHaveBeenCalled(); - enableObjectGenericSchemes(object, ini, ESchemeType.ITEM, "sr_idle@second"); + enableObjectBaseSchemes(object, ini, ESchemeType.ITEM, "sr_idle@second"); expect(SchemePhysicalOnHit.activate).toHaveBeenCalledWith(object, ini, EScheme.PH_ON_HIT, "ph_on_hit@another"); }); - it("'enableObjectGenericSchemes' should correctly enables schemes for monsters", () => { + it("'enableObjectBaseSchemes' should correctly enables schemes for monsters", () => { const object: ClientObject = mockClientGameObject(); const state: IRegistryObjectState = registerObject(object); const ini: IniFile = mockIniFile("test.ltx", { @@ -417,14 +417,14 @@ describe("'scheme logic' utils", () => { $fromArray([SchemeMobCombat, SchemeMobDeath, SchemeHit, SchemeCombatIgnore]) ); - enableObjectGenericSchemes(object, ini, ESchemeType.MONSTER, "sr_idle@first"); + enableObjectBaseSchemes(object, ini, ESchemeType.MONSTER, "sr_idle@first"); expect(SchemeHit.activate).not.toHaveBeenCalled(); expect(SchemeMobCombat.activate).not.toHaveBeenCalled(); expect(SchemeMobDeath.activate).not.toHaveBeenCalled(); expect(SchemeCombatIgnore.activate).toHaveBeenCalledWith(object, ini, EScheme.COMBAT_IGNORE, null); expect(object.invulnerable).toHaveBeenCalledTimes(2); - enableObjectGenericSchemes(object, ini, ESchemeType.MONSTER, "sr_idle@second"); + enableObjectBaseSchemes(object, ini, ESchemeType.MONSTER, "sr_idle@second"); expect(SchemeHit.activate).toHaveBeenCalledWith(object, ini, EScheme.HIT, "hit@another"); expect(SchemeMobCombat.activate).toHaveBeenCalledWith(object, ini, EScheme.MOB_COMBAT, "mob_combat@another"); expect(SchemeMobDeath.activate).toHaveBeenCalledWith(object, ini, EScheme.MOB_DEATH, "mob_death@another"); @@ -432,7 +432,7 @@ describe("'scheme logic' utils", () => { expect(object.invulnerable).toHaveBeenCalledTimes(4); }); - it("'enableObjectGenericSchemes' should correctly enables schemes for stalkers", () => { + it("'enableObjectBaseSchemes' should correctly enables schemes for stalkers", () => { const object: ClientObject = mockClientGameObject(); const state: IRegistryObjectState = registerObject(object); const ini: IniFile = mockIniFile("test.ltx", { @@ -472,7 +472,7 @@ describe("'scheme logic' utils", () => { loadSchemeImplementations($fromArray(schemes)); registerActor(mockClientGameObject()); - enableObjectGenericSchemes(object, ini, ESchemeType.STALKER, "sr_idle@first"); + enableObjectBaseSchemes(object, ini, ESchemeType.STALKER, "sr_idle@first"); expect(SchemeAbuse.activate).toHaveBeenCalledWith(object, ini, EScheme.ABUSE, "sr_idle@first"); expect(SchemeWounded.activate).toHaveBeenCalledWith(object, ini, EScheme.WOUNDED, null); expect(SchemeHelpWounded.activate).toHaveBeenCalledWith(object, ini, EScheme.HELP_WOUNDED, null); @@ -487,7 +487,7 @@ describe("'scheme logic' utils", () => { expect(SchemeMeet.activate).toHaveBeenCalledWith(object, ini, EScheme.MEET, null); expect(SchemeReachTask.activate).toHaveBeenCalledWith(object, ini, EScheme.REACH_TASK, null); - enableObjectGenericSchemes(object, ini, ESchemeType.STALKER, "sr_idle@second"); + enableObjectBaseSchemes(object, ini, ESchemeType.STALKER, "sr_idle@second"); expect(SchemeAbuse.activate).toHaveBeenNthCalledWith(2, object, ini, EScheme.ABUSE, "sr_idle@second"); expect(SchemeWounded.activate).toHaveBeenNthCalledWith(2, object, ini, EScheme.WOUNDED, "wounded@another"); expect(SchemeHelpWounded.activate).toHaveBeenNthCalledWith(2, object, ini, EScheme.HELP_WOUNDED, null); @@ -508,14 +508,6 @@ describe("'scheme logic' utils", () => { expect(object.disable_info_portion).toHaveBeenNthCalledWith(2, "d"); }); - it("'configureObjectSchemes' should correctly configure scheme for objects", () => { - // todo; - }); - - it("'initializeObjectSchemeLogic' should correctly initialize scheme logic", () => { - // todo; - }); - it("'resetObjectGenericSchemesOnSectionSwitch' should correctly reset base schemes", () => { registerActor(mockClientGameObject()); diff --git a/src/engine/core/utils/scheme/logic.ts b/src/engine/core/utils/scheme/logic.ts index 1725d52c5..f3ee6d9bd 100644 --- a/src/engine/core/utils/scheme/logic.ts +++ b/src/engine/core/utils/scheme/logic.ts @@ -1,7 +1,6 @@ -import { callback, clsid, game, ini_file, time_global } from "xray16"; +import { callback, clsid, game, time_global } from "xray16"; -import { getObjectLogicIniConfig, IRegistryObjectState, IStoredOfflineObject, registry } from "@/engine/core/database"; -import { TradeManager } from "@/engine/core/managers/interaction/TradeManager"; +import { IRegistryObjectState, IStoredOfflineObject, registry } from "@/engine/core/database"; import { MapDisplayManager } from "@/engine/core/managers/interface/MapDisplayManager"; import { SmartTerrain } from "@/engine/core/objects"; import { ISmartTerrainJob } from "@/engine/core/objects/server/smart_terrain/types"; @@ -12,10 +11,10 @@ import { ObjectRestrictionsManager, TAbstractSchemeConstructor, } from "@/engine/core/schemes"; -import { abort, assert, assertDefined } from "@/engine/core/utils/assertion"; +import { assert, assertDefined } from "@/engine/core/utils/assertion"; import { getObjectConfigOverrides, pickSectionFromCondList } from "@/engine/core/utils/ini/config"; import { getSchemeFromSection } from "@/engine/core/utils/ini/parse"; -import { readIniConditionList, readIniNumber, readIniString } from "@/engine/core/utils/ini/read"; +import { readIniConditionList, readIniString } from "@/engine/core/utils/ini/read"; import { LuaLogger } from "@/engine/core/utils/logging"; import { getObjectSmartTerrain, @@ -28,23 +27,15 @@ import { sendToNearestAccessibleVertex, setObjectInfo, } from "@/engine/core/utils/object/object_general"; -import { disableObjectBaseSchemes } from "@/engine/core/utils/scheme/setup"; -import { spawnDefaultObjectItems } from "@/engine/core/utils/spawn"; -import { ERelation } from "@/engine/lib/constants/relations"; import { NIL } from "@/engine/lib/constants/words"; import { AnyArgs, AnyContextualCallable, ClientObject, - EClientObjectRelation, EScheme, ESchemeType, IniFile, Optional, - TCount, - TName, - TNumberId, - TPath, TSection, } from "@/engine/lib/types"; @@ -212,102 +203,14 @@ export function activateSchemeBySection( } /** - * todo; - * todo; - * todo; - */ -export function configureObjectSchemes( - object: ClientObject, - ini: IniFile, - iniName: TName, - schemeType: ESchemeType, - section: TSection, - smartTerrainName: Optional -): IniFile { - const objectId: TNumberId = object.id(); - const state: IRegistryObjectState = registry.objects.get(objectId); - - // Deactivate previous scheme section. - if (state.activeSection) { - emitSchemeEvent(object, state[state.activeScheme!]!, ESchemeEvent.DEACTIVATE, object); - } - - let actualIni: IniFile; - let actualIniFilename: TName; - - if (!ini.section_exist(section)) { - if (smartTerrainName === "") { - actualIniFilename = iniName; - actualIni = ini; - } else { - abort("ERROR: object '%s': unable to find section '%s' in '%s'", object.name(), section, tostring(iniName)); - } - } else { - const filename: Optional = readIniString(ini, section, "cfg", false, ""); - - if (filename !== null) { - actualIniFilename = filename; - actualIni = new ini_file(filename); - - assert( - actualIni.section_exist(section), - "object '%s' configuration file [%s] !FOUND || section [logic] isn't assigned ", - object.name(), - filename - ); - - return configureObjectSchemes(object, actualIni, actualIniFilename, schemeType, section, smartTerrainName); - } else { - if (schemeType === ESchemeType.STALKER || schemeType === ESchemeType.MONSTER) { - const currentSmart: Optional = getObjectSmartTerrain(object); - - if (currentSmart !== null) { - const job: any = currentSmart.getJob(objectId); - - if (job) { - state.job_ini = job.ini_path; - } else { - state.job_ini = null; - } - } - } - - actualIniFilename = iniName; - actualIni = ini; - } - } - - disableObjectBaseSchemes(object, schemeType); - enableObjectGenericSchemes(object, actualIni, schemeType, section); - - state.activeSection = null; - state.activeScheme = null; - state.gulag_name = smartTerrainName; - - state.schemeType = schemeType; - state.ini = actualIni; - state.ini_filename = actualIniFilename; - state.section_logic = section; - - if (schemeType === ESchemeType.STALKER) { - const tradeIni: TPath = readIniString(actualIni, section, "trade", false, "", "misc\\trade\\trade_generic.ltx"); - - TradeManager.getInstance().initForObject(object, tradeIni); - spawnDefaultObjectItems(object, state); - } - - return state.ini; -} - -/** - * Enable generic schemes for object on logics activation. + * Enable generic base schemes for object on logics activation. * * @param object - target client object * @param ini - target object ini configuration * @param schemeType - type of object applied scheme * @param section - next active logic section */ -export function enableObjectGenericSchemes( +export function enableObjectBaseSchemes( object: ClientObject, ini: IniFile, schemeType: ESchemeType, @@ -425,74 +328,6 @@ export function enableObjectGenericSchemes( } } -/** - * todo; - * todo; - * todo; Move to initialize.ts file. - */ -export function initializeObjectSchemeLogic( - object: ClientObject, - state: IRegistryObjectState, - isLoaded: boolean, - actor: ClientObject, - schemeType: ESchemeType -): void { - logger.info("Initialize object:", object.name(), ESchemeType[schemeType], isLoaded); - - if (isLoaded) { - const iniFilename: Optional = state.loaded_ini_filename; - - if (iniFilename) { - const iniFile: IniFile = configureObjectSchemes( - object, - getObjectLogicIniConfig(object, iniFilename), - iniFilename, - schemeType, - state.loaded_section_logic as TSection, - state.loaded_gulag_name - ); - - activateSchemeBySection(object, iniFile, state.loaded_active_section as TSection, state.loaded_gulag_name, true); - } - } else { - const iniFilename: TName = ""; - const iniFile: IniFile = configureObjectSchemes( - object, - getObjectLogicIniConfig(object, iniFilename), - iniFilename, - schemeType, - "logic", - "" - ); - - const section: TSection = getSectionToActivate(object, iniFile, "logic"); - - activateSchemeBySection(object, iniFile, section, state.gulag_name, false); - - const relation: Optional = readIniString(iniFile, "logic", "relation", false, "") as ERelation; - - if (relation !== null) { - switch (relation) { - case ERelation.NEUTRAL: - object.set_relation(EClientObjectRelation.NEUTRAL, registry.actor); - break; - case ERelation.ENEMY: - object.set_relation(EClientObjectRelation.ENEMY, registry.actor); - break; - case ERelation.FRIEND: - object.set_relation(EClientObjectRelation.FRIEND, registry.actor); - break; - } - } - - const sympathy: Optional = readIniNumber(iniFile, "logic", "sympathy", false); - - if (sympathy !== null) { - object.set_sympathy(sympathy); - } - } -} - /** * Reset generic schemes on activation of new scheme. * Called after scheme switch to new section. @@ -538,7 +373,7 @@ export function resetObjectGenericSchemesOnSectionSwitch( } case ESchemeType.MONSTER: { - scriptReleaseObject(object, ""); // ??? + scriptReleaseObject(object, ""); // ???, todo: need details if (object.clsid() === clsid.bloodsucker_s) { object.set_manual_invisibility(scheme !== EScheme.NIL); diff --git a/src/engine/core/utils/scheme/switch.ts b/src/engine/core/utils/scheme/switch.ts index 172585f25..e7a1d43c6 100644 --- a/src/engine/core/utils/scheme/switch.ts +++ b/src/engine/core/utils/scheme/switch.ts @@ -142,7 +142,7 @@ export function switchObjectSchemeToSection(object: ClientObject, ini: IniFile, state.activeSection = null; state.activeScheme = null; - activateSchemeBySection(object, ini, section, state.gulag_name, false); + activateSchemeBySection(object, ini, section, state.smartTerrainName, false); return true; } diff --git a/src/engine/core/utils/spawn.ts b/src/engine/core/utils/spawn.ts index 61bbe59db..62acf7a6f 100644 --- a/src/engine/core/utils/spawn.ts +++ b/src/engine/core/utils/spawn.ts @@ -64,6 +64,7 @@ export function spawnItemsForObject( for (const it of $range(1, count)) { if (math.random(100) <= probability) { + // todo: Get simulator only once? alife().create(itemSection, position, lvid, gvid, id); itemsSpawned += 1; } @@ -139,34 +140,6 @@ export function spawnItemsForObjectFromList( } } -/** - * todo: description - */ -export function spawnDefaultObjectItems(object: ClientObject, state: IRegistryObjectState): void { - logger.info("Spawn default items for object:", object.name()); - - const itemsToSpawn: LuaTable = new LuaTable(); - const spawnItemsSection: Optional = readIniString(state.ini, state.section_logic, "spawn", false, "", null); - - if (spawnItemsSection === null) { - return; - } - - const itemSectionsCount: TCount = state.ini.line_count(spawnItemsSection); - - for (const it of $range(0, itemSectionsCount - 1)) { - const [result, id, value] = state.ini.r_line(spawnItemsSection, it, "", ""); - - itemsToSpawn.set(id as TInventoryItem, value === "" ? 1 : tonumber(value)!); - } - - for (const [id, count] of itemsToSpawn) { - if (object.object(id) === null) { - spawnItemsForObject(object, id, count); - } - } -} - /** * Get matching translation for section if it exists. * diff --git a/src/engine/lib/configs/LogicsConfig.ts b/src/engine/lib/configs/LogicsConfig.ts index c16c6e838..41ea28682 100644 --- a/src/engine/lib/configs/LogicsConfig.ts +++ b/src/engine/lib/configs/LogicsConfig.ts @@ -77,4 +77,7 @@ export const logicsConfig = { RESPAWN_RADIUS_RESTRICTION_SQR: 150 * 150, DEFAULT_ARRIVAL_DISTANCE: 25, }, + TRADE: { + DEFAULT_TRADE_LTX_PATH: "misc\\trade\\trade_generic.ltx", + }, } as const; diff --git a/src/fixtures/xray/mocks/objects/AlifeSimulator.mock.ts b/src/fixtures/xray/mocks/objects/AlifeSimulator.mock.ts index bee56452d..63934e114 100644 --- a/src/fixtures/xray/mocks/objects/AlifeSimulator.mock.ts +++ b/src/fixtures/xray/mocks/objects/AlifeSimulator.mock.ts @@ -1,11 +1,12 @@ import { jest } from "@jest/globals"; -import { AlifeSimulator, ServerObject } from "@/engine/lib/types"; +import { AlifeSimulator, Optional, ServerObject } from "@/engine/lib/types"; /** * todo; */ export class MockAlifeSimulator { + public static simulator: Optional = null; public static registry: Record = {}; public static addToRegistry(object: ServerObject): void { @@ -16,10 +17,20 @@ export class MockAlifeSimulator { delete MockAlifeSimulator.registry[id]; } + public static getInstance(): MockAlifeSimulator { + if (!MockAlifeSimulator.simulator) { + MockAlifeSimulator.simulator = new MockAlifeSimulator(); + } + + return MockAlifeSimulator.simulator; + } + public actor = jest.fn(() => MockAlifeSimulator.registry[0] || null); public object = jest.fn((id: number) => MockAlifeSimulator.registry[id] || null); + public create = jest.fn(() => {}); + public create_ammo = jest.fn(() => {}); public release = jest.fn((object: ServerObject) => { @@ -31,5 +42,5 @@ export class MockAlifeSimulator { * todo; */ export function mockAlifeSimulator(): AlifeSimulator { - return new MockAlifeSimulator() as unknown as AlifeSimulator; + return MockAlifeSimulator.getInstance() as unknown as AlifeSimulator; }