diff --git a/docs/docs_server_configuration_reference.md b/docs/docs_server_configuration_reference.md index b4fc576e4d..99ea14c621 100644 --- a/docs/docs_server_configuration_reference.md +++ b/docs/docs_server_configuration_reference.md @@ -223,6 +223,18 @@ The name of a localizaiton file in `data/localization` that would be used by `M. } ``` +## enableConsoleCommandsForAll + +Enable console commands for all, useful for testing. + +```json5 +{ + // ... + "enableConsoleCommandsForAll": true + // ... +} +``` + ## sweetPieMinimumPlayersToStart The minimal amount of players to begin deathmatch. This setting is sweetpie only and does not affect vanilla server. By default is 5. diff --git a/libespm/include/libespm/ACHR.h b/libespm/include/libespm/ACHR.h index 9ae188fa1e..73b499ea31 100644 --- a/libespm/include/libespm/ACHR.h +++ b/libespm/include/libespm/ACHR.h @@ -9,6 +9,8 @@ class ACHR final : public RecordHeader { public: static constexpr auto kType = "ACHR"; + + bool StartsDead() const noexcept; }; static_assert(sizeof(ACHR) == sizeof(RecordHeader)); diff --git a/libespm/include/libespm/NPC_.h b/libespm/include/libespm/NPC_.h index 625c7ae970..39819178d1 100644 --- a/libespm/include/libespm/NPC_.h +++ b/libespm/include/libespm/NPC_.h @@ -33,9 +33,9 @@ class NPC_ final : public RecordHeader bool isProtected = false; uint32_t race = 0; - uint16_t healthOffset = 0; - uint16_t magickaOffset = 0; - uint16_t staminaOffset = 0; + int16_t healthOffset = 0; + int16_t magickaOffset = 0; + int16_t staminaOffset = 0; ObjectBounds objectBounds = {}; }; diff --git a/libespm/include/libespm/RecordHeader.h b/libespm/include/libespm/RecordHeader.h index 0753264d45..d15a7ef644 100644 --- a/libespm/include/libespm/RecordHeader.h +++ b/libespm/include/libespm/RecordHeader.h @@ -36,7 +36,7 @@ class RecordHeader uint32_t GetFieldsSizeSum() const noexcept; -private: +protected: uint32_t flags; uint32_t id; uint32_t revision; diff --git a/libespm/src/ACHR.cpp b/libespm/src/ACHR.cpp new file mode 100644 index 0000000000..c0f36b0937 --- /dev/null +++ b/libespm/src/ACHR.cpp @@ -0,0 +1,12 @@ +#include "libespm/ACHR.h" +#include "libespm/RecordHeaderAccess.h" +#include + +namespace espm { + +bool ACHR::StartsDead() const noexcept +{ + return this->flags & 0x200; +} + +} diff --git a/libespm/src/NPC_.cpp b/libespm/src/NPC_.cpp index b715a0c703..0309ec71e7 100644 --- a/libespm/src/NPC_.cpp +++ b/libespm/src/NPC_.cpp @@ -26,9 +26,9 @@ NPC_::Data NPC_::GetData( result.isEssential = !!(flags & 0x02); result.isProtected = !!(flags & 0x800); - result.magickaOffset = *reinterpret_cast(data + 4); - result.staminaOffset = *reinterpret_cast(data + 6); - result.healthOffset = *reinterpret_cast(data + 20); + result.magickaOffset = *reinterpret_cast(data + 4); + result.staminaOffset = *reinterpret_cast(data + 6); + result.healthOffset = *reinterpret_cast(data + 20); } else if (!std::memcmp(type, "RNAM", 4)) { result.race = *reinterpret_cast(data); diff --git a/skymp5-client/src/features/worldCleaner.ts b/skymp5-client/src/features/worldCleaner.ts index f41f76cbe6..797a3c9f16 100644 --- a/skymp5-client/src/features/worldCleaner.ts +++ b/skymp5-client/src/features/worldCleaner.ts @@ -28,6 +28,12 @@ function processOneActor(): void { actor.disableNoWait(true); // Seems to not crash return; } + + if (actor.isDead()) { + actor.blockActivation(true); + return; + } + actor.disable(false).then(() => { const ac = Actor.from(Game.getFormEx(actorId)); if (!ac || isInDialogue(ac)) return; diff --git a/skymp5-client/src/messages.ts b/skymp5-client/src/messages.ts index 315e1ac544..9e066257e9 100644 --- a/skymp5-client/src/messages.ts +++ b/skymp5-client/src/messages.ts @@ -27,6 +27,8 @@ export enum MsgType { OnHit = 17, DeathStateContainer = 18, DropItem = 19, + Teleport = 20, + OpenContainer = 21, } export interface SetInventory { @@ -34,18 +36,6 @@ export interface SetInventory { inventory: Inventory; } -export interface OpenContainer { - type: "openContainer"; - target: number; -} - -export interface Teleport { - type: "teleport"; - pos: number[]; - rot: number[]; - worldOrCell: number; -} - export interface CreateActorMessage { type: "createActor"; idx: number; @@ -74,13 +64,6 @@ export interface UpdatePropertyMessage { propName: string; } -export interface DeathStateContainerMessage { - t: MsgType.DeathStateContainer; - tTeleport?: Teleport, - tChangeValues?: ChangeValuesMessage, - tIsDead: UpdatePropertyMessage, -} - export interface SetRaceMenuOpenMessage { type: "setRaceMenuOpen"; open: boolean; diff --git a/skymp5-client/src/modelSource/msgHandler.ts b/skymp5-client/src/modelSource/msgHandler.ts index b5d64c2b04..0a39ead3b5 100644 --- a/skymp5-client/src/modelSource/msgHandler.ts +++ b/skymp5-client/src/modelSource/msgHandler.ts @@ -1,5 +1,6 @@ import * as msg from "../messages"; import { ChangeValuesMessage } from "../services/messages/changeValues"; +import { DeathStateContainerMessage } from "../services/messages/deathStateContainerMessage"; import { UpdateAnimationMessage } from "../services/messages/updateAnimationMessage"; import { UpdateAppearanceMessage } from "../services/messages/updateAppearanceMessage"; import { UpdateEquipmentMessage } from "../services/messages/updateEquipmentMessage"; @@ -15,7 +16,7 @@ export interface MsgHandler { ChangeValues(msg: ChangeValuesMessage): void; setRaceMenuOpen(msg: msg.SetRaceMenuOpenMessage): void; customPacket(msg: msg.CustomPacket): void; - DeathStateContainer(msg: msg.DeathStateContainerMessage): void; + DeathStateContainer(msg: DeathStateContainerMessage): void; handleConnectionAccepted(): void; handleDisconnect(): void; diff --git a/skymp5-client/src/modelSource/remoteServer.ts b/skymp5-client/src/modelSource/remoteServer.ts index 4b0cdb9ee3..97bf0f19c6 100644 --- a/skymp5-client/src/modelSource/remoteServer.ts +++ b/skymp5-client/src/modelSource/remoteServer.ts @@ -36,6 +36,7 @@ import { Movement } from '../sync/movement'; import { learnSpells, removeAllSpells } from '../sync/spell'; import { ModelApplyUtils } from '../view/modelApplyUtils'; import { + getObjectReference, getViewFromStorage, localIdToRemoteId, remoteIdToLocalId, @@ -55,6 +56,10 @@ import { CustomEventMessage } from '../services/messages/customEventMessage'; import { FinishSpSnippetMessage } from '../services/messages/finishSpSnippetMessage'; import { RagdollService } from '../services/services/ragdollService'; import { UpdateAppearanceMessage } from '../services/messages/updateAppearanceMessage'; +import { TeleportMessage } from '../services/messages/teleportMessage'; +import { DeathStateContainerMessage } from '../services/messages/deathStateContainerMessage'; +import { RespawnNeededError } from '../lib/errors'; +import { OpenContainer } from '../services/messages/openContainer'; const onceLoad = ( refrId: number, @@ -212,7 +217,7 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { }); } - openContainer(msg: messages.OpenContainer): void { + OpenContainer(msg: OpenContainer): void { once('update', async () => { await Utility.wait(0.1); // Give a chance to update inventory ( @@ -232,18 +237,23 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { }); } - teleport(msg: messages.Teleport): void { + Teleport(msg: TeleportMessage): void { once('update', () => { + const id = this.getIdManager().getId(msg.idx); + const refr = id === this.getMyActorIndex() ? Game.getPlayer() : getObjectReference(id); printConsole( - 'Teleporting...', + `Teleporting (idx=${msg.idx}) ${refr?.getFormID().toString(16)}...`, msg.pos, 'cell/world is', msg.worldOrCell.toString(16), ); const ragdollService = SpApiInteractor.makeController().lookupListener(RagdollService); - ragdollService.safeRemoveRagdollFromWorld(Game.getPlayer()!, () => { + + const refrId = refr?.getFormID(); + + const removeRagdollCallback = () => { TESModPlatform.moveRefrToPosition( - Game.getPlayer()!, + ObjectReference.from(Game.getFormEx(refrId || 0)), Cell.from(Game.getFormEx(msg.worldOrCell)), WorldSpace.from(Game.getFormEx(msg.worldOrCell)), msg.pos[0], @@ -253,7 +263,14 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { msg.rot[1], msg.rot[2], ); - }); + }; + const actor = Actor.from(refr); + if (actor /*&& actor.getFormID() === 0x14*/) { + ragdollService.safeRemoveRagdollFromWorld(actor, removeRagdollCallback); + } + else { + removeRagdollCallback(); + } }); } @@ -347,10 +364,10 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { Game.getPlayer() as Actor, msg.equipment ? { - entries: msg.equipment.inv.entries.filter( - (x) => !!Armor.from(Game.getFormEx(x.baseId)), - ), - } + entries: msg.equipment.inv.entries.filter( + (x) => !!Armor.from(Game.getFormEx(x.baseId)), + ), + } : { entries: [] }, false, ); @@ -410,7 +427,7 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { const sqr = (x: number) => x * x; const distance = Math.sqrt( sqr(pos[0] - msg.transform.pos[0]) + - sqr(pos[1] - msg.transform.pos[1]), + sqr(pos[1] - msg.transform.pos[1]), ); if (distance < 256) { break; @@ -481,16 +498,16 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { msg.transform.worldOrCell, msg.appearance ? { - name: msg.appearance.name, - raceId: msg.appearance.raceId, - face: { - hairColor: msg.appearance.hairColor, - bodySkinColor: msg.appearance.skinColor, - headTextureSetId: msg.appearance.headTextureSetId, - headPartIds: msg.appearance.headpartIds, - presets: msg.appearance.presets, - }, - } + name: msg.appearance.name, + raceId: msg.appearance.raceId, + face: { + hairColor: msg.appearance.hairColor, + bodySkinColor: msg.appearance.skinColor, + headTextureSetId: msg.appearance.headTextureSetId, + headPartIds: msg.appearance.headpartIds, + presets: msg.appearance.presets, + }, + } : undefined, ); once('update', () => { @@ -585,7 +602,7 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { (form as Record)[msg.propName] = msg.data; } - DeathStateContainer(msg: messages.DeathStateContainerMessage): void { + DeathStateContainer(msg: DeathStateContainerMessage): void { once('update', () => printConsole(`Received death state: ${JSON.stringify(msg.tIsDead)}`), ); @@ -601,7 +618,7 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { once('update', () => this.UpdateProperty(msg.tIsDead)); if (msg.tTeleport) { - this.teleport(msg.tTeleport); + this.Teleport(msg.tTeleport); } const id = this.getIdManager().getId(msg.tIsDead.idx); @@ -612,10 +629,19 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { ? Game.getPlayer()! : Actor.from(Game.getFormEx(remoteIdToLocalId(form.refrId ?? 0))); if (actor) { - SpApiInteractor.makeController().emitter.emit("applyDeathStateEvent", { - actor: Game.getPlayer()!, - isDead: msg.tIsDead.data as boolean - }); + try { + SpApiInteractor.makeController().emitter.emit("applyDeathStateEvent", { + actor: actor, + isDead: msg.tIsDead.data as boolean + }); + } catch (e) { + if (e instanceof RespawnNeededError) { + actor.disableNoWait(false); + actor.delete(); + } else { + throw e; + } + } } }); } @@ -627,11 +653,13 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { loginWithSkympIoCredentials(); } - handleDisconnect(): void {} + handleDisconnect(): void { } ChangeValues(msg: ChangeValuesMessage): void { once('update', () => { - const ac = Game.getPlayer(); + const id = this.getIdManager().getId(msg.idx); + const refr = id === this.getMyActorIndex() ? Game.getPlayer() : getObjectReference(id); + const ac = Actor.from(refr); if (!ac) return; setActorValuePercentage(ac, 'health', msg.data.health); setActorValuePercentage(ac, 'stamina', msg.data.stamina); @@ -728,7 +756,7 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { storage['eventSourceContexts'] = []; } else { storage['eventSourceContexts'].forEach((ctx: Record) => { - ctx.sendEvent = () => {}; + ctx.sendEvent = () => { }; ctx._expired = true; }); } diff --git a/skymp5-client/src/services/messages/anyMessage.ts b/skymp5-client/src/services/messages/anyMessage.ts index f26de0c975..3f771f8563 100644 --- a/skymp5-client/src/services/messages/anyMessage.ts +++ b/skymp5-client/src/services/messages/anyMessage.ts @@ -10,8 +10,10 @@ import { FinishSpSnippetMessage } from "./finishSpSnippetMessage"; import { HitMessage } from "./hitMessage"; import { HostMessage } from "./hostMessage"; import { OnEquipMessage } from "./onEquipMessage"; +import { OpenContainer } from "./openContainer"; import { PutItemMessage } from "./putItemMessage"; import { TakeItemMessage } from "./takeItemMessage"; +import { TeleportMessage } from "./teleportMessage"; import { UpdateAnimationMessage } from "./updateAnimationMessage"; import { UpdateAppearanceMessage } from "./updateAppearanceMessage"; import { UpdateEquipmentMessage } from "./updateEquipmentMessage"; @@ -34,3 +36,5 @@ export type AnyMessage = ActivateMessage | CustomEventMessage | CustomPacketMessage | FinishSpSnippetMessage + | TeleportMessage + | OpenContainer diff --git a/skymp5-client/src/services/messages/changeValues.ts b/skymp5-client/src/services/messages/changeValues.ts index 24afb14fef..23e824e1d4 100644 --- a/skymp5-client/src/services/messages/changeValues.ts +++ b/skymp5-client/src/services/messages/changeValues.ts @@ -4,4 +4,5 @@ import { ActorValues } from "../../sync/actorvalues"; export interface ChangeValuesMessage { t: MsgType.ChangeValues; data: ActorValues; + idx: number; } diff --git a/skymp5-client/src/services/messages/deathStateContainerMessage.ts b/skymp5-client/src/services/messages/deathStateContainerMessage.ts new file mode 100644 index 0000000000..0030527136 --- /dev/null +++ b/skymp5-client/src/services/messages/deathStateContainerMessage.ts @@ -0,0 +1,10 @@ +import { MsgType, UpdatePropertyMessage } from "../../messages"; +import { ChangeValuesMessage } from "./changeValues"; +import { TeleportMessage } from "./teleportMessage"; + +export interface DeathStateContainerMessage { + t: MsgType.DeathStateContainer; + tTeleport?: TeleportMessage, + tChangeValues?: ChangeValuesMessage, + tIsDead: UpdatePropertyMessage, +} diff --git a/skymp5-client/src/services/messages/openContainer.ts b/skymp5-client/src/services/messages/openContainer.ts new file mode 100644 index 0000000000..eeee55c516 --- /dev/null +++ b/skymp5-client/src/services/messages/openContainer.ts @@ -0,0 +1,6 @@ +import { MsgType } from "../../messages"; + +export interface OpenContainer { + t: MsgType.OpenContainer; + target: number; +} diff --git a/skymp5-client/src/services/messages/teleportMessage.ts b/skymp5-client/src/services/messages/teleportMessage.ts new file mode 100644 index 0000000000..f400072aeb --- /dev/null +++ b/skymp5-client/src/services/messages/teleportMessage.ts @@ -0,0 +1,9 @@ +import { MsgType } from "../../messages"; + +export interface TeleportMessage { + t: MsgType.Teleport; + idx: number; + pos: number[]; + rot: number[]; + worldOrCell: number; +} diff --git a/skymp5-client/src/services/services/hitService.ts b/skymp5-client/src/services/services/hitService.ts index e85a3c0acb..c8528b4f90 100644 --- a/skymp5-client/src/services/services/hitService.ts +++ b/skymp5-client/src/services/services/hitService.ts @@ -1,10 +1,9 @@ // TODO: refactor this out import { localIdToRemoteId } from "../../view/worldViewMisc"; -import { HitEvent } from "skyrimPlatform"; +import { FormType, HitEvent, storage } from "skyrimPlatform"; import { ClientListener, CombinedController, Sp } from "./clientListener"; import { MsgType } from "../../messages"; -import { SkympClient } from "./skympClient"; import { Hit } from "../messages/hitMessage"; export class HitService extends ClientListener { @@ -15,13 +14,32 @@ export class HitService extends ClientListener { private onHit(e: HitEvent) { // TODO: add more logging in case of 'return' - // TODO: allow npcs to attack // TODO: allow non-weapon sources - // TODO: allow non-actor targets - const playerFormId = 0x14; - if (e.target.getFormID() === playerFormId) return; - if (e.aggressor.getFormID() !== playerFormId) return; - if (this.sp.Weapon.from(e.source) && this.sp.Actor.from(e.target)) { + const aggressor = e.aggressor.getFormID(); + if (aggressor < 0xff000000 && aggressor !== 0x14) return; // all skymp npcs are FF+ + + if (aggressor >= 0xff000000) { + // TODO: make host service + const hosted = storage['hosted']; + let alreadyHosted = false; + if (Array.isArray(hosted)) { + const remoteId = localIdToRemoteId(aggressor); + if (hosted.includes(remoteId) || hosted.includes(remoteId + 0x100000000)) { + alreadyHosted = true; + } + } + + if (!alreadyHosted) return; + } + + const base = e.target.getBaseObject(); + const type = base?.getType(); + + if (type === FormType.Static || type === FormType.MovableStatic) { + return; + } + + if (this.sp.Weapon.from(e.source)) { this.controller.emitter.emit("sendMessage", { message: { t: MsgType.OnHit, data: this.getHitData(e) }, reliability: "reliable" diff --git a/skymp5-client/src/view/formView.ts b/skymp5-client/src/view/formView.ts index 890e71a047..b3880d1f63 100644 --- a/skymp5-client/src/view/formView.ts +++ b/skymp5-client/src/view/formView.ts @@ -1,4 +1,4 @@ -import { Actor, ActorBase, createText, destroyText, Form, FormType, Game, Keyword, NetImmerse, ObjectReference, once, printConsole, setTextPos, setTextString, TESModPlatform, Utility, worldPointToScreenPoint } from "skyrimPlatform"; +import { Actor, ActorBase, createText, destroyText, Form, FormType, Game, Keyword, NetImmerse, ObjectReference, once, printConsole, setTextPos, setTextString, storage, TESModPlatform, Utility, worldPointToScreenPoint } from "skyrimPlatform"; import { setDefaultAnimsDisabled, applyAnimation } from "../sync/animation"; import { Appearance, applyAppearance } from "../sync/appearance"; import { isBadMenuShown, applyEquipment } from "../sync/equipment"; @@ -14,6 +14,7 @@ import { PlayerCharacterDataHolder } from "./playerCharacterDataHolder"; import { getMovement } from "../sync/movementGet"; import { lastTryHost, tryHost } from "./hostAttempts"; import { ModelApplyUtils } from "./modelApplyUtils"; +import { localIdToRemoteId } from "./worldViewMisc"; export interface ScreenResolution { width: number; @@ -222,20 +223,31 @@ export class FormView implements View { } } - if (model.movement) { - const ac = Actor.from(refr); - if (ac) { - if (model.isHostedByOther !== this.wasHostedByOther) { - this.wasHostedByOther = model.isHostedByOther; - this.movState.lastApply = 0; - if (model.isHostedByOther) { - setDefaultAnimsDisabled(ac.getFormID(), true); - } else { - setDefaultAnimsDisabled(ac.getFormID(), false); - } - } + // TODO: make host service + const hosted = storage['hosted']; + let alreadyHosted = false; + if (Array.isArray(hosted)) { + const remoteId = localIdToRemoteId(this.refrId); + + if (hosted.includes(remoteId) || hosted.includes(remoteId + 0x100000000)) { + alreadyHosted = true; } + // printConsole("remoteId=", remoteId.toString(16), "hosted=", hosted.map(x => x.toString(16))); + } + setDefaultAnimsDisabled(this.refrId, alreadyHosted ? false : true); + + // if (model.baseId === 0x7 || !model.baseId) { + // setDefaultAnimsDisabled(this.refrId, true); + // } + // else { + // setDefaultAnimsDisabled(this.refrId, false); + // } + if (alreadyHosted) { + Actor.from(refr)?.clearKeepOffsetFromActor(); + } + if (model.movement) { + let ac = Actor.from(refr); if ( this.movState.lastApply && Date.now() - this.movState.lastApply > 1500 @@ -279,14 +291,28 @@ export class FormView implements View { this.movState.lastNumChanges = +(model.numMovementChanges as number); this.movState.everApplied = true; } else { - if (ac) { - ac.clearKeepOffsetFromActor(); - TESModPlatform.setWeaponDrawnMode(ac, -1); - } const remoteId = this.remoteRefrId; if (ac && remoteId && ac.is3DLoaded()) { - this.tryHostIfNeed(ac, remoteId); - printConsole("tryHostIfNeed - reason: not hosted by anyone OR never applied movement"); + ac.clearKeepOffsetFromActor(); + + // TODO: make host service + const hosted = storage['hosted']; + let alreadyHosted = false; + if (Array.isArray(hosted)) { + const remoteId = localIdToRemoteId(ac.getFormID()); + if (hosted.includes(remoteId)) { + alreadyHosted = true; + } + } + + if (!alreadyHosted) { + if (this.tryHostIfNeed(ac, remoteId)) { + + // previously, we did this cleanup on each update + // but I guess it's too expensive and can possibly hurt FPS + TESModPlatform.setWeaponDrawnMode(ac, -1); + } + } } } } @@ -361,9 +387,9 @@ export class FormView implements View { const maxNicknameDrawDistance = 1000; const playerActor = Game.getPlayer()!; const isVisibleByPlayer = !model.movement?.isSneaking - && playerActor.getDistance(refr) <= maxNicknameDrawDistance - && playerActor.hasLOS(refr) - && !this.isSweetHidePerson(refr); + && playerActor.getDistance(refr) <= maxNicknameDrawDistance + && playerActor.hasLOS(refr) + && !this.isSweetHidePerson(refr); if (isVisibleByPlayer) { const headScreenPos = worldPointToScreenPoint([ NetImmerse.getNodeWorldPositionX(refr, headPart, false), @@ -427,9 +453,11 @@ export class FormView implements View { getMovement(ac).worldOrCell === getMovement(Game.getPlayer() as Actor).worldOrCell ) { - return tryHost(remoteId); + tryHost(remoteId); + return true; } } + return false; }; getLocalRefrId(): number { diff --git a/skymp5-client/src/view/formViewArray.ts b/skymp5-client/src/view/formViewArray.ts index a0b93df94d..1952811781 100644 --- a/skymp5-client/src/view/formViewArray.ts +++ b/skymp5-client/src/view/formViewArray.ts @@ -96,5 +96,9 @@ export class FormViewArray { return formView ? formView.getLocalRefrId() : 0; } + getNthFormView(i: number): FormView | undefined { + return this.formViews[i]; + } + private formViews = new Array(); } diff --git a/skymp5-client/src/view/worldView.ts b/skymp5-client/src/view/worldView.ts index 0a9ed63550..4efa709b25 100644 --- a/skymp5-client/src/view/worldView.ts +++ b/skymp5-client/src/view/worldView.ts @@ -55,7 +55,7 @@ export class WorldView implements View { const skipUpdates = settings['skymp5-client']['skipUpdates']; - // skip 50% of updated if said in the settings + // skip 50% of updates if specified in the settings this.counter = !this.counter; if (this.counter && skipUpdates) return; @@ -84,6 +84,10 @@ export class WorldView implements View { this.formViews.resize(0); } + getFormViews() { + return this.formViews; + } + private formViews = new FormViewArray(); private cloneFormViews = new FormViewArray(); private allowUpdate = false; diff --git a/skymp5-client/src/view/worldViewMisc.ts b/skymp5-client/src/view/worldViewMisc.ts index 706800ad16..274a8e7afa 100644 --- a/skymp5-client/src/view/worldViewMisc.ts +++ b/skymp5-client/src/view/worldViewMisc.ts @@ -1,4 +1,4 @@ -import { storage } from "skyrimPlatform"; +import { Game, ObjectReference, storage } from "skyrimPlatform"; import { WorldView } from "./worldView"; export const getViewFromStorage = (): WorldView | undefined => { @@ -30,3 +30,20 @@ export const remoteIdToLocalId = (remoteFormId: number): number => { } return remoteFormId; }; + +export const getObjectReference = (i: number): ObjectReference | null => { + const view = getViewFromStorage(); + if (view) { + const formView = view.getFormViews().getNthFormView(i); + if (formView) { + const refrId = formView.getLocalRefrId(); + if (refrId > 0) { + const refr = ObjectReference.from(Game.getFormEx(refrId)); + if (refr !== null) { + return refr; + } + } + } + } + return null; +}; diff --git a/skymp5-server/cpp/addon/ScampServer.cpp b/skymp5-server/cpp/addon/ScampServer.cpp index 126778b880..e016e7e485 100644 --- a/skymp5-server/cpp/addon/ScampServer.cpp +++ b/skymp5-server/cpp/addon/ScampServer.cpp @@ -220,7 +220,7 @@ ScampServer::ScampServer(const Napi::CallbackInfo& info) } } else { std::stringstream msg; - msg << "\"npcSettings\" are not found in the server configuration " + msg << "\"npcSettings\" weren't found in the server configuration " "file."; msg << (partOne->worldState.npcEnabled ? "Allowing all npc by default" @@ -229,6 +229,21 @@ ScampServer::ScampServer(const Napi::CallbackInfo& info) spdlog::info(msg.str()); } + if (serverSettings.find("enableConsoleCommandsForAll") != + serverSettings.end()) { + if (serverSettings.at("enableConsoleCommandsForAll").is_boolean()) { + bool enableConsoleCommandsForAll = + serverSettings["enableConsoleCommandsForAll"].get(); + spdlog::info("enableConsoleCommandsForAll is explicitly set to {}", + enableConsoleCommandsForAll); + partOne->worldState.SetEnableConsoleCommandsForAllSetting( + enableConsoleCommandsForAll); + } else { + spdlog::error("Unexpected value of enableConsoleCommandsForAll " + "setting, should be true or false"); + } + } + partOne->worldState.isPapyrusHotReloadEnabled = serverSettings.count("isPapyrusHotReloadEnabled") != 0 && serverSettings.at("isPapyrusHotReloadEnabled").get(); diff --git a/skymp5-server/cpp/messages/ChangeValuesMessage.cpp b/skymp5-server/cpp/messages/ChangeValuesMessage.cpp new file mode 100644 index 0000000000..fac1a3753f --- /dev/null +++ b/skymp5-server/cpp/messages/ChangeValuesMessage.cpp @@ -0,0 +1,51 @@ +#include "ChangeValuesMessage.h" +#include +#include +#include "SerializationUtil/BitStreamUtil.h" + +void ChangeValuesMessage::WriteBinary(SLNet::BitStream& stream) const +{ + SerializationUtil::WriteToBitStream(stream, idx); + stream.Write(health); + stream.Write(magicka); + stream.Write(stamina); +} + +void ChangeValuesMessage::ReadBinary(SLNet::BitStream& stream) +{ + SerializationUtil::ReadFromBitStream(stream, idx); + stream.Read(health); + stream.Read(magicka); + stream.Read(stamina); +} + +void ChangeValuesMessage::WriteJson(nlohmann::json& json) const +{ + nlohmann::json res = nlohmann::json::object(); + res["t"] = kMsgType; + res["data"] = nlohmann::json::object(); + res["data"]["health"] = health; + res["data"]["magicka"] = magicka; + res["data"]["stamina"] = stamina; + if (idx.has_value()) { + res["idx"] = *idx; + } + json = std::move(res); +} + +void ChangeValuesMessage::ReadJson(const nlohmann::json& json) +{ + ChangeValuesMessage res; + + auto it = json.find("idx"); + if (it != json.end()) { + res.idx = it->get(); + } + + const auto& data = json.at("data"); + res.health = data.at("health").get(); + res.magicka = data.at("magicka").get(); + res.stamina = data.at("stamina").get(); + + *this = std::move(res); +} diff --git a/skymp5-server/cpp/messages/ChangeValuesMessage.h b/skymp5-server/cpp/messages/ChangeValuesMessage.h new file mode 100644 index 0000000000..5ddb0470f3 --- /dev/null +++ b/skymp5-server/cpp/messages/ChangeValuesMessage.h @@ -0,0 +1,22 @@ +#pragma once +#include "MessageBase.h" +#include "MsgType.h" +#include + +struct ChangeValuesMessage : public MessageBase +{ + const static char kMsgType = static_cast(MsgType::ChangeValues); + const static char kHeaderByte = static_cast(MsgType::ChangeValues); + + void WriteBinary(SLNet::BitStream& stream) const override; + void ReadBinary(SLNet::BitStream& stream) override; + void WriteJson(nlohmann::json& json) const override; + void ReadJson(const nlohmann::json& json) override; + + std::optional idx; + + // percentages + float health = 0; + float magicka = 0; + float stamina = 0; +}; diff --git a/skymp5-server/cpp/messages/DeathStateContainerMessage.cpp b/skymp5-server/cpp/messages/DeathStateContainerMessage.cpp new file mode 100644 index 0000000000..6d401be576 --- /dev/null +++ b/skymp5-server/cpp/messages/DeathStateContainerMessage.cpp @@ -0,0 +1,101 @@ +#include "DeathStateContainerMessage.h" +#include +#include + +#include "SerializationUtil/BitStreamUtil.h" + +void DeathStateContainerMessage::WriteBinary(SLNet::BitStream& stream) const +{ + stream.Write(tTeleport.has_value()); + if (tTeleport) { + tTeleport->WriteBinary(stream); + } + + stream.Write(tChangeValues.has_value()); + if (tChangeValues) { + tChangeValues->WriteBinary(stream); + } + + stream.Write(tIsDead.has_value()); + if (tIsDead) { + tIsDead->WriteBinary(stream); + } + + // TODO: use std::optional wrapper like: + // SerializationUtil::WriteToBitStream(stream, tTeleport); +} + +void DeathStateContainerMessage::ReadBinary(SLNet::BitStream& stream) +{ + bool hasTeleport = false; + stream.Read(hasTeleport); + if (hasTeleport) { + tTeleport = TeleportMessage(); + tTeleport->ReadBinary(stream); + } + + bool hasChangeValues = false; + stream.Read(hasChangeValues); + if (hasChangeValues) { + tChangeValues = ChangeValuesMessage(); + tChangeValues->ReadBinary(stream); + } + + bool hasIsDead = false; + stream.Read(hasIsDead); + if (hasIsDead) { + tIsDead = UpdatePropertyMessage(); + tIsDead->ReadBinary(stream); + } +} + +void DeathStateContainerMessage::WriteJson(nlohmann::json& json) const +{ + nlohmann::json res = nlohmann::json::object(); + res["t"] = kMsgType; + + if (tTeleport) { + tTeleport->WriteJson(res["tTeleport"]); + } else { + res["tTeleport"] = nlohmann::json{}; + } + + if (tChangeValues) { + tChangeValues->WriteJson(res["tChangeValues"]); + } else { + res["tChangeValues"] = nlohmann::json{}; + } + + if (tIsDead) { + tIsDead->WriteJson(res["tIsDead"]); + } else { + res["tIsDead"] = nlohmann::json{}; + } + + json = std::move(res); +} + +void DeathStateContainerMessage::ReadJson(const nlohmann::json& json) +{ + DeathStateContainerMessage res; + + auto it = json.find("tTeleport"); + if (it != json.end() && it->is_object()) { + res.tTeleport = TeleportMessage(); + res.tTeleport->ReadJson(*it); + } + + it = json.find("tChangeValues"); + if (it != json.end() && it->is_object()) { + res.tChangeValues = ChangeValuesMessage(); + res.tChangeValues->ReadJson(*it); + } + + it = json.find("tIsDead"); + if (it != json.end() && it->is_object()) { + res.tIsDead = UpdatePropertyMessage(); + res.tIsDead->ReadJson(*it); + } + + *this = std::move(res); +} diff --git a/skymp5-server/cpp/messages/DeathStateContainerMessage.h b/skymp5-server/cpp/messages/DeathStateContainerMessage.h new file mode 100644 index 0000000000..cc1099c881 --- /dev/null +++ b/skymp5-server/cpp/messages/DeathStateContainerMessage.h @@ -0,0 +1,26 @@ +#pragma once +#include "MessageBase.h" +#include "MsgType.h" + +#include "ChangeValuesMessage.h" +#include "TeleportMessage.h" +#include "UpdatePropertyMessage.h" + +#include + +struct DeathStateContainerMessage + : public MessageBase +{ + const static char kMsgType = static_cast(MsgType::DeathStateContainer); + const static char kHeaderByte = + static_cast(MsgType::DeathStateContainer); + + void WriteBinary(SLNet::BitStream& stream) const override; + void ReadBinary(SLNet::BitStream& stream) override; + void WriteJson(nlohmann::json& json) const override; + void ReadJson(const nlohmann::json& json) override; + + std::optional tTeleport; + std::optional tChangeValues; + std::optional tIsDead; +}; diff --git a/skymp5-server/cpp/messages/MessageBase.cpp b/skymp5-server/cpp/messages/MessageBase.cpp index 9fc7df7534..296ce4a229 100644 --- a/skymp5-server/cpp/messages/MessageBase.cpp +++ b/skymp5-server/cpp/messages/MessageBase.cpp @@ -1,3 +1,3 @@ #include "MessageBase.h" -MessageBase::~MessageBase() = default; +IMessageBase::~IMessageBase() = default; diff --git a/skymp5-server/cpp/messages/MessageBase.h b/skymp5-server/cpp/messages/MessageBase.h index 15ca836846..6980051eb3 100644 --- a/skymp5-server/cpp/messages/MessageBase.h +++ b/skymp5-server/cpp/messages/MessageBase.h @@ -2,9 +2,12 @@ #include #include -struct MessageBase +struct IMessageBase { - virtual ~MessageBase(); + virtual ~IMessageBase(); + + virtual char GetHeaderByte() const noexcept = 0; + virtual char GetMsgType() const noexcept = 0; virtual void WriteBinary(SLNet::BitStream& stream) const = 0; virtual void ReadBinary(SLNet::BitStream& stream) = 0; @@ -12,3 +15,10 @@ struct MessageBase virtual void WriteJson(nlohmann::json& json) const = 0; virtual void ReadJson(const nlohmann::json& json) = 0; }; + +template +struct MessageBase : public IMessageBase +{ + char GetHeaderByte() const noexcept override { return Message::kHeaderByte; } + char GetMsgType() const noexcept override { return Message::kMsgType; } +}; diff --git a/skymp5-server/cpp/messages/MessageSerializerFactory.cpp b/skymp5-server/cpp/messages/MessageSerializerFactory.cpp index 057f22bba9..1836de68a2 100644 --- a/skymp5-server/cpp/messages/MessageSerializerFactory.cpp +++ b/skymp5-server/cpp/messages/MessageSerializerFactory.cpp @@ -7,6 +7,13 @@ #include namespace { +void Serialize(const IMessageBase& message, SLNet::BitStream& outputStream) +{ + outputStream.Write(static_cast(Networking::MinPacketId)); + outputStream.Write(static_cast(message.GetHeaderByte())); + message.WriteBinary(outputStream); +} + template void Serialize(const nlohmann::json& inputJson, SLNet::BitStream& outputStream) { @@ -14,9 +21,7 @@ void Serialize(const nlohmann::json& inputJson, SLNet::BitStream& outputStream) message.ReadJson( inputJson); // may throw. we shouldn't pollute outputStream in this case - outputStream.Write(static_cast(Networking::MinPacketId)); - outputStream.Write(static_cast(Message::kHeaderByte)); - message.WriteBinary(outputStream); + Serialize(message, outputStream); } template @@ -87,8 +92,7 @@ MessageSerializerFactory::CreateMessageSerializer() std::vector deserializeFns( kDeserializeFnMax); - REGISTER_MESSAGE(MovementMessage) - REGISTER_MESSAGE(UpdateAnimationMessage) + REGISTER_MESSAGES // make_shared isn't working for private constructors return std::shared_ptr( @@ -134,6 +138,12 @@ void MessageSerializer::Serialize(const char* jsonContent, serializerFn(parsedJson, outputStream); } +void MessageSerializer::Serialize(const IMessageBase& message, + SLNet::BitStream& outputStream) +{ + ::Serialize(message, outputStream); +} + std::optional MessageSerializer::Deserialize( const uint8_t* rawMessageJsonOrBinary, size_t length) { @@ -144,7 +154,10 @@ std::optional MessageSerializer::Deserialize( auto headerByte = rawMessageJsonOrBinary[1]; if (headerByte == '{') { - spdlog::trace("MessageSerializer::Deserialize - Encountered JSON message"); + std::string s(reinterpret_cast(rawMessageJsonOrBinary) + 1, + length - 1); + spdlog::trace( + "MessageSerializer::Deserialize - Encountered JSON message {}", s); for (auto fn : deserializerFns) { if (fn) { auto result = fn(rawMessageJsonOrBinary, length); diff --git a/skymp5-server/cpp/messages/MessageSerializerFactory.h b/skymp5-server/cpp/messages/MessageSerializerFactory.h index 44313b24d8..fd22618693 100644 --- a/skymp5-server/cpp/messages/MessageSerializerFactory.h +++ b/skymp5-server/cpp/messages/MessageSerializerFactory.h @@ -27,7 +27,7 @@ enum class DeserializeInputFormat struct DeserializeResult { MsgType msgType = MsgType::Invalid; - std::unique_ptr message; + std::unique_ptr message; DeserializeInputFormat format = DeserializeInputFormat::Json; }; @@ -38,6 +38,8 @@ class MessageSerializer public: void Serialize(const char* jsonContent, SLNet::BitStream& outputStream); + void Serialize(const IMessageBase& message, SLNet::BitStream& outputStream); + std::optional Deserialize( const uint8_t* rawMessageJsonOrBinary, size_t length); diff --git a/skymp5-server/cpp/messages/Messages.h b/skymp5-server/cpp/messages/Messages.h index 691b590092..91d02cc4c5 100644 --- a/skymp5-server/cpp/messages/Messages.h +++ b/skymp5-server/cpp/messages/Messages.h @@ -1,3 +1,19 @@ #pragma once +#include "ChangeValuesMessage.h" +#include "DeathStateContainerMessage.h" #include "MovementMessage.h" +#include "OpenContainerMessage.h" +#include "TeleportMessage.h" #include "UpdateAnimationMessage.h" +#include "UpdateEquipmentMessage.h" +#include "UpdatePropertyMessage.h" + +#define REGISTER_MESSAGES \ + REGISTER_MESSAGE(MovementMessage) \ + REGISTER_MESSAGE(UpdateAnimationMessage) \ + REGISTER_MESSAGE(DeathStateContainerMessage) \ + REGISTER_MESSAGE(ChangeValuesMessage) \ + REGISTER_MESSAGE(TeleportMessage) \ + REGISTER_MESSAGE(UpdatePropertyMessage) \ + REGISTER_MESSAGE(OpenContainerMessage) \ + REGISTER_MESSAGE(UpdateEquipmentMessage) diff --git a/skymp5-server/cpp/messages/MovementMessage.h b/skymp5-server/cpp/messages/MovementMessage.h index 751f438b74..beb2493936 100644 --- a/skymp5-server/cpp/messages/MovementMessage.h +++ b/skymp5-server/cpp/messages/MovementMessage.h @@ -22,7 +22,7 @@ const std::string& ToString(RunMode runMode); RunMode RunModeFromString(std::string_view str); -struct MovementMessage : public MessageBase +struct MovementMessage : public MessageBase { const static char kMsgType = static_cast(MsgType::UpdateMovement); const static char kHeaderByte = 'M'; diff --git a/skymp5-server/cpp/messages/MsgType.h b/skymp5-server/cpp/messages/MsgType.h index 513c4251e0..24f1985ae1 100644 --- a/skymp5-server/cpp/messages/MsgType.h +++ b/skymp5-server/cpp/messages/MsgType.h @@ -23,6 +23,8 @@ enum class MsgType : uint8_t OnHit = 17, DeathStateContainer = 18, DropItem = 19, + Teleport = 20, + OpenContainer = 21, Max }; diff --git a/skymp5-server/cpp/messages/OpenContainerMessage.cpp b/skymp5-server/cpp/messages/OpenContainerMessage.cpp new file mode 100644 index 0000000000..9f9f06063d --- /dev/null +++ b/skymp5-server/cpp/messages/OpenContainerMessage.cpp @@ -0,0 +1,28 @@ +#include "OpenContainerMessage.h" +#include +#include + +void OpenContainerMessage::WriteBinary(SLNet::BitStream& stream) const +{ + stream.Write(target); +} + +void OpenContainerMessage::ReadBinary(SLNet::BitStream& stream) +{ + stream.Read(target); +} + +void OpenContainerMessage::WriteJson(nlohmann::json& json) const +{ + nlohmann::json res = nlohmann::json::object(); + res["t"] = kMsgType; + res["target"] = target; + json = std::move(res); +} + +void OpenContainerMessage::ReadJson(const nlohmann::json& json) +{ + OpenContainerMessage res; + res.target = json.at("target").get(); + *this = std::move(res); +} diff --git a/skymp5-server/cpp/messages/OpenContainerMessage.h b/skymp5-server/cpp/messages/OpenContainerMessage.h new file mode 100644 index 0000000000..9ba9e964eb --- /dev/null +++ b/skymp5-server/cpp/messages/OpenContainerMessage.h @@ -0,0 +1,17 @@ +#pragma once +#include "MessageBase.h" +#include "MsgType.h" +#include + +struct OpenContainerMessage : public MessageBase +{ + const static char kMsgType = static_cast(MsgType::OpenContainer); + const static char kHeaderByte = static_cast(MsgType::OpenContainer); + + void WriteBinary(SLNet::BitStream& stream) const override; + void ReadBinary(SLNet::BitStream& stream) override; + void WriteJson(nlohmann::json& json) const override; + void ReadJson(const nlohmann::json& json) override; + + uint32_t target = 0; +}; diff --git a/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.cpp b/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.cpp index c9cb4347a4..9a38f2f19e 100644 --- a/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.cpp +++ b/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.cpp @@ -1,4 +1,5 @@ #include "BitStreamUtil.h" +#include void SerializationUtil::WriteToBitStream(SLNet::BitStream& stream, const std::string& str) @@ -20,3 +21,21 @@ void SerializationUtil::ReadFromBitStream(SLNet::BitStream& stream, str.clear(); } } + +void SerializationUtil::WriteToBitStream(SLNet::BitStream& stream, + const nlohmann::json& json) +{ + WriteToBitStream(stream, json.dump()); +} + +void SerializationUtil::ReadFromBitStream(SLNet::BitStream& stream, + nlohmann::json& json) +{ + std::string s; + ReadFromBitStream(stream, s); + try { + json = nlohmann::json::parse(s); + } catch (nlohmann::json::parse_error& e) { + json = nlohmann::json{}; + } +} diff --git a/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.h b/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.h index bb5e7e066d..2c40f2153b 100644 --- a/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.h +++ b/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -45,6 +46,10 @@ void WriteToBitStream(SLNet::BitStream& stream, const std::string& str); void ReadFromBitStream(SLNet::BitStream& stream, std::string& str); +void WriteToBitStream(SLNet::BitStream& stream, const nlohmann::json& json); + +void ReadFromBitStream(SLNet::BitStream& stream, nlohmann::json& json); + template T ReadFromBitStream(SLNet::BitStream& stream); diff --git a/skymp5-server/cpp/messages/TeleportMessage.cpp b/skymp5-server/cpp/messages/TeleportMessage.cpp new file mode 100644 index 0000000000..ae70beef64 --- /dev/null +++ b/skymp5-server/cpp/messages/TeleportMessage.cpp @@ -0,0 +1,50 @@ +#include "TeleportMessage.h" +#include +#include + +void TeleportMessage::WriteBinary(SLNet::BitStream& stream) const +{ + stream.Write(idx); + for (int i = 0; i < std::size(pos); ++i) { + stream.Write(pos[i]); + } + for (int i = 0; i < std::size(rot); ++i) { + stream.Write(rot[i]); + } + stream.Write(worldOrCell); +} + +void TeleportMessage::ReadBinary(SLNet::BitStream& stream) +{ + stream.Read(idx); + for (int i = 0; i < std::size(pos); ++i) { + stream.Read(pos[i]); + } + for (int i = 0; i < std::size(rot); ++i) { + stream.Read(rot[i]); + } + stream.Read(worldOrCell); +} + +void TeleportMessage::WriteJson(nlohmann::json& json) const +{ + nlohmann::json res = nlohmann::json::object(); + res["t"] = kMsgType; + res["pos"] = pos; + res["rot"] = rot; + res["worldOrCell"] = worldOrCell; + res["idx"] = idx; + json = std::move(res); +} + +void TeleportMessage::ReadJson(const nlohmann::json& json) +{ + TeleportMessage res; + res.idx = json.at("idx").get(); + + res.pos = json.at("pos").get>(); + res.rot = json.at("rot").get>(); + res.worldOrCell = json.at("worldOrCell").get(); + + *this = std::move(res); +} diff --git a/skymp5-server/cpp/messages/TeleportMessage.h b/skymp5-server/cpp/messages/TeleportMessage.h new file mode 100644 index 0000000000..8b92b5aea3 --- /dev/null +++ b/skymp5-server/cpp/messages/TeleportMessage.h @@ -0,0 +1,20 @@ +#pragma once +#include "MessageBase.h" +#include "MsgType.h" +#include + +struct TeleportMessage : public MessageBase +{ + const static char kMsgType = static_cast(MsgType::Teleport); + const static char kHeaderByte = static_cast(MsgType::Teleport); + + void WriteBinary(SLNet::BitStream& stream) const override; + void ReadBinary(SLNet::BitStream& stream) override; + void WriteJson(nlohmann::json& json) const override; + void ReadJson(const nlohmann::json& json) override; + + uint32_t idx = 0; + + std::array pos, rot; + uint32_t worldOrCell = 0; +}; diff --git a/skymp5-server/cpp/messages/UpdateAnimationMessage.h b/skymp5-server/cpp/messages/UpdateAnimationMessage.h index 1b582d087f..90146b4f3e 100644 --- a/skymp5-server/cpp/messages/UpdateAnimationMessage.h +++ b/skymp5-server/cpp/messages/UpdateAnimationMessage.h @@ -5,7 +5,7 @@ #include #include -struct UpdateAnimationMessage : public MessageBase +struct UpdateAnimationMessage : public MessageBase { const static char kMsgType = static_cast(MsgType::UpdateAnimation); const static char kHeaderByte = static_cast(MsgType::UpdateAnimation); diff --git a/skymp5-server/cpp/messages/UpdateEquipmentMessage.cpp b/skymp5-server/cpp/messages/UpdateEquipmentMessage.cpp new file mode 100644 index 0000000000..187ef2874f --- /dev/null +++ b/skymp5-server/cpp/messages/UpdateEquipmentMessage.cpp @@ -0,0 +1,35 @@ +#include "UpdateEquipmentMessage.h" + +#include +#include + +#include "SerializationUtil/BitStreamUtil.h" + +void UpdateEquipmentMessage::WriteBinary(SLNet::BitStream& stream) const +{ + stream.Write(idx); + SerializationUtil::WriteToBitStream(stream, data); +} + +void UpdateEquipmentMessage::ReadBinary(SLNet::BitStream& stream) +{ + stream.Read(idx); + SerializationUtil::ReadFromBitStream(stream, data); +} + +void UpdateEquipmentMessage::WriteJson(nlohmann::json& json) const +{ + nlohmann::json res = nlohmann::json::object(); + res["t"] = kMsgType; + res["idx"] = idx; + res["data"] = data; + json = std::move(res); +} + +void UpdateEquipmentMessage::ReadJson(const nlohmann::json& json) +{ + UpdateEquipmentMessage res; + res.idx = json.at("idx").get(); + res.data = json.at("data"); + *this = std::move(res); +} diff --git a/skymp5-server/cpp/messages/UpdateEquipmentMessage.h b/skymp5-server/cpp/messages/UpdateEquipmentMessage.h new file mode 100644 index 0000000000..f62c309139 --- /dev/null +++ b/skymp5-server/cpp/messages/UpdateEquipmentMessage.h @@ -0,0 +1,22 @@ +#pragma once + +#include "MessageBase.h" +#include "MsgType.h" +#include +#include +#include +#include + +struct UpdateEquipmentMessage : public MessageBase +{ + const static char kMsgType = static_cast(MsgType::UpdateEquipment); + const static char kHeaderByte = static_cast(MsgType::UpdateEquipment); + + void WriteBinary(SLNet::BitStream& stream) const override; + void ReadBinary(SLNet::BitStream& stream) override; + void WriteJson(nlohmann::json& json) const override; + void ReadJson(const nlohmann::json& json) override; + + uint32_t idx = 0; + nlohmann::json data; // TODO: static typing +}; diff --git a/skymp5-server/cpp/messages/UpdatePropertyMessage.cpp b/skymp5-server/cpp/messages/UpdatePropertyMessage.cpp new file mode 100644 index 0000000000..6ff4a345ee --- /dev/null +++ b/skymp5-server/cpp/messages/UpdatePropertyMessage.cpp @@ -0,0 +1,52 @@ +#include "UpdatePropertyMessage.h" + +#include +#include + +#include "SerializationUtil/BitStreamUtil.h" + +void UpdatePropertyMessage::WriteBinary(SLNet::BitStream& stream) const +{ + stream.Write(idx); + SerializationUtil::WriteToBitStream(stream, propName); + stream.Write(refrId); + SerializationUtil::WriteToBitStream(stream, data); + SerializationUtil::WriteToBitStream(stream, baseRecordType); +} + +void UpdatePropertyMessage::ReadBinary(SLNet::BitStream& stream) +{ + stream.Read(idx); + SerializationUtil::ReadFromBitStream(stream, propName); + stream.Read(refrId); + SerializationUtil::ReadFromBitStream(stream, data); + SerializationUtil::ReadFromBitStream(stream, baseRecordType); +} + +void UpdatePropertyMessage::WriteJson(nlohmann::json& json) const +{ + nlohmann::json res = nlohmann::json::object(); + res["t"] = kMsgType; + res["idx"] = idx; + res["propName"] = propName; + res["refrId"] = refrId; + res["data"] = data; + res["baseRecordType"] = + baseRecordType ? nlohmann::json(*baseRecordType) : nlohmann::json{}; + json = std::move(res); +} + +void UpdatePropertyMessage::ReadJson(const nlohmann::json& json) +{ + UpdatePropertyMessage res; + res.idx = json.at("idx").get(); + res.propName = json.at("propName").get(); + res.refrId = json.at("refrId").get(); + res.data = json.at("data"); + + auto it = json.find("baseRecordType"); + if (it != json.end()) { + res.baseRecordType = it->get(); + } + *this = std::move(res); +} diff --git a/skymp5-server/cpp/messages/UpdatePropertyMessage.h b/skymp5-server/cpp/messages/UpdatePropertyMessage.h new file mode 100644 index 0000000000..734133e716 --- /dev/null +++ b/skymp5-server/cpp/messages/UpdatePropertyMessage.h @@ -0,0 +1,25 @@ +#pragma once + +#include "MessageBase.h" +#include "MsgType.h" +#include +#include +#include +#include + +struct UpdatePropertyMessage : public MessageBase +{ + const static char kMsgType = static_cast(MsgType::UpdateProperty); + const static char kHeaderByte = static_cast(MsgType::UpdateProperty); + + void WriteBinary(SLNet::BitStream& stream) const override; + void ReadBinary(SLNet::BitStream& stream) override; + void WriteJson(nlohmann::json& json) const override; + void ReadJson(const nlohmann::json& json) override; + + uint32_t idx = 0; + std::string propName; + uint32_t refrId = 0; + nlohmann::json data; + std::optional baseRecordType; +}; diff --git a/skymp5-server/cpp/server_guest_lib/ActionListener.cpp b/skymp5-server/cpp/server_guest_lib/ActionListener.cpp index ab6909fb4b..3a98e1f88a 100644 --- a/skymp5-server/cpp/server_guest_lib/ActionListener.cpp +++ b/skymp5-server/cpp/server_guest_lib/ActionListener.cpp @@ -19,6 +19,8 @@ #include #include +#include "UpdateEquipmentMessage.h" + MpActor* ActionListener::SendToNeighbours( uint32_t idx, const simdjson::dom::element& jMessage, Networking::UserId userId, Networking::PacketData data, size_t length, @@ -278,14 +280,10 @@ void RecalculateWorn(MpObjectReference& refr) if (!actor) { continue; } - std::string s; - s += Networking::MinPacketId; - s += nlohmann::json{ - { "t", MsgType::UpdateEquipment }, - { "idx", ac->GetIdx() }, - { "data", newEq.ToJson() } - }.dump(); - actor->SendToUser(s.data(), s.size(), true); + UpdateEquipmentMessage msg; + msg.data = newEq.ToJson(); + msg.idx = ac->GetIdx(); + actor->SendToUser(msg, true); } } @@ -489,14 +487,16 @@ void ActionListener::OnHostAttempt(const RawMessageData& rawMsgData, uint32_t remoteId) { MpActor* me = partOne.serverState.ActorByUser(rawMsgData.userId); - if (!me) + if (!me) { throw std::runtime_error("Unable to host without actor attached"); + } auto& remote = partOne.worldState.GetFormAt(remoteId); auto user = partOne.serverState.UserByActor(dynamic_cast(&remote)); - if (user != Networking::InvalidUserId) + if (user != Networking::InvalidUserId) { return; + } auto& hoster = partOne.worldState.hosters[remoteId]; const uint32_t prevHoster = hoster; @@ -528,6 +528,24 @@ void ActionListener::OnHostAttempt(const RawMessageData& rawMsgData, R"({ "type": "hostStart", "target": %llu })", longFormId); + // Otherwise, health percentage would remain unsynced until someone hits + // npc + auto formId = remote.GetFormId(); + partOne.worldState.SetTimer(std::chrono::seconds(1)) + .Then([this, formId](Viet::Void) { + // Check if form is still here + auto& remote = partOne.worldState.GetFormAt(formId); + + auto changeForm = remote.GetChangeForm(); + + ChangeValuesMessage msg; + msg.idx = remote.GetIdx(); + msg.health = changeForm.actorValues.healthPercentage; + msg.magicka = changeForm.actorValues.magickaPercentage; + msg.stamina = changeForm.actorValues.staminaPercentage; + remote.SendToUser(msg, true); // in fact sends to hoster + }); + if (MpActor* prevHosterActor = dynamic_cast( partOne.worldState.LookupFormById(prevHoster).get())) { auto prevHosterUser = partOne.serverState.UserByActor(prevHosterActor); @@ -546,11 +564,13 @@ void ActionListener::OnCustomEvent(const RawMessageData& rawMsgData, simdjson::dom::element& args) { auto ac = partOne.serverState.ActorByUser(rawMsgData.userId); - if (!ac) + if (!ac) { return; + } - if (eventName[0] != '_') + if (eventName[0] != '_') { return; + } for (auto& listener : partOne.GetListeners()) { listener->OnMpApiEvent(eventName, args, ac->GetFormId()); @@ -608,31 +628,38 @@ bool IsUnarmedAttack(const uint32_t sourceFormId) } float CalculateCurrentHealthPercentage(const MpActor& actor, float damage, - float healthPercentage) + float healthPercentage, + float* outBaseHealth) { uint32_t baseId = actor.GetBaseId(); uint32_t raceId = actor.GetRaceId(); WorldState* espmProvider = actor.GetParent(); float baseHealth = GetBaseActorValues(espmProvider, baseId, raceId).health; + if (outBaseHealth) { + *outBaseHealth = baseHealth; + } + float damagePercentage = damage / baseHealth; float currentHealthPercentage = healthPercentage - damagePercentage; return currentHealthPercentage; } -float GetReach(const MpActor& actor, const uint32_t source) +float GetReach(const MpActor& actor, const uint32_t source, + float reachHotfixMult) { auto espmProvider = actor.GetParent(); if (IsUnarmedAttack(source)) { uint32_t raceId = actor.GetRaceId(); - return espm::GetData(raceId, espmProvider).unarmedReach; + return reachHotfixMult * + espm::GetData(raceId, espmProvider).unarmedReach; } auto weapDNAM = espm::GetData(source, espmProvider).weapDNAM; float fCombatDistance = espm::GetData(espm::GMST::kFCombatDistance, espmProvider) .value; float weaponReach = weapDNAM ? weapDNAM->reach : 0; - return weaponReach * fCombatDistance; + return reachHotfixMult * weaponReach * fCombatDistance; } NiPoint3 RotateZ(const NiPoint3& point, float angle) @@ -687,7 +714,14 @@ bool IsDistanceValid(const MpActor& actor, const MpActor& targetActor, const HitData& hitData) { float sqrDistance = GetSqrDistanceToBounds(actor, targetActor); - float reach = GetReach(actor, hitData.source); + + // TODO: fix bounding boxes for creatures such as chicken, mudcrab, etc + float reachPveHotfixMult = + (actor.GetBaseId() <= 0x7 && targetActor.GetBaseId() <= 0x7) + ? 1.f + : std::numeric_limits::infinity(); + + float reach = GetReach(actor, hitData.source, reachPveHotfixMult); // For bow/crossbow shots we don't want to check melee radius if (!hitData.isBashAttack) { @@ -743,26 +777,39 @@ void ActionListener::OnHit(const RawMessageData& rawMsgData_, const HitData& hitData_) { auto currentHitTime = std::chrono::steady_clock::now(); - MpActor* aggressor = partOne.serverState.ActorByUser(rawMsgData_.userId); - if (!aggressor) { + MpActor* myActor = partOne.serverState.ActorByUser(rawMsgData_.userId); + if (!myActor) { throw std::runtime_error("Unable to change values without Actor attached"); } - if (aggressor->IsDead()) { - spdlog::debug(fmt::format("{:x} actor is dead and can't attack", - aggressor->GetFormId())); - return; - } + MpActor* aggressor = nullptr; HitData hitData = hitData_; - if (hitData.aggressor == 0x14) { + aggressor = myActor; hitData.aggressor = aggressor->GetFormId(); } else { - throw std::runtime_error("Events from non aggressor is not supported yet"); + aggressor = &partOne.worldState.GetFormAt(hitData.aggressor); + auto it = partOne.worldState.hosters.find(hitData.aggressor); + if (it == partOne.worldState.hosters.end() || + it->second != myActor->GetFormId()) { + spdlog::error("SendToNeighbours - No permission to send OnHit with " + "aggressor actor {:x}", + aggressor->GetFormId()); + return; + } } + if (hitData.target == 0x14) { - hitData.target = aggressor->GetFormId(); + hitData.target = myActor->GetFormId(); + } + + if (aggressor->IsDead()) { + spdlog::debug(fmt::format("{:x} actor is dead and can't attack. " + "requesting respawn in order to fix death state", + aggressor->GetFormId())); + aggressor->RespawnWithDelay(true); + return; } if (aggressor->GetEquipment().inv.HasItem(hitData.source) == false && @@ -778,7 +825,34 @@ void ActionListener::OnHit(const RawMessageData& rawMsgData_, return; }; - auto& targetActor = partOne.worldState.GetFormAt(hitData.target); + auto refr = std::dynamic_pointer_cast( + partOne.worldState.LookupFormById(hitData.target)); + if (!refr) { + spdlog::error("ActionListener::OnHit - MpObjectReference not found for " + "hitData.target {:x}", + hitData.target); + return; + } + + auto& browser = partOne.worldState.GetEspm().GetBrowser(); + std::array args; + args[0] = VarValue(aggressor->ToGameObject()); // akAgressor + args[1] = VarValue(std::make_shared( + browser.LookupById(hitData.source))); // akSource + args[2] = VarValue::None(); // akProjectile + args[3] = VarValue(hitData.isPowerAttack); // abPowerAttack + args[4] = VarValue(hitData.isSneakAttack); // abSneakAttack + args[5] = VarValue(hitData.isBashAttack); // abBashAttack + args[6] = VarValue(hitData.isHitBlocked); // abHitBlocked + refr->SendPapyrusEvent("OnHit", args.data(), args.size()); + + auto targetActorPtr = dynamic_cast(refr.get()); + if (!targetActorPtr) { + return; // Not an actor, damage calculation is not needed + } + + auto& targetActor = *targetActorPtr; + auto lastHitTime = targetActor.GetLastHitTime(); std::chrono::duration timePassed = currentHitTime - lastHitTime; @@ -799,7 +873,14 @@ void ActionListener::OnHit(const RawMessageData& rawMsgData_, if (IsDistanceValid(*aggressor, targetActor, hitData) == false) { float distance = std::sqrt(GetSqrDistanceToBounds(*aggressor, targetActor)); - float reach = GetReach(*aggressor, hitData.source); + + // TODO: fix bounding boxes for creatures such as chicken, mudcrab, etc + float reachPveHotfixMult = + (aggressor->GetBaseId() <= 0x7 && targetActor.GetBaseId() <= 0x7) + ? 1.f + : std::numeric_limits::infinity(); + + float reach = GetReach(*aggressor, hitData.source, reachPveHotfixMult); uint32_t aggressorId = aggressor->GetFormId(); uint32_t targetId = targetActor.GetFormId(); spdlog::debug( @@ -812,16 +893,15 @@ void ActionListener::OnHit(const RawMessageData& rawMsgData_, ActorValues currentActorValues = targetActor.GetChangeForm().actorValues; float healthPercentage = currentActorValues.healthPercentage; - float magickaPercentage = currentActorValues.magickaPercentage; - float staminaPercentage = currentActorValues.staminaPercentage; hitData.isHitBlocked = targetActor.IsBlockActive() ? ShouldBeBlocked(*aggressor, targetActor) : false; float damage = partOne.CalculateDamage(*aggressor, targetActor, hitData); damage = damage < 0.f ? 0.f : damage; - currentActorValues.healthPercentage = - CalculateCurrentHealthPercentage(targetActor, damage, healthPercentage); + float outBaseHealth = 0.f; + currentActorValues.healthPercentage = CalculateCurrentHealthPercentage( + targetActor, damage, healthPercentage, &outBaseHealth); currentActorValues.healthPercentage = currentActorValues.healthPercentage < 0.f @@ -831,11 +911,10 @@ void ActionListener::OnHit(const RawMessageData& rawMsgData_, targetActor.NetSetPercentages(currentActorValues, aggressor); targetActor.SetLastHitTime(); - spdlog::debug("Target {0:x} is hitted by {1} damage. Current health " - "percentage: {2}. Last " - "health percentage: {3}. (Last: {3} => Current: {2})", + spdlog::debug("Target {0:x} is hitted by {1} damage. Percentage was: {3}, " + "percentage now: {2}, base health: {4})", hitData.target, damage, currentActorValues.healthPercentage, - healthPercentage); + healthPercentage, outBaseHealth); } void ActionListener::OnUnknown(const RawMessageData& rawMsgData) diff --git a/skymp5-server/cpp/server_guest_lib/ConsoleCommands.cpp b/skymp5-server/cpp/server_guest_lib/ConsoleCommands.cpp index e8523ad426..fd88cd8201 100644 --- a/skymp5-server/cpp/server_guest_lib/ConsoleCommands.cpp +++ b/skymp5-server/cpp/server_guest_lib/ConsoleCommands.cpp @@ -50,6 +50,14 @@ namespace { void EnsureAdmin(const MpActor& me) { + if (auto worldState = me.GetParent()) { + if (worldState->enableConsoleCommandsForAll) { + spdlog::trace("Bypassing EnsureAdmin check: enableConsoleCommandsForAll " + "set to true"); + return; + } + } + bool isAdmin = me.GetConsoleCommandsAllowedFlag(); if (!isAdmin) { throw std::runtime_error("Not enough permissions to use this command"); diff --git a/skymp5-server/cpp/server_guest_lib/FormCallbacks.h b/skymp5-server/cpp/server_guest_lib/FormCallbacks.h index 3cab10d89f..1fcb241197 100644 --- a/skymp5-server/cpp/server_guest_lib/FormCallbacks.h +++ b/skymp5-server/cpp/server_guest_lib/FormCallbacks.h @@ -1,4 +1,5 @@ #pragma once +#include "MessageBase.h" #include class MpObjectReference; @@ -9,11 +10,13 @@ class FormCallbacks public: using SubscribeCallback = std::function; - using SendToUserFn = std::function; - using SendToUserDeferredFn = - std::function; + using SendToUserFn = std::function; + + // TODO: use MessageBase instead of raw data + using SendToUserDeferredFn = std::function; SubscribeCallback subscribe, unsubscribe; SendToUserFn sendToUser; @@ -21,8 +24,7 @@ class FormCallbacks static FormCallbacks DoNothing() { - return { [](auto, auto) {}, [](auto, auto) {}, - [](auto, auto, auto, auto) {}, - [](auto, auto, auto, auto, auto) {} }; + return { [](auto, auto) {}, [](auto, auto) {}, [](auto, auto&, auto) {}, + [](auto, auto, auto, auto, auto, auto) {} }; } }; diff --git a/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.cpp b/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.cpp index fca1cf3799..afe2d2a134 100644 --- a/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.cpp +++ b/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.cpp @@ -1,6 +1,8 @@ #include "GetBaseActorValues.h" #include "WorldState.h" +#include + void BaseActorValues::VisitBaseActorValues(BaseActorValues& baseActorValues, MpChangeForm& changeForm, const PropertiesVisitor& visitor) @@ -31,12 +33,43 @@ BaseActorValues GetBaseActorValues(WorldState* worldState, uint32_t baseId, auto npcData = espm::GetData(baseId, worldState); uint32_t raceID = raceIdOverride ? raceIdOverride : npcData.race; auto raceData = espm::GetData(raceID, worldState); + BaseActorValues actorValues; + actorValues.health = raceData.startingHealth + npcData.healthOffset; + if (actorValues.health <= 0) { + spdlog::warn("GetBaseActorValues {:x} {:x} - Negative Health found: " + "startingHealth={}, healthOffset={}, defaulting to 100", + baseId, raceIdOverride, raceData.startingHealth, + npcData.healthOffset); + actorValues.health = 100.f; + } + actorValues.magicka = raceData.startingMagicka + npcData.magickaOffset; + if (actorValues.magicka <= 0) { + spdlog::warn("GetBaseActorValues {:x} {:x} - Negative Magicka found: " + "startingMagicka={}, magickaOffset={}, defaulting to 100", + baseId, raceIdOverride, raceData.startingMagicka, + npcData.magickaOffset); + actorValues.magicka = 100.f; + } + actorValues.stamina = raceData.startingStamina + npcData.staminaOffset; + if (actorValues.stamina <= 0) { + spdlog::warn("GetBaseActorValues {:x} {:x} - Negative Stamina found: " + "startingStamina={}, staminaOffset={}, defaulting to 100", + baseId, raceIdOverride, raceData.startingStamina, + npcData.staminaOffset); + actorValues.stamina = 100.f; + } + actorValues.healRate = raceData.healRegen; actorValues.magickaRate = raceData.magickaRegen; actorValues.staminaRate = raceData.staminaRegen; + + spdlog::trace( + "GetBaseActorValues {:x} {:x} - startingHealth={}, healthOffset={}", + baseId, raceIdOverride, raceData.startingHealth, npcData.healthOffset); + return actorValues; } diff --git a/skymp5-server/cpp/server_guest_lib/MpActor.cpp b/skymp5-server/cpp/server_guest_lib/MpActor.cpp index 6b94964795..f6ffc6aa2b 100644 --- a/skymp5-server/cpp/server_guest_lib/MpActor.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpActor.cpp @@ -22,6 +22,9 @@ #include #include +#include "ChangeValuesMessage.h" +#include "TeleportMessage.h" + struct MpActor::Impl { std::map> snippetPromises; @@ -163,21 +166,22 @@ void MpActor::Disable() pImpl->snippetPromises.clear(); } -void MpActor::SendToUser(const void* data, size_t size, bool reliable) +void MpActor::SendToUser(const IMessageBase& message, bool reliable) { if (callbacks->sendToUser) { - callbacks->sendToUser(this, data, size, reliable); + callbacks->sendToUser(this, message, reliable); } else { throw std::runtime_error("sendToUser is nullptr"); } } void MpActor::SendToUserDeferred(const void* data, size_t size, bool reliable, - int deferredChannelId) + int deferredChannelId, + bool overwritePreviousChannelMessages) { if (callbacks->sendToUserDeferred) { callbacks->sendToUserDeferred(this, data, size, reliable, - deferredChannelId); + deferredChannelId, overwritePreviousChannelMessages); } else { throw std::runtime_error("sendToUserDeferred is nullptr"); } @@ -325,18 +329,12 @@ void MpActor::SetPercentages(const ActorValues& actorValues, void MpActor::NetSendChangeValues(const ActorValues& actorValues) { - std::string s; - s += Networking::MinPacketId; - s += nlohmann::json{ - { "t", MsgType::ChangeValues }, - { "data", - { - { "health", actorValues.healthPercentage }, - { "magicka", actorValues.magickaPercentage }, - { "stamina", actorValues.staminaPercentage }, - } } - }.dump(); - SendToUser(s.data(), s.size(), true); + ChangeValuesMessage message; + message.idx = GetIdx(); + message.health = actorValues.healthPercentage; + message.magicka = actorValues.magickaPercentage; + message.stamina = actorValues.staminaPercentage; + SendToUser(message, true); } void MpActor::NetSetPercentages(const ActorValues& actorValues, @@ -396,7 +394,7 @@ bool MpActor::IsSpellLearned(const uint32_t baseId) const const auto npcData = espm::GetData(GetBaseId(), GetParent()); const auto raceData = espm::GetData(npcData.race, GetParent()); - return npcData.spells.contains(baseId) || raceData.spells.contains(baseId) || + return npcData.spells.count(baseId) || raceData.spells.count(baseId) || ChangeForm().learnedSpells.IsSpellLearned(baseId); } @@ -457,8 +455,8 @@ void MpActor::SendAndSetDeathState(bool isDead, bool shouldTeleport) float attribute = isDead ? 0.f : 1.f; auto position = GetSpawnPoint(); - std::string respawnMsg = GetDeathStateMsg(position, isDead, shouldTeleport); - SendToUser(respawnMsg.data(), respawnMsg.size(), true); + auto respawnMsg = GetDeathStateMsg(position, isDead, shouldTeleport); + SendToUser(respawnMsg, true); EditChangeForm([&](MpChangeForm& changeForm) { changeForm.isDead = isDead; @@ -473,40 +471,33 @@ void MpActor::SendAndSetDeathState(bool isDead, bool shouldTeleport) } } -std::string MpActor::GetDeathStateMsg(const LocationalData& position, - bool isDead, bool shouldTeleport) +DeathStateContainerMessage MpActor::GetDeathStateMsg( + const LocationalData& position, bool isDead, bool shouldTeleport) { - nlohmann::json tTeleport = nlohmann::json{}; - nlohmann::json tChangeValues = nlohmann::json{}; - nlohmann::json tIsDead = PreparePropertyMessage(this, "isDead", isDead); + DeathStateContainerMessage res; + res.tIsDead = PreparePropertyMessage(this, "isDead", isDead); if (shouldTeleport) { - tTeleport = nlohmann::json{ - { "pos", { position.pos[0], position.pos[1], position.pos[2] } }, - { "rot", { position.rot[0], position.rot[1], position.rot[2] } }, - { "worldOrCell", - position.cellOrWorldDesc.ToFormId(GetParent()->espmFiles) }, - { "type", "teleport" } - }; - } - if (isDead == false) { - const float attribute = 1.f; - tChangeValues = nlohmann::json{ { "t", MsgType::ChangeValues }, - { "data", - { { "health", attribute }, - { "magicka", attribute }, - { "stamina", attribute } } } }; - } - - std::string DeathStateMsg; - DeathStateMsg += Networking::MinPacketId; - DeathStateMsg += nlohmann::json{ - { "t", MsgType::DeathStateContainer }, - { "tTeleport", tTeleport }, - { "tChangeValues", tChangeValues }, - { "tIsDead", tIsDead } - }.dump(); - return DeathStateMsg; + res.tTeleport = TeleportMessage(); + res.tTeleport->idx = GetIdx(); + std::copy(&position.pos[0], &position.pos[0] + 3, + std::begin(res.tTeleport->pos)); + std::copy(&position.rot[0], &position.rot[0] + 3, + std::begin(res.tTeleport->rot)); + res.tTeleport->worldOrCell = + position.cellOrWorldDesc.ToFormId(GetParent()->espmFiles); + } + + if (!isDead) { + constexpr float kAttributePercentageFull = 1.f; + res.tChangeValues = ChangeValuesMessage(); + res.tChangeValues->idx = GetIdx(); + res.tChangeValues->health = kAttributePercentageFull; + res.tChangeValues->magicka = kAttributePercentageFull; + res.tChangeValues->stamina = kAttributePercentageFull; + } + + return res; } void MpActor::MpApiDeath(MpActor* killer) @@ -663,16 +654,12 @@ void MpActor::Respawn(bool shouldTeleport) void MpActor::Teleport(const LocationalData& position) { - std::string teleportMsg; - teleportMsg += Networking::MinPacketId; - teleportMsg += nlohmann::json{ - { "pos", { position.pos[0], position.pos[1], position.pos[2] } }, - { "rot", { position.rot[0], position.rot[1], position.rot[2] } }, - { "worldOrCell", - position.cellOrWorldDesc.ToFormId(GetParent()->espmFiles) }, - { "type", "teleport" } - }.dump(); - SendToUser(teleportMsg.data(), teleportMsg.size(), true); + TeleportMessage msg; + msg.idx = GetIdx(); + std::copy(&position.pos[0], &position.pos[0] + 3, std::begin(msg.pos)); + std::copy(&position.rot[0], &position.rot[0] + 3, std::begin(msg.rot)); + msg.worldOrCell = position.cellOrWorldDesc.ToFormId(GetParent()->espmFiles); + SendToUser(msg, true); SetCellOrWorldObsolete(position.cellOrWorldDesc); SetPos(position.pos); diff --git a/skymp5-server/cpp/server_guest_lib/MpActor.h b/skymp5-server/cpp/server_guest_lib/MpActor.h index 312ba649c8..98eb7d7464 100644 --- a/skymp5-server/cpp/server_guest_lib/MpActor.h +++ b/skymp5-server/cpp/server_guest_lib/MpActor.h @@ -7,6 +7,8 @@ #include #include +#include "DeathStateContainerMessage.h" + class WorldState; struct ActorValues; @@ -42,9 +44,10 @@ class MpActor : public MpObjectReference VisitPropertiesMode mode) override; void Disable() override; - void SendToUser(const void* data, size_t size, bool reliable); + void SendToUser(const IMessageBase& message, bool reliable); void SendToUserDeferred(const void* data, size_t size, bool reliable, - int deferredChannelId); + int deferredChannelId, + bool overwritePreviousChannelMessages); [[nodiscard]] bool OnEquip(uint32_t baseId); @@ -128,8 +131,9 @@ class MpActor : public MpObjectReference void SendAndSetDeathState(bool isDead, bool shouldTeleport); - std::string GetDeathStateMsg(const LocationalData& position, bool isDead, - bool shouldTeleport); + DeathStateContainerMessage GetDeathStateMsg(const LocationalData& position, + bool isDead, + bool shouldTeleport); void MpApiDeath(MpActor* killer = nullptr); void EatItem(uint32_t baseId, espm::Type t); diff --git a/skymp5-server/cpp/server_guest_lib/MpFormGameObject.cpp b/skymp5-server/cpp/server_guest_lib/MpFormGameObject.cpp index 73dc111875..e3e120829d 100644 --- a/skymp5-server/cpp/server_guest_lib/MpFormGameObject.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpFormGameObject.cpp @@ -2,6 +2,9 @@ #include "WorldState.h" +#include +#include + MpFormGameObject::MpFormGameObject(MpForm* form_) : form(form_) , parent(form_ ? form_->GetParent() : nullptr) @@ -33,3 +36,14 @@ bool MpFormGameObject::EqualsByValue(const IGameObject& obj) const } return false; } + +const char* MpFormGameObject::GetStringID() +{ + static std::unordered_map> g_strings; + auto formId = form->GetFormId(); + auto& v = g_strings[formId]; + if (!v) { + v.reset(new std::string(fmt::format("form {:x}", formId))); + } + return v->data(); +} diff --git a/skymp5-server/cpp/server_guest_lib/MpFormGameObject.h b/skymp5-server/cpp/server_guest_lib/MpFormGameObject.h index de9b98990a..e4f141f107 100644 --- a/skymp5-server/cpp/server_guest_lib/MpFormGameObject.h +++ b/skymp5-server/cpp/server_guest_lib/MpFormGameObject.h @@ -11,6 +11,8 @@ class MpFormGameObject : public IGameObject const char* GetParentNativeScript() override; bool EqualsByValue(const IGameObject& obj) const override; + const char* GetStringID() override; + private: WorldState* const parent; MpForm* const form; diff --git a/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp b/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp index d6b4c55d57..933b99caba 100644 --- a/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp @@ -20,20 +20,22 @@ #include #include +#include "OpenContainerMessage.h" +#include "TeleportMessage.h" + constexpr uint32_t kPlayerCharacterLevel = 1; -std::string MpObjectReference::CreatePropertyMessage( +UpdatePropertyMessage MpObjectReference::CreatePropertyMessage( MpObjectReference* self, const char* name, const nlohmann::json& value) { - std::string str; - str += Networking::MinPacketId; - str += PreparePropertyMessage(self, name, value).dump(); - return str; + return PreparePropertyMessage(self, name, value); } -nlohmann::json MpObjectReference::PreparePropertyMessage( +UpdatePropertyMessage MpObjectReference::PreparePropertyMessage( MpObjectReference* self, const char* name, const nlohmann::json& value) { + UpdatePropertyMessage res; + std::string baseRecordType; auto& loader = self->GetParent()->GetEspm(); @@ -42,19 +44,18 @@ nlohmann::json MpObjectReference::PreparePropertyMessage( baseRecordType = base.rec->GetType().ToString(); } - auto object = nlohmann::json{ { "idx", self->GetIdx() }, - { "t", MsgType::UpdateProperty }, - { "propName", name }, - { "refrId", self->GetFormId() }, - { "data", value } }; + res.idx = self->GetIdx(); + res.propName = name; + res.refrId = self->GetFormId(); + res.data = value; // See 'perf: improve game framerate #1186' // Client needs to know if it is DOOR or not if (baseRecordType == "DOOR") { - object["baseRecordType"] = baseRecordType; + res.baseRecordType = baseRecordType; } - return object; + return res; } class OccupantDestroyEventSink : public MpActor::DestroyEventSink @@ -546,12 +547,13 @@ void MpObjectReference::UpdateHoster(uint32_t newHosterId) auto notHostedMsg = CreatePropertyMessage(this, "isHostedByOther", false); for (auto listener : this->GetListeners()) { auto listenerAsActor = dynamic_cast(listener); - if (listenerAsActor) + if (listenerAsActor) { this->SendPropertyTo(newHosterId != 0 && newHosterId != listener->GetFormId() ? hostedMsg : notHostedMsg, *listenerAsActor); + } } } @@ -1140,26 +1142,25 @@ void MpObjectReference::ProcessActivate(MpObjectReference& activationSource) GetWorldOrCell(loader.GetBrowser(), destinationRecord)); static const auto g_pi = std::acos(-1.f); - const NiPoint3 rot = { teleport->rotRadians[0] / g_pi * 180, - teleport->rotRadians[1] / g_pi * 180, - teleport->rotRadians[2] / g_pi * 180 }; - - std::string msg; - msg += Networking::MinPacketId; - msg += nlohmann::json{ - { "pos", { teleport->pos[0], teleport->pos[1], teleport->pos[2] } }, - { "rot", { rot[0], rot[1], rot[2] } }, - { "worldOrCell", teleportWorldOrCell }, - { "type", "teleport" } - }.dump(); - if (actorActivator) - actorActivator->SendToUser(msg.data(), msg.size(), true); + const auto& pos = teleport->pos; + const float rot[] = { teleport->rotRadians[0] / g_pi * 180, + teleport->rotRadians[1] / g_pi * 180, + teleport->rotRadians[2] / g_pi * 180 }; + + TeleportMessage msg; + msg.idx = activationSource.GetIdx(); + std::copy(std::begin(pos), std::end(pos), msg.pos.begin()); + std::copy(std::begin(rot), std::end(rot), msg.rot.begin()); + msg.worldOrCell = teleportWorldOrCell; + + if (actorActivator) { + actorActivator->SendToUser(msg, true); + } activationSource.SetCellOrWorldObsolete( FormDesc::FromFormId(teleportWorldOrCell, worldState->espmFiles)); - activationSource.SetPos( - { teleport->pos[0], teleport->pos[1], teleport->pos[2] }); - activationSource.SetAngle(rot); + activationSource.SetPos({ pos[0], pos[1], pos[2] }); + activationSource.SetAngle({ rot[0], rot[1], rot[2] }); } else { SetOpen(!IsOpen()); @@ -1322,7 +1323,7 @@ void MpObjectReference::SendInventoryUpdate() { "type", "setInventory" } }.dump(); actor->SendToUserDeferred(msg.data(), msg.size(), true, - kChannelSetInventory); + kChannelSetInventory, true); } } @@ -1330,12 +1331,9 @@ void MpObjectReference::SendOpenContainer(uint32_t targetId) { auto actor = dynamic_cast(this); if (actor) { - std::string msg; - msg += Networking::MinPacketId; - msg += nlohmann::json{ - { "target", targetId }, { "type", "openContainer" } - }.dump(); - actor->SendToUser(msg.data(), msg.size(), true); + OpenContainerMessage msg; + msg.target = targetId; + actor->SendToUser(msg, true); } } @@ -1473,11 +1471,12 @@ void MpObjectReference::CheckInteractionAbility(MpObjectReference& refr) void MpObjectReference::SendPropertyToListeners(const char* name, const nlohmann::json& value) { - auto str = CreatePropertyMessage(this, name, value); + auto msg = CreatePropertyMessage(this, name, value); for (auto listener : GetListeners()) { auto listenerAsActor = dynamic_cast(listener); - if (listenerAsActor) - listenerAsActor->SendToUser(str.data(), str.size(), true); + if (listenerAsActor) { + listenerAsActor->SendToUser(msg, true); + } } } @@ -1485,14 +1484,14 @@ void MpObjectReference::SendPropertyTo(const char* name, const nlohmann::json& value, MpActor& target) { - auto str = CreatePropertyMessage(this, name, value); - SendPropertyTo(str, target); + auto msg = CreatePropertyMessage(this, name, value); + SendPropertyTo(msg, target); } -void MpObjectReference::SendPropertyTo(const std::string& preparedPropMsg, +void MpObjectReference::SendPropertyTo(const IMessageBase& preparedPropMsg, MpActor& target) { - target.SendToUser(preparedPropMsg.data(), preparedPropMsg.size(), true); + target.SendToUser(preparedPropMsg, true); } void MpObjectReference::BeforeDestroy() diff --git a/skymp5-server/cpp/server_guest_lib/MpObjectReference.h b/skymp5-server/cpp/server_guest_lib/MpObjectReference.h index 81865fac3d..6a3eb07e0f 100644 --- a/skymp5-server/cpp/server_guest_lib/MpObjectReference.h +++ b/skymp5-server/cpp/server_guest_lib/MpObjectReference.h @@ -21,6 +21,8 @@ #include #include +#include "UpdatePropertyMessage.h" + struct GridPosInfo { uint32_t worldOrCell = 0; @@ -167,10 +169,11 @@ class MpObjectReference return "private.indexed."; } -protected: void SendPapyrusEvent(const char* eventName, const VarValue* arguments = nullptr, size_t argumentsCount = 0) override; + +protected: void Init(WorldState* parent, uint32_t formId, bool hasChangeForm) override; void EnsureBaseContainerAdded(espm::Loader& espm); @@ -178,7 +181,7 @@ class MpObjectReference void SendPropertyToListeners(const char* name, const nlohmann::json& value); void SendPropertyTo(const char* name, const nlohmann::json& value, MpActor& target); - void SendPropertyTo(const std::string& preparedPropMsg, MpActor& target); + void SendPropertyTo(const IMessageBase& preparedPropMsg, MpActor& target); private: void AddContainerObject(const espm::CONT::ContainerObject& containerObject, @@ -214,11 +217,12 @@ class MpObjectReference protected: void BeforeDestroy() override; - std::string CreatePropertyMessage(MpObjectReference* self, const char* name, - const nlohmann::json& value); - nlohmann::json PreparePropertyMessage(MpObjectReference* self, - const char* name, - const nlohmann::json& value); + UpdatePropertyMessage CreatePropertyMessage(MpObjectReference* self, + const char* name, + const nlohmann::json& value); + UpdatePropertyMessage PreparePropertyMessage(MpObjectReference* self, + const char* name, + const nlohmann::json& value); const std::shared_ptr callbacks; }; diff --git a/skymp5-server/cpp/server_guest_lib/PacketParser.cpp b/skymp5-server/cpp/server_guest_lib/PacketParser.cpp index a74ffdeea3..08ee26a03b 100644 --- a/skymp5-server/cpp/server_guest_lib/PacketParser.cpp +++ b/skymp5-server/cpp/server_guest_lib/PacketParser.cpp @@ -79,7 +79,7 @@ void PacketParser::TransformPacketIntoAction(Networking::UserId userId, { message->rot[0], message->rot[1], message->rot[2] }, message->isInJumpState, message->isWeapDrawn, message->isBlocking, message->worldOrCell); - break; + return; } case MsgType::UpdateAnimation: { auto message = @@ -89,15 +89,48 @@ void PacketParser::TransformPacketIntoAction(Networking::UserId userId, animationData.numChanges = message->numChanges; actionListener.OnUpdateAnimation(rawMsgData, message->idx, animationData); - break; + return; + } + case MsgType::UpdateEquipment: { + auto message = + reinterpret_cast(result->message.get()); + auto idx = message->idx; + auto data = pImpl->simdjsonParser.parse(message->data.dump()).value(); + auto inv = Inventory::FromJson(message->data.at("inv")); + auto leftSpell = message->data.contains("leftSpell") + ? message->data.at("leftSpell").get() + : 0; + auto rightSpell = message->data.contains("rightSpell") + ? message->data.at("rightSpell").get() + : 0; + auto voiceSpell = message->data.contains("voiceSpell") + ? message->data.at("voiceSpell").get() + : 0; + auto instantSpell = message->data.contains("instantSpell") + ? message->data.at("instantSpell").get() + : 0; + + actionListener.OnUpdateEquipment(rawMsgData, idx, data, inv, leftSpell, + rightSpell, voiceSpell, instantSpell); + return; + } + case MsgType::ChangeValues: { + auto message = + reinterpret_cast(result->message.get()); + ActorValues actorValues; + actorValues.healthPercentage = message->health; + actorValues.magickaPercentage = message->magicka; + actorValues.staminaPercentage = message->stamina; + actionListener.OnChangeValues(rawMsgData, actorValues); + return; } default: { - spdlog::error("Unhandled MsgType {} after Deserialize", + // likel a binary packet, can't just fall back to simdjson parsing + spdlog::error("PacketParser.cpp doesn't implement MsgType {}", static_cast(result->msgType)); - break; + return; } } - return; } rawMsgData.parsed = @@ -125,46 +158,6 @@ void PacketParser::TransformPacketIntoAction(Networking::UserId userId, actionListener.OnUpdateAppearance(rawMsgData, idx, Appearance::FromJson(jData)); } break; - case MsgType::UpdateEquipment: { - uint32_t idx; - ReadEx(jMessage, JsonPointers::idx, &idx); - simdjson::dom::element data_; - ReadEx(jMessage, JsonPointers::data, &data_); - simdjson::dom::element inv; - ReadEx(data_, JsonPointers::inv, &inv); - - uint32_t leftSpell = 0; - - if (data_.at_pointer(JsonPointers::leftSpell.GetData()).error() == - simdjson::error_code::SUCCESS) { - ReadEx(data_, JsonPointers::leftSpell, &leftSpell); - } - - uint32_t rightSpell = 0; - - if (data_.at_pointer(JsonPointers::rightSpell.GetData()).error() == - simdjson::error_code::SUCCESS) { - ReadEx(data_, JsonPointers::rightSpell, &rightSpell); - } - - uint32_t voiceSpell = 0; - - if (data_.at_pointer(JsonPointers::voiceSpell.GetData()).error() == - simdjson::error_code::SUCCESS) { - ReadEx(data_, JsonPointers::voiceSpell, &voiceSpell); - } - - uint32_t instantSpell = 0; - - if (data_.at_pointer(JsonPointers::instantSpell.GetData()).error() == - simdjson::error_code::SUCCESS) { - ReadEx(data_, JsonPointers::instantSpell, &instantSpell); - } - - actionListener.OnUpdateEquipment(rawMsgData, idx, data_, - Inventory::FromJson(inv), leftSpell, - rightSpell, voiceSpell, instantSpell); - } break; case MsgType::Activate: { simdjson::dom::element data_; ReadEx(jMessage, JsonPointers::data, &data_); @@ -264,16 +257,6 @@ void PacketParser::TransformPacketIntoAction(Networking::UserId userId, actionListener.OnCustomEvent(rawMsgData, eventName, args); break; } - case MsgType::ChangeValues: { - simdjson::dom::element data_; - ReadEx(jMessage, JsonPointers::data, &data_); - ActorValues actorValues; - ReadEx(data_, JsonPointers::health, &actorValues.healthPercentage); - ReadEx(data_, JsonPointers::magicka, &actorValues.magickaPercentage); - ReadEx(data_, JsonPointers::stamina, &actorValues.staminaPercentage); - actionListener.OnChangeValues(rawMsgData, actorValues); - break; - } case MsgType::OnHit: { simdjson::dom::element data_; ReadEx(jMessage, JsonPointers::data, &data_); diff --git a/skymp5-server/cpp/server_guest_lib/PartOne.cpp b/skymp5-server/cpp/server_guest_lib/PartOne.cpp index 99e4175c0a..dc30c2ab34 100644 --- a/skymp5-server/cpp/server_guest_lib/PartOne.cpp +++ b/skymp5-server/cpp/server_guest_lib/PartOne.cpp @@ -4,6 +4,7 @@ #include "FormCallbacks.h" #include "IdManager.h" #include "JsonUtils.h" +#include "MessageSerializerFactory.h" #include "MsgType.h" #include "PacketParser.h" #include @@ -17,16 +18,18 @@ class FakeSendTarget : public Networking::ISendTarget void Send(Networking::UserId targetUserId, Networking::PacketData data, size_t length, bool reliable) override { - std::string s(reinterpret_cast(data + 1), length - 1); - PartOne::Message m; - try { - m = PartOne::Message{ nlohmann::json::parse(s), targetUserId, reliable }; - } catch (std::exception& e) { - std::stringstream ss; - ss << e.what() << std::endl << "`" << s << "`"; - throw std::runtime_error(ss.str()); + static auto g_serializer = + MessageSerializerFactory::CreateMessageSerializer(); + auto deserializeResult = g_serializer->Deserialize(data, length); + nlohmann::json j; + if (deserializeResult) { + deserializeResult->message->WriteJson(j); + } else { + std::string s(reinterpret_cast(data + 1), length - 1); + j = nlohmann::json::parse(s); } - messages.push_back(m); + + messages.push_back(PartOne::Message{ j, targetUserId, reliable }); } std::vector messages; @@ -442,6 +445,9 @@ void PartOne::RequestPacketHistoryPlayback(Networking::UserId userId, FormCallbacks PartOne::CreateFormCallbacks() { + static auto g_serializer = + MessageSerializerFactory::CreateMessageSerializer(); + auto st = &serverState; FormCallbacks::SubscribeCallback @@ -455,18 +461,31 @@ FormCallbacks PartOne::CreateFormCallbacks() }; FormCallbacks::SendToUserFn sendToUser = - [this, st](MpActor* actor, const void* data, size_t size, bool reliable) { + [this, st](MpActor* actor, const IMessageBase& message, bool reliable) { + SLNet::BitStream stream; + g_serializer->Serialize(message, stream); + + auto hosterIterator = worldState.hosters.find(actor->GetFormId()); + if (hosterIterator != worldState.hosters.end()) { + auto& hosterActor = + worldState.GetFormAt(hosterIterator->second); + actor = &hosterActor; + // Direct messages such as Teleport, ChangeValues to our host + } + auto targetuserId = st->UserByActor(actor); if (targetuserId != Networking::InvalidUserId && - st->disconnectingUserId != targetuserId) - pImpl->sendTarget->Send(targetuserId, - reinterpret_cast(data), - size, reliable); + st->disconnectingUserId != targetuserId) { + pImpl->sendTarget->Send( + targetuserId, + reinterpret_cast(stream.GetData()), + stream.GetNumberOfBytesUsed(), reliable); + } }; FormCallbacks::SendToUserDeferredFn sendToUserDeferred = [this, st](MpActor* actor, const void* data, size_t size, bool reliable, - int deferredChannelId) { + int deferredChannelId, bool overwritePreviousChannelMessages) { if (deferredChannelId < 0 || deferredChannelId >= 100) { return spdlog::error( "sendToUserDeferred - invalid deferredChannelId {}", @@ -497,7 +516,12 @@ FormCallbacks PartOne::CreateFormCallbacks() userInfo->deferredChannels.resize(deferredChannelId + 1); } - userInfo->deferredChannels[deferredChannelId] = deferredMessage; + if (overwritePreviousChannelMessages) { + userInfo->deferredChannels[deferredChannelId] = { deferredMessage }; + } else { + userInfo->deferredChannels[deferredChannelId].push_back( + deferredMessage); + } }; return { subscribe, unsubscribe, sendToUser, sendToUserDeferred }; @@ -784,25 +808,23 @@ void PartOne::TickDeferredMessages() if (!userInfo) { continue; } - for (auto& deferredMessage : userInfo->deferredChannels) { - if (deferredMessage != std::nullopt) { + for (auto& channel : userInfo->deferredChannels) { + for (auto& message : channel) { auto actor = serverState.ActorByUser(userId); if (!actor) { continue; } - if (deferredMessage->actorIdExpected != actor->GetFormId()) { + if (message.actorIdExpected != actor->GetFormId()) { continue; } - pImpl->sendTarget->Send(userId, - reinterpret_cast( - deferredMessage->packetData.data()), - deferredMessage->packetData.size(), - deferredMessage->packetReliable); - - deferredMessage = std::nullopt; + pImpl->sendTarget->Send( + userId, + reinterpret_cast(message.packetData.data()), + message.packetData.size(), message.packetReliable); } + channel.clear(); } } } diff --git a/skymp5-server/cpp/server_guest_lib/ServerState.h b/skymp5-server/cpp/server_guest_lib/ServerState.h index d52223087d..1bcff02932 100644 --- a/skymp5-server/cpp/server_guest_lib/ServerState.h +++ b/skymp5-server/cpp/server_guest_lib/ServerState.h @@ -48,7 +48,7 @@ struct UserInfo std::optional> packetHistoryStartTime; - std::vector> deferredChannels; + std::vector> deferredChannels; }; class ServerState diff --git a/skymp5-server/cpp/server_guest_lib/SpSnippet.cpp b/skymp5-server/cpp/server_guest_lib/SpSnippet.cpp index 30461b26d6..165c821713 100644 --- a/skymp5-server/cpp/server_guest_lib/SpSnippet.cpp +++ b/skymp5-server/cpp/server_guest_lib/SpSnippet.cpp @@ -25,7 +25,10 @@ Viet::Promise SpSnippet::Execute(MpActor* actor) Networking::Format( [&](Networking::PacketData data, size_t len) { - actor->SendToUser(data, len, true); + // The only reason for deferred here is that it still supports raw binary data send + // TODO: change to SendToUser + constexpr int kChannelSpSnippet = 1; + actor->SendToUserDeferred(data, len, true, kChannelSpSnippet, false); }, R"({"type": "spSnippet", "class": "%s", "function": "%s", "arguments": %s, "selfId": %u, "snippetIdx": %u})", cl, func, args, targetSelfId, snippetIdx); diff --git a/skymp5-server/cpp/server_guest_lib/WorldState.cpp b/skymp5-server/cpp/server_guest_lib/WorldState.cpp index e740907b11..fe6605caab 100644 --- a/skymp5-server/cpp/server_guest_lib/WorldState.cpp +++ b/skymp5-server/cpp/server_guest_lib/WorldState.cpp @@ -270,16 +270,30 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, return false; } + bool startsDead = false; + if (isNpc) { + auto* achr = reinterpret_cast(record); + startsDead = achr->StartsDead(); + if (startsDead) { + return false; // TODO: Load dead references + } + } + // TODO: Load disabled references enum { - InitiallyDisabled = 0x800 + InitiallyDisabled = 0x800, + DeletedRecord = 0x20 }; if (refr->GetFlags() & InitiallyDisabled) { return false; } + if (refr->GetFlags() & DeletedRecord) { + return false; + } + if (!npcEnabled && isNpc) { return false; } @@ -504,12 +518,36 @@ void WorldState::SendPapyrusEvent(MpForm* form, const char* eventName, const VarValue* arguments, size_t argumentsCount) { + std::vector args = { arguments, arguments + argumentsCount }; + + if (spdlog::should_log(spdlog::level::trace)) { + std::vector argsStrings(args.size()); + for (size_t i = 0; i < args.size(); ++i) { + argsStrings[i] = args[i].ToString(); + } + + if (!strcmp(eventName, "OnTrigger")) { + static std::once_flag g_once; + std::call_once(g_once, [&] { + spdlog::trace("WorldState::SendPapyrusEvent {:x} - {} [{}]", + form->GetFormId(), eventName, + fmt::join(argsStrings, ", ")); + spdlog::trace("WorldState::SendPapyrusEvent {:x} - Muting {} globally " + "to keep logs clear", + form->GetFormId(), eventName); + }); + } else { + spdlog::trace("WorldState::SendPapyrusEvent {:x} - {} [{}]", + form->GetFormId(), eventName, + fmt::join(argsStrings, ", ")); + } + } + VirtualMachine::OnEnter onEnter = [&](const StackIdHolder& holder) { pImpl->policy->BeforeSendPapyrusEvent(form, eventName, arguments, argumentsCount, holder.GetStackId()); }; auto& vm = GetPapyrusVm(); - std::vector args = { arguments, arguments + argumentsCount }; return vm.SendEvent(form->ToGameObject(), eventName, args, onEnter); } @@ -813,6 +851,11 @@ void WorldState::SetForbiddenRelootTypes(const std::set& types) pImpl->forbiddenRelootTypes = types; } +void WorldState::SetEnableConsoleCommandsForAllSetting(bool enable) +{ + enableConsoleCommandsForAll = enable; +} + bool WorldState::IsRelootForbidden(std::string type) const noexcept { return pImpl->forbiddenRelootTypes.find(type) != diff --git a/skymp5-server/cpp/server_guest_lib/WorldState.h b/skymp5-server/cpp/server_guest_lib/WorldState.h index cdfa791ce9..7f877a30e0 100644 --- a/skymp5-server/cpp/server_guest_lib/WorldState.h +++ b/skymp5-server/cpp/server_guest_lib/WorldState.h @@ -22,6 +22,7 @@ #include #include #include +#include #ifdef AddForm # undef AddForm @@ -199,11 +200,14 @@ class WorldState std::chrono::system_clock::duration dur); std::optional GetRelootTime( std::string recordType) const; + // Only for tests auto& GetGrids() { return grids; } + void SetNpcSettings( std::unordered_map&& settings); void SetForbiddenRelootTypes(const std::set& types); + void SetEnableConsoleCommandsForAllSetting(bool enable); public: std::vector espmFiles; @@ -212,7 +216,7 @@ class WorldState actorIdByPrivateIndexedProperty; std::shared_ptr logger; std::vector> listeners; - std::map hosters; + std::unordered_map hosters; std::vector> lastMovUpdateByIdx; @@ -221,6 +225,7 @@ class WorldState bool npcEnabled = false; std::unordered_map npcSettings; NpcSettingsEntry defaultSetting; + bool enableConsoleCommandsForAll = false; private: bool AttachEspmRecord(const espm::CombineBrowser& br, diff --git a/unit/ChangeValuesTest.cpp b/unit/ChangeValuesTest.cpp index c3ed9b1945..77af1b9f46 100644 --- a/unit/ChangeValuesTest.cpp +++ b/unit/ChangeValuesTest.cpp @@ -6,51 +6,13 @@ #include "PacketParser.h" #include "libespm/Loader.h" +#include "ChangeValuesMessage.h" + PartOne& GetPartOne(); extern espm::Loader l; using namespace std::chrono_literals; -TEST_CASE("ChangeValues packet is parsed correctly", "[ChangeValues]") -{ - class MyActionListener : public ActionListener - { - public: - MyActionListener() - : ActionListener(GetPartOne()) - { - } - - void OnChangeValues(const RawMessageData& rawMsgData_, - const ActorValues& actorValues_) override - { - rawMsgData = rawMsgData_; - actorValues = actorValues_; - } - - RawMessageData rawMsgData; - ActorValues actorValues; - }; - - nlohmann::json j{ - { "t", MsgType::ChangeValues }, - { "data", { { "health", 0.5 }, { "magicka", 0.3 }, { "stamina", 0 } } } - }; - - auto msg = MakeMessage(j); - - MyActionListener listener; - - PacketParser p; - p.TransformPacketIntoAction( - 122, reinterpret_cast(msg.data()), msg.size(), - listener); - - REQUIRE(listener.actorValues.healthPercentage == 0.5f); - REQUIRE(listener.actorValues.magickaPercentage == 0.3f); - REQUIRE(listener.actorValues.staminaPercentage == 0.0f); -} - TEST_CASE("Player attribute percentages are changing correctly", "[ChangeValues] ") { @@ -142,16 +104,13 @@ TEST_CASE("ChangeValues message is being delivered to client", auto& ac = partOne.worldState.GetFormAt(0xff000000); partOne.Messages().clear(); - nlohmann::json j = nlohmann::json{ { "t", MsgType::ChangeValues }, - { "data", - { - { "health", 1.0f }, - { "magicka", 1.0f }, - { "stamina", 1.0f }, - } } }; - std::string s = MakeMessage(j); + ChangeValuesMessage msg; + msg.idx = ac.GetIdx(); + msg.health = 1.f; + msg.magicka = 1.f; + msg.stamina = 1.f; - ac.SendToUser(s.data(), s.size(), true); + ac.SendToUser(msg, true); REQUIRE(partOne.Messages().size() == 1); nlohmann::json message = partOne.Messages()[0].j; diff --git a/unit/ConsoleCommandTest.cpp b/unit/ConsoleCommandTest.cpp index 6a6e763c02..1d9da71762 100644 --- a/unit/ConsoleCommandTest.cpp +++ b/unit/ConsoleCommandTest.cpp @@ -92,18 +92,20 @@ TEST_CASE("AddItem executes", "[ConsoleCommand][espm]") p.GetActionListener().OnConsoleCommand(msgData, "additem", { 0x14, 0x12eb7, 0x108 }); - p.Tick(); // send deferred inventory update messages + p.Tick(); // send deferred messages nlohmann::json expectedInv{ { "entries", { { { "baseId", 0x12eb7 }, { "count", 0x108 } } } } }; REQUIRE(p.Messages().size() == 2); REQUIRE( - p.Messages()[0].j.dump() == - R"({"arguments":[{"formId":77495,"type":"weapon"},264,false],"class":"SkympHacks","function":"AddItem","selfId":0,"snippetIdx":0,"type":"spSnippet"})"); + p.Messages()[0].j == + nlohmann::json::parse( + R"({"inventory":{"entries":[{"baseId":77495,"count":264}]},"type":"setInventory"})")); REQUIRE( - p.Messages()[1].j.dump() == - R"({"inventory":{"entries":[{"baseId":77495,"count":264}]},"type":"setInventory"})"); + p.Messages()[1].j == + nlohmann::json::parse( + R"({"arguments":[{"formId":77495,"type":"weapon"},264,false],"class":"SkympHacks","function":"AddItem","selfId":0,"snippetIdx":0,"type":"spSnippet"})")); p.DestroyActor(0xff000000); DoDisconnect(p, 0); diff --git a/unit/DistContentsTest.cpp b/unit/DistContentsTest.cpp index e8e447965b..10902de1e7 100644 --- a/unit/DistContentsTest.cpp +++ b/unit/DistContentsTest.cpp @@ -1,5 +1,6 @@ #include "TestUtils.hpp" #include +#include namespace { size_t PrintMissing(const std::set& whatIsMissing, @@ -71,6 +72,11 @@ auto GetExpectedPaths(const nlohmann::json& j) TEST_CASE("Distribution folder must contain all requested files", "[DistContents]") { + auto ci = getenv("CI"); + if (!ci || strcmp(ci, "true") != 0) { + return spdlog::info("Skipping DistContentsTest - CI env not detected"); + } + auto distDir = std::filesystem::path(DIST_DIR); auto begin = std::filesystem::recursive_directory_iterator(distDir); auto end = std::filesystem::recursive_directory_iterator(); diff --git a/unit/EspmTest.cpp b/unit/EspmTest.cpp index d34010b9c0..2099f4101a 100644 --- a/unit/EspmTest.cpp +++ b/unit/EspmTest.cpp @@ -10,6 +10,11 @@ extern espm::Loader l; TEST_CASE("Hash check", "[espm]") { + auto ci = getenv("CI"); + if (!ci || strcmp(ci, "true") != 0) { + return spdlog::info("Skipping EspmTest Hash check - CI env not detected"); + } + const auto hashes = l.GetFilesInfo(); for (const auto& [filename, info] : hashes) { DYNAMIC_SECTION(filename << " checksum and size test") diff --git a/unit/PapyrusDebugTest.cpp b/unit/PapyrusDebugTest.cpp index ab713b7e58..b5bbe67b11 100644 --- a/unit/PapyrusDebugTest.cpp +++ b/unit/PapyrusDebugTest.cpp @@ -26,6 +26,8 @@ TEST_CASE("Notification", "[Papyrus][Debug]") debug.Notification(VarValue::AttachTestStackId(), { VarValue("Hello, \"world!\"") }); + p.Tick(); // Tick deferred messages + REQUIRE(p.Messages().size() == 3); REQUIRE(p.Messages()[1].userId == 3); REQUIRE(p.Messages()[1].reliable); diff --git a/unit/PapyrusObjectReferenceTest.cpp b/unit/PapyrusObjectReferenceTest.cpp index 3f1e5697a7..5e5959cd15 100644 --- a/unit/PapyrusObjectReferenceTest.cpp +++ b/unit/PapyrusObjectReferenceTest.cpp @@ -191,7 +191,7 @@ TEST_CASE("MoveTo", "[Papyrus][ObjectReference]") { auto it = std::find_if( messages.begin(), messages.end(), - [](PartOne::Message& msg) { return msg.j["type"] == "teleport"; }); + [](PartOne::Message& msg) { return msg.j["t"] == MsgType::Teleport; }); REQUIRE(it != messages.end()); } } diff --git a/unit/PartOne_ActivateTest.cpp b/unit/PartOne_ActivateTest.cpp index 8c68ebc007..f0d4791915 100644 --- a/unit/PartOne_ActivateTest.cpp +++ b/unit/PartOne_ActivateTest.cpp @@ -251,7 +251,7 @@ TEST_CASE("Activate WRDoorMainGate01 in Whiterun", "[PartOne][espm]") REQUIRE(partOne.Messages()[0].j["t"] == MsgType::UpdateProperty); REQUIRE(partOne.Messages().size() >= 2); - REQUIRE(partOne.Messages()[1].j["type"] == "teleport"); + REQUIRE(partOne.Messages()[1].j["t"] == MsgType::Teleport); REQUIRE(partOne.Messages()[1].j["pos"].dump() == nlohmann::json{ 19243.53515625, -7427.3427734375, -3595.4052734375 } .dump()); @@ -387,7 +387,7 @@ TEST_CASE("BarrelFood01 PutItem/TakeItem", "[PartOne][espm]") REQUIRE(partOne.Messages()[1].j["propName"] == "inventory"); REQUIRE(partOne.Messages()[1].j["idx"] == ref.GetIdx()); REQUIRE(partOne.Messages().size() == 3); - REQUIRE(partOne.Messages()[2].j["type"] == "openContainer"); + REQUIRE(partOne.Messages()[2].j["t"] == MsgType::OpenContainer); REQUIRE(partOne.Messages()[2].j["target"] == ref.GetFormId()); REQUIRE_THROWS_WITH( diff --git a/unit/PartOne_MovementTest.cpp b/unit/PartOne_MovementTest.cpp index 71d6c38fe9..0b8de89494 100644 --- a/unit/PartOne_MovementTest.cpp +++ b/unit/PartOne_MovementTest.cpp @@ -71,7 +71,7 @@ TEST_CASE("UpdateMovement", "[PartOne]") partOne.Messages().clear(); doMovement(); REQUIRE(partOne.Messages().size() == 1); - REQUIRE(partOne.Messages().at(0).j.dump() == jMovement.dump()); + REQUIRE(partOne.Messages().at(0).j == jMovement); // UpdateMovement actually changes position and rotation REQUIRE( @@ -113,8 +113,8 @@ TEST_CASE("UpdateMovement", "[PartOne]") partOne.Messages().clear(); doMovement(); REQUIRE(partOne.Messages().size() == 2); - REQUIRE(partOne.Messages().at(0).j.dump() == jMovement.dump()); - REQUIRE(partOne.Messages().at(1).j.dump() == jMovement.dump()); + REQUIRE(partOne.Messages().at(0).j == jMovement); + REQUIRE(partOne.Messages().at(1).j == jMovement); // Another player is being moved away and now doesn't see our movement auto acAbcd = dynamic_cast( diff --git a/unit/RespawnTest.cpp b/unit/RespawnTest.cpp index e607beb68b..61f44acd18 100644 --- a/unit/RespawnTest.cpp +++ b/unit/RespawnTest.cpp @@ -61,7 +61,7 @@ TEST_CASE("DeathState packed is correct if actor is respawning", "[Respawn]") REQUIRE(updateProperyMsg["data"] == false); REQUIRE(updateProperyMsg["idx"] == ac.GetIdx()); - REQUIRE(teleportMsg["type"] == "teleport"); + REQUIRE(teleportMsg["t"] == MsgType::Teleport); REQUIRE(changeValuesMsg["t"] == MsgType::ChangeValues); REQUIRE(ac.IsDead() == false);