diff --git a/skymp5-client/src/index.ts b/skymp5-client/src/index.ts index 397c322823..c913e75a95 100644 --- a/skymp5-client/src/index.ts +++ b/skymp5-client/src/index.ts @@ -50,6 +50,7 @@ import { BlockedAnimationsService } from "./services/services/blockedAnimationsS import { WorldView } from "./view/worldView"; import { KeyboardEventsService } from "./services/services/keyboardEventsService"; import { MagicSyncService } from "./services/services/magicSyncService"; +import { SweetTaffySuspendSyncInTutorialService } from "./services/services/sweetTaffySuspendSyncInTutorialService"; once("update", () => { Utility.setINIBool("bAlwaysActive:General", true); @@ -86,6 +87,7 @@ const main = () => { new SweetTaffySweetCantDropService(sp, controller), new SweetTaffyPlayerCombatService(sp, controller), new SweetTaffySkillMenuService(sp, controller), + new SweetTaffySuspendSyncInTutorialService(sp, controller), new DisableSkillAdvanceService(sp, controller), new DisableFastTravelService(sp, controller), new DisableDifficultySelectionService(sp, controller), diff --git a/skymp5-client/src/services/events/events.ts b/skymp5-client/src/services/events/events.ts index ea5c797102..1e42a32c21 100644 --- a/skymp5-client/src/services/events/events.ts +++ b/skymp5-client/src/services/events/events.ts @@ -36,7 +36,7 @@ import { QueryBlockSetInventoryEvent } from "./queryBlockSetInventoryEvent"; import { QueryKeyCodeBindings } from "./queryKeyCodeBindings"; import { SpellCastMessage } from "../messages/spellCastMessage"; import { UpdateAnimVariablesMessage } from "../messages/updateAnimVariablesMessage"; - +import { QuerySuspendSyncEvent } from "./querySuspendSync"; type EventTypes = { 'gameLoad': [GameLoadEvent], @@ -79,7 +79,8 @@ type EventTypes = { 'anyMessage': [ConnectionMessage], 'newLocalLagValueCalculated': [NewLocalLagValueCalculatedEvent], 'queryBlockSetInventoryEvent': [QueryBlockSetInventoryEvent], - 'queryKeyCodeBindings': [QueryKeyCodeBindings] + 'queryKeyCodeBindings': [QueryKeyCodeBindings], + 'querySuspendSync': [QuerySuspendSyncEvent], } // https://blog.makerx.com.au/a-type-safe-event-emitter-in-node-js/ diff --git a/skymp5-client/src/services/events/querySuspendSync.ts b/skymp5-client/src/services/events/querySuspendSync.ts new file mode 100644 index 0000000000..d2c98e5413 --- /dev/null +++ b/skymp5-client/src/services/events/querySuspendSync.ts @@ -0,0 +1,3 @@ +export interface QuerySuspendSyncEvent { + suspend: () => void +} diff --git a/skymp5-client/src/services/services/sweetTaffyPlayerCombatService.ts b/skymp5-client/src/services/services/sweetTaffyPlayerCombatService.ts index 78b7ddfa2f..4b8919d95e 100644 --- a/skymp5-client/src/services/services/sweetTaffyPlayerCombatService.ts +++ b/skymp5-client/src/services/services/sweetTaffyPlayerCombatService.ts @@ -243,6 +243,8 @@ export class SweetTaffyPlayerCombatService extends ClientListener { // It's well-tested though, so we can enable it once we have a protection mechanism. // const weaponTimings = this.getSettingsFromFile(); + + // See also SweetTaffySuspendSyncInTutorialService.ts const weaponTimings = this.getSettingsDefault(); if (!weaponTimings) { return null; diff --git a/skymp5-client/src/services/services/sweetTaffySuspendSyncInTutorialService.ts b/skymp5-client/src/services/services/sweetTaffySuspendSyncInTutorialService.ts new file mode 100644 index 0000000000..5eb4608b9f --- /dev/null +++ b/skymp5-client/src/services/services/sweetTaffySuspendSyncInTutorialService.ts @@ -0,0 +1,129 @@ +import { logError, logTrace } from "../../logging"; +import { QuerySuspendSyncEvent } from "../events/querySuspendSync"; +import { ClientListener, Sp, CombinedController } from "./clientListener"; +import { ObjectReferenceEx } from "../../extensions/objectReferenceEx"; + +interface SuspendZonePoint { + pos: number[]; + radius: number; + worldOrCell: string; +} + +interface SuspendZoneSettings { + points: SuspendZonePoint[]; + keywordImmuneNoSyncZone?: string; +} + +export class SweetTaffySuspendSyncInTutorialService extends ClientListener { + constructor(private sp: Sp, private controller: CombinedController) { + super(); + + if (!this.hasSweetPie()) { + logTrace(this, "SweetTaffy features disabled"); + return; + } + + logTrace(this, "SweetTaffy features enabled"); + + this.controller.emitter.on("querySuspendSync", (e) => this.onQuerySuspendSync(e)); + } + + private onQuerySuspendSync(e: QuerySuspendSyncEvent) { + // See also sweetTaffySuspendSyncInTutorialService.ts + // const suspendZoneSettings = this.getSettingsFromFile(); + const suspendZoneSettings = this.getSettingsDefault(); + + if (!suspendZoneSettings) { + return; + } + + const { points, keywordImmuneNoSyncZone } = suspendZoneSettings; + const suspendNeeded = this.isSyncSuspendNeeded(points, keywordImmuneNoSyncZone) + + if (suspendNeeded) { + e.suspend(); + } + } + + private isSyncSuspendNeeded(points: SuspendZonePoint[], keywordImmuneNoSyncZone?: string) { + // TODO: de-duplicate implementation with Green Zone implementation in gamemode + let player = this.sp.Game.getPlayer()!; + + let isNoSyncZone = false; + + let pos = [ + player.getPositionX(), + player.getPositionY(), + player.getPositionZ(), + ]; + + const worldOrCell = ObjectReferenceEx.getWorldOrCell(player); + + for (let point of points) { + if (parseInt(point.worldOrCell) !== worldOrCell) { + continue; + } + + let distance = Math.sqrt( + Math.pow(pos[0] - point.pos[0], 2) + + Math.pow(pos[1] - point.pos[1], 2) + + Math.pow(pos[2] - point.pos[2], 2) + ); + if (distance < point.radius) { + isNoSyncZone = true; + break; + } + } + + const isImmune = !keywordImmuneNoSyncZone || !player.wornHasKeyword(this.sp.Keyword.getKeyword(keywordImmuneNoSyncZone)); + + if (isImmune !== this.wasImmune) { + this.wasImmune = isImmune; + logTrace(this, "Immune to NoSync zone:", isImmune); + } + + if (isNoSyncZone !== this.wasInNoSyncZone) { + this.wasInNoSyncZone = isNoSyncZone; + logTrace(this, "NoSync zone:", isNoSyncZone); + } + + return isNoSyncZone && !isImmune; + } + + private getSettingsFromFile(): SuspendZoneSettings | null { + const sweetTaffySuspendSyncInTutorialService = this.sp.settings["skymp5-client"]["sweetTaffySuspendSyncInTutorialService"]; + + if (!sweetTaffySuspendSyncInTutorialService || typeof sweetTaffySuspendSyncInTutorialService !== "object") { + logError(this, `No sweetTaffySuspendSyncInTutorialService settings found`); + return null; + } + + const suspendZoneSettings = (sweetTaffySuspendSyncInTutorialService as Record).suspendZoneSettings; + if (!suspendZoneSettings || typeof suspendZoneSettings !== "object") { + logError(this, `No suspendZoneSettings settings found`); + return null; + } + + return suspendZoneSettings as SuspendZoneSettings; + } + + private getSettingsDefault(): SuspendZoneSettings | null { + return { + points: [], + keywordImmuneNoSyncZone: "", + }; + } + + private hasSweetPie(): boolean { + const modCount = this.sp.Game.getModCount(); + for (let i = 0; i < modCount; ++i) { + if (this.sp.Game.getModName(i).toLowerCase().includes('sweetpie')) { + return true; + } + } + return false; + } + + private wasInNoSyncZone = false; + private wasImmune = false; +} diff --git a/skymp5-client/src/view/formViewArray.ts b/skymp5-client/src/view/formViewArray.ts index 830361f43f..d5e917c994 100644 --- a/skymp5-client/src/view/formViewArray.ts +++ b/skymp5-client/src/view/formViewArray.ts @@ -30,15 +30,34 @@ export class FormViewArray { } updateAll(model: WorldModel, showMe: boolean, isCloneView: boolean) { - const gamemodeUpdateService = SpApiInteractor.getControllerInstance().lookupListener(GamemodeUpdateService); + const controller = SpApiInteractor.getControllerInstance(); + + const gamemodeUpdateService = controller.lookupListener(GamemodeUpdateService); gamemodeUpdateService.setFormViewArray(this); + this.isSyncSuspended = false; + controller.emitter.emit("querySuspendSync", { + suspend: () => { + this.isSyncSuspended = true; + } + }); + const forms = model.forms; const n = forms.length; for (let i = 0; i < n; ++i) { const form = forms[i]; - if (!form || (model.playerCharacterFormIdx === i && !showMe)) { + if (form === undefined) { + this.destroyForm(i); + continue; + } + + if (this.isSyncSuspended) { + this.destroyForm(i); + continue; + } + + if (model.playerCharacterFormIdx === i && !showMe) { this.destroyForm(i); continue; } @@ -111,4 +130,5 @@ export class FormViewArray { } private formViews = new Array(); + private isSyncSuspended = false; }