Repository navigation
Conversation
PartyService::OnPartyKick called BroadcastPlayerList(pKick). That argument is meant for a player who is leaving the server, so every client dropped the kicked (still connected) player from NotifyPlayerList and the kicked player got no list at all. The kick now broadcasts the full list. The client's PartyService also keeps the last name seen per player id for the session (GetKnownName), so a player who drops out of the list is still shown by name. It is cleared on disconnect. Verified: - Source: the server kick path and BroadcastPlayerList's ignore argument (also used by OnPlayerLeave, which is unchanged). - SkyrimTogetherServer and SkyrimTogetherClient build (releasedbg, MSVC). - Played on the fork's 1.8-playtest builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit d2f0e89)
Name tags (ImGui, NameTagService):
- Plain name text above other players, no level or id. The party leader gets
the party widget's crown (crown.svg, drawn as convex pieces). An optional
thin "Name backdrop" (On/Party/Off, default Party).
- Anchored on the neck bone ("NPC Neck [Neck]") plus the head height, so tags
stay centred in profile, crouching and lying down, and stay on the body if a
dismemberment mod removes the head. Helmet clearance of 16 world units,
clamped to 14-56 px; text scale clamp(800 / distance, 0.65, 1.2).
- Fade start / invisible distances for names and for party (3000 / 5000).
- A tag hides while the crosshair targets that player, so the HUD's activate
prompt stays readable.
- Hidden while full-screen menus are open; dead players only up close.
- Font: the overlay's Futura Condensed (Latin, Cyrillic).
Player UI settings (Angular, stored per client):
- Known names: All / Party / None. Unknown names show as an italic "Stranger"
in tags, the player list, the party HUD, the party menu and party invites.
An eye in the player list overrides it per username.
- Party invitations (on/off), Preview mini chat (On/Party/Off), Name tags
(On/Party/Off, default Party), name tag size.
- Locales: English and Swedish; other languages fall back to English.
Verified:
- Ghidra (SkyrimSE 1.6.1170):
- NiAVObject world translate at +0xA0 (decapitation code 0x140685200 walks
parent +0x30, local transform +0x48, world +0x7C, reads +0xA0/A4/A8).
- NiAVObject::GetByName is vtable slot 0x2A (+0x150), called with
FixedStrings entries (0x1406b5c10); "NPC Neck [Neck]" is FixedStrings
+0x238.
- CrosshairPickData (ID 401585 -> 0x143138ed0): the activate code
0x14073e980 reads the target handle at +0x4.
- World-to-screen (ID 70640, 0x140d2c780) only rejects |w| <= tolerance,
so the z <= epsilon check is what drops points behind the camera.
- SkyrimTogetherClient, SkyrimTogetherServer and the Angular production build
succeed. Played on the fork's 1.8-playtest builds.
Assumed:
- Neck-to-head-top (24 units) and helmet clearance (16 units) are tuned by
eye in game, not measured.
- Actor nodes and camera globals are read from the ImGui draw (Present path),
as the original v1.8.1 name tags did; the thread was not verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f618aef)
Deduplicate effective UI settings including revealed player IDs, remove unconditional CEF event logs, and avoid replacing unchanged game-thread settings. (cherry picked from commit ea837ff)
Depends on tiltedphoques#931 (player name tags with privacy settings): this commit builds on its NameTagService, PlayerUiService and settings, and is stacked on that branch. Merge tiltedphoques#931 first. Floating subtitle chat (On/Party/Off, default Party): - Party: party members' chat and dialogue choices float above them. On: also strangers' chat, never their dialogue choices (dialogue is only synced within the party). - The line replaces the player's name tag while it shows: same anchor, helmet gap and distance scale. It starts with "Name:" in the F2 chat's colour for its type (stranger names italic), wraps at 300 px (22 px base) to two lines with "...", and shows for 2 s + 60 ms per character, at most 8 s. A newer line replaces the older one. Lines show even where the Name tags filter hides the tag. The layout is cached per line and only rebuilt when the size (half pixels) or the stranger label changes. - Drawn by Skyrim Together itself; no SKSE plugin is needed. - NotifyChatMessageBroadcast gains PlayerId (the sender; 0 for server and script messages), so clients can find the sender's character. The server sets it in sendPlayerMessage. Known chat names (On/Party/Off, default On, the same as today): - Chat sender names in the F2 chat and the mini chat follow it, and so does the name in front of a floating line. The eye in the player list overrides it per name. The name is decided when the message arrives. Locales: English; Swedish keys are present but empty (fall back to English). Verified: - SkyrimTogetherClient, SkyrimTogetherServer, TPTests and the Angular production build succeed; TPTests include a NotifyChatMessageBroadcast round trip with PlayerId. - Reviewed with a high-effort code review; played on the fork's 1.8-playtest builds. Assumed / known limits: - Floating lines are written from dispatcher events and read in the ImGui draw without a lock, like the name tag visibility data; this assumes the draw runs on the main thread. - The UI treats a chat sender missing from the player list as the local player, so a player whose list entry hasn't arrived yet (just joined) is shown by name in chat. - Chat names are matched by username; players sharing a name share the setting, as with the eye. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 7a3319b)
Translations by the user (KNOWN_CHAT_NAMES, KNOWN_CHAT_NAMES_HINT, FLOATING_CHAT, FLOATING_CHAT_HINT). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit a338491)
Players can invite each other from the player list and trade items through a trade window docked beside the chat. Both sides build an offer, ready up, and the server swaps the items once both are ready and the offers still validate. Server (TradeService): - Invites, responses, offers, ready state, cancel reasons and completion. - Validates every offer against the player's synced inventory (quest items, worn items, temporary forms and internal items are refused; quantities are reserved across the whole offer) and publishes neither inventory until both offers and the resulting stacks are valid. - Drops malformed trade packets instead of crashing; large inventories are no longer sent as truncated messages. Client (TradeService, engine helpers): - Trades from the player's live inventory and removes the exact traded item. - Recreates crafted potions and poisons (AddPotion / AddPoison) and weapons coated with crafted poisons; keeps resolvable effects of synced enchantments; never frees or fabricates forms while applying a trade. - Each recipient only accepts the partner items it can resolve. - Non-playable or nameless items (creature skins, transformation gear, bound weapons, traps, dummies) are kept out of trades, using the engine's IsPlayable (TESForm vtable slot 0x19). - Trade partners are shown by their last known name, never by id. UI: - Trade window with search, Skyrim item categories, quantity controls, offer and partner offer, ready/countdown, and item details (enchantments, charge, poison, soul level, refinement, custom names). - Trade invite popups and notifications. An optional TRADE_INVITE_POLICY hook lets another feature decide accepted invites and shown names. - Locales: English and Swedish. Tests: Code/tests/trading.cpp (validation, offers, encoding) and Code/skyrim_ui/test/trade-ui.test.cjs (npm run test:trading). This squashes the history of tiltedphoques#918 onto the current dev. Verified: - Ghidra (SkyrimSE 1.6.1170): TESForm slot 0x19 is IsPlayable for every carryable item type (ARMO/MISC/BOOK/ALCH/INGR/KEYM/SLGM/SCRL/LIGH -> 0x140141260 checks form flag 0x4; WEAP 0x140289040 and AMMO 0x140263d70 check their own non-playable flags). - SkyrimTogetherClient, SkyrimTogetherServer, TPTests and the Angular production build succeed; trade tests pass. Played on the fork's 1.8-playtest builds. Assumed: - The server can't check item playability (no form data); like the other client-side trade rules it relies on the offering client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit cd40638)
Depends on tiltedphoques#931 (player name tags with privacy settings): it uses tiltedphoques#931's PlayerUiService, Known names and Player UI settings, and this branch is stacked on tiltedphoques#931. Merge tiltedphoques#931 first. - PlayerUiService provides TRADE_INVITE_POLICY: - displayName: trade popups and the trade window show "Stranger" for players whose name isn't known, the same rule as the player list (Known names plus the eye). - acceptsTradeInvite: the new "Trade invitations" setting (On/Party/Off, default On). Party: only party members can invite you; other invites are declined silently. - Settings row under Player UI filters; English and Swedish strings. Tests: the policy paths are covered by trade-ui.test.cjs (refused invites are declined without a popup, popups use the policy's name, no policy shows real names). Verified: - Angular production build; trade UI tests pass. Played on the fork's 1.8-playtest builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit ddb1c96)
… is handled HookActivate does not sync an activation right away: it queues an ActivateEvent through RunnerService::Trigger, and ObjectService::OnActivate handles it during the next World::Update. The event held only raw pointers to the activated object and to the activator. When the game deleted the reference in between, for example an item an NPC picked up (activating it moves it into the inventory and deletes the reference), OnActivate read the freed object: its lock, its form ID, and then GetParentCellEx, which falls back to the virtual GetSaveParentCell when parentCell is null and called through a vtable pointer that now pointed into the heap. Crash log (Skyrim 1.6.1170, a playtest build of the absol89 fork, in Fort Dunstad with bandits and a skeleton fighting nearby): https://pastebin.com/enXSqvPf EXCEPTION_ACCESS_VIOLATION, call [rax+0x4B8] with rax pointing into the heap TESObjectREFR::GetParentCellEx ObjectService::OnActivate RunnerService::Trigger<ActivateEvent> (the queued call) TiltedPhoques::TaskQueue::Drain World::Update HookVMUpdate The event now also records the form IDs of the object and the activator. OnActivate looks both up again with TESForm::GetById and handles the event only if they still name the same objects; otherwise it skips it with a debug log line. The pointers are no longer read before that check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 190394a)
Actor::KillImpl (ID 37896) adds the base's death items to every dying actor except reanimated corpses, so a summoned flame atronach still gets its fire salts. The summon vanishes right after, but the item add went through the inventory hook and was synced to the server and every other player. AddDeathItems now skips commanded actors, as it already skips remote ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 4586480)
Actor::SetMagicEquipment equipped whatever TESForm::GetById returned for the owner's hand spells and shout. When the owner has magic from a mod this client doesn't have, GetGameId returns 0 and a null form gets equipped. Combat AI later walks the actor's spells (Actor::VisitSpells) and reads the form type through that null pointer, crashing a BSJobs thread with an access violation at SkyrimSE+0x811CF7 (ID 44882) when the remote character fights nearby. Seen with a Khajiit player using Moonlight Tales, which the other client didn't have: crashes began once he reverted from werewolf form and happened whenever he loaded near the Northwatch guards. Skip and warn about hand spells that aren't MagicItems and voice-slot forms that are neither shouts nor powers, as the equip-change path in InventoryService already does for unknown items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Mdy6FmrJTc1m7KuVM1n3H (cherry picked from commit 8199d24)
HookLaunch asserted that the engine's launch returned a resolvable projectile and then read its power anyway. When the handle didn't resolve, the game crashed on a null pointer, or froze on the assert's breakpoint with a debugger attached. Such launches are now left unsynced with a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 2fcf2c4)
… cell Loading a cell queues its references' saved changes on BGSSaveLoadGame without a lock, while the background QueuedPromoteReferencesTasks drain the same queue. MoveActor (and the teleport command and map overlay) loaded cells without the pause the game uses for its own cell loads, so a drain could run concurrently or later on freed references (crash at SkyrimSE 35567+0x111, call [rdx+0x160]). Pause the promote tasks around the load and apply the queued changes before resuming, as the game does. Verified in Ghidra (SkyrimSE 1.6.1170): - 403330 is BGSSaveLoadGame (0x348 bytes, flags at +0x340). +0x318 is a BSTArray of 0x28-byte BGSLoadFormData. Each entry holds a raw form pointer at +0x10 with no refcount. - 35567 drains the queue (InitLoadGame/FinishLoadGame) under its own spinlock and sets flags |= 8 while it runs. 35569 is the only push and does not take that lock. Nothing removes an entry when its form is freed. - 20460 TESWorldSpace::LoadCell -> TESObjectCELL::InitItemImpl (18881/18882) -> 35569 pushes and does not drain. - 13333/13334 pause/resume the promote tasks (15335/15336). Pause cancels queued tasks and waits for running ones. Vanilla cell loads (coc/cow 22875/22876, 36641, 40437, 40745, grid update 13288) hold it. Both functions ignore `this`. - The 2026-10-21 crash was on a QueuedPromoteLocationReferencesTask thread, 17 ms after a burst of NotifyActorTeleport -> MoveActor. The 2026-10-18 crash was the main-thread grid drain. Verified on 1.7.104 by comparing raw bytes of the unpacked exes (equal apart from rel32 displacements): the BGSSaveLoadGame ctor/size/+0x340 flags, 35567's flag updates and +0x318/+0x328 accesses, 35569's prologue and kLoading test, and 13333/13334 resolving to 15335/15336 on 400326. Assumed: draining right after loading an unattached cell is safe, as vanilla 15328 does the same from a worker thread. On 1.7.104, 20460 still reaching InitItemImpl was not checked. Not built or tested in game yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 58d4d31)
…r mods Behavior variables are synced by slot position, but the descriptor built for Nemesis/Pandora-patched behaviors sorted its slots by the local graph index, while the vanilla descriptor uses its own list order. Between a vanilla and a patched client almost every slot landed in another variable, e.g. the owner's currentDefaultState in the viewer's iIsInSneak, so NPCs and remote players looked sneaking. Modded descriptors now keep the vanilla slot order, then the replacer extras sorted by name. Variables a graph lacks keep their slot as an out-of-range sentinel that saving and loading skip. Variable diffs against a different-sized previous now compare with zero, as the receiver does, so those empty slots cost a bit each instead of a full value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit b2d5650)
…hain Playtest only, not for upstream TiltedEvolution: the proper fix is a pull request to powerof3/FloatingSubtitles (copy the hooked prologue with relocation instead of verbatim). This branch keeps the fork's playtest builds compatible with the current Floating Subtitles release until that lands. Nothing else in the fork depends on the plugin being installed. ST hooked SubtitleManager::ShowSubtitle (ID 52626) with TP_HOOK. That hook is committed by TP_HOOK_COMMIT in main.cpp, before LoadScriptExtender(), so ST's MinHook jump was already in the prologue when SKSE plugins loaded. po3's Floating Subtitles hooks the same prologue with stl::hook_function_prologue<ShowSubtitle, 5>, which copies the first 5 bytes verbatim into its trampoline. It copied ST's relative jmp, which lands at the wrong address when run from the trampoline. The hook is now installed with TP_HOOK_IMMEDIATE on the first TiltedOnlineApp::Update, i.e. after the plugins have hooked. MinHook relocates a leading rel32 jmp, so the chain becomes ST -> plugin -> game. RealShowSubtitle is still set at startup, so ST's own ShowSubtitle calls for remote subtitles work before the first frame, and afterwards they pass through the plugin's hook too. Verified: - Ghidra (SkyrimSE 1.6.1170): ShowSubtitle at 0x14096cb00 (1.7.104: 0x140983e80) begins 4c 8b dc 55 56 (mov r11,rsp; push rbp; push rsi), the 5 bytes po3 expects to copy. - Ghidra: po3's other hooks don't overlap ST's. The call at 77226+0x275 (0x140e42655) and the call at 52629+0x37 (0x14096d067) are real e8 calls; ST only MinHooks 77226's prologue and patches +0x175. - Source: po3 FloatingSubtitles @ d966aef (src/PCH.h, src/Hooks.cpp, src/ImGui/Renderer.cpp); ST main.cpp hook/SKSE order; TiltedReverse FunctionHook TP_HOOK vs TP_HOOK_IMMEDIATE. - SkyrimTogetherClient builds (releasedbg, MSVC). Assumed / not tested: - The first TiltedOnlineApp::Update runs after SKSE has sent kPostLoad (po3 installs its subtitle hooks at kPostLoad). - Not tested in game, with or without Floating Subtitles installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 4f2b3e7) (cherry picked from commit f32927d)
…ltedphoques#810) After the party leader leaves a cell, NPCs handed to another player could stand still and never draw their weapon, spamming Unequip, until the original owner came back. Actor::SetWeaponDrawnEx flips the internal weapon state when the replica already reports the requested state so that the native call forces the animation again; for a sheathed replica asked to sheathe, the behavior graph refuses the forced Unequip and the actor stays in "wants to sheathe" forever, so its AI can neither draw nor attack once it gains ownership. Keep the flip and the forced call, but record the native result of the forced action in HookPerformAction and, when the state had been flipped and the action was refused or never issued, restore the requested stable state. The flip is kept because a replica can carry the owner's weapon state from replayed actions while its graph never received WeapEquip; there the forced action is accepted and corrects the visible weapon. Client targets build; TPTests pass (27 assertions in 5 test cases). Verified in game on a v1.8.1 tracing build carrying this same change: the Embershard bandit draws and attacks the player who received it while the leader is inside the mine, with roles inverted, and after repeated hand-overs; players' draw/sheathe sync, remote NPCs appearing with weapons drawn and neutral NPCs unchanged. (cherry picked from commit c72b1df)
Implements tiltedphoques#342: a server can now be configured in one docker command without editing STServer.ini first. A new EnvSettingsProvider maps each setting to the upper-cased key name from the issue's examples (UDIFFICULTY, BENABLEMODCHECK, SPASSWORD) and the dedicated runner applies the environment after the ini file and the command line, so environment values win. Includes unit tests for the name mapping and the overrides. (cherry picked from commit 1f75853)
Squashed from tiltedphoques#879 (absol89/TiltedEvolutionScriptFixes PR-branch, DJLegends1011/feat/mod-anim-event-sync): syncs animation events sent by mods through the console `sae` command (including ConsoleUtil callers) and papyrus Debug.SendAnimationEvent, translating targets to server ids, and adds /wave, which plays the vanilla wave when weapons are sheathed. Client and server must match: the server relays the new script animation messages. Upstream PR head: bb65834
Ownership transfers only reach players the server considers in range, so a client that still had the actor loaded could keep the old epoch and drop every later inventory, equipment, actor value and death update, while the leader's claims kept being rejected. Remotes now adopt a newer epoch since the server only relays the current owner, a repeated spawn catches the actor up, and a stale leader claim is answered with the current owner so the leader can claim again. (cherry picked from commit 2e74896)
The hooks can't send inventory changes before a `LocalComponent` exists, so anything equipped between discovery and the assignment reply, such as the outfit as the actor loads, never reached the server. Transfers and remote spawns apply the server's inventory, so those NPCs ended up naked for everyone. The new owner now sends its whole inventory through `RequestInventorySnapshot` when it differs, and the server relays it to the other clients in range. (cherry picked from commit e44ab60)
The spawn inventory is applied once the 3D loads and overwrote any changes relayed before that. Inventory changes now go into the spawn snapshot, and equipment changes are queued in `WaitingFor3D` and replayed after it is applied. A newer full inventory drops the queue. (cherry picked from commit 121c25b)
A leader could decline a grant and then accept the assignment reply for the same epoch, so it kept streaming an actor the server had already removed while the next client to load it became a second owner. `NotifyRemoveCharacter` now also drops a local claim, and an actor that is still loaded asks for a fresh assignment instead of running unsynced. (cherry picked from commit b1f7040)
Nearby NPCs are updated in parallel engine jobs, so AI equips such as torches reach the hooks on worker threads, where the ownership lookup raced with the main thread adding and removing Local/Remote components. The hooks now only queue the event, and the owner and epoch are looked up when the runner hands it to the main thread. (cherry picked from commit 21d85fa)
The epoch rules were spread across `Utils`, `InventoryService`, `ActorValueService` and `CharacterService`. They now live in one system: `AcceptRemoteUpdate` and `AcceptOwnerUpdate` for relayed updates, `GetOutgoingOwnership` for local changes, and `GetOwnershipEpoch` for the transfer check. No behaviour change. (cherry picked from commit ea11e63)
Keep one door/container binding per local reference and update reassigned server IDs. Notify clients before server objects are retired, preserve entity generation bits, and clear object bindings on disconnect. Add regression coverage for client bindings, message encoding, and server cell retirement. Quest progression, scenes, and dialogue are unchanged. (cherry picked from commit 9b19246)
) While connected, the client locks the game clock and CalendarService writes the server's time every frame, so sleeping or waiting did nothing: the sleep/wait menu advanced the clock and the next frame overwrote it. A party had to disconnect or use /settime to skip the night. The client now listens to the engine's sleep and wait start/stop events, records the requested hours (the events carry start and desired end times in game days) and, when the sleep or wait ends uninterrupted while online, sends RequestTimeSkip. The server applies the same permission rule as /settime (admins always, the party leader on private servers) and CalendarService advances the shared clock with DateTime::AddHours, the day/month/year rollover that Update already did, then resends ServerTimeSettings to everyone. When a server time update jumps forward, the client adds the difference to GameDaysPassed, measured against a mirror of the server clock so that a midnight crossed on the server is not counted twice when the calendar is not synced. Client, server, runner, early-load and TPProcess build; TPTests (64/9) and TPServerTests (145/9) pass. Verified with two local clients on a build carrying this change: the leader waiting 3 h and sleeping 3, 2, 8 and 19 h moves both clients together, including day changes; a non-leader's wait is ignored; after recreating the party the new leader can skip time; /settime and cell changes behave as before. (cherry picked from commit 87b8d7e)
…nces Items the local player drops are now registered by the server instead of being re-dropped on every client. The dropper simulates the physics and streams the item's transform until it settles. Other clients spawn a keyframed copy that follows that stream, then hand it back to physics once it comes to rest. The server keeps drops for the whole session (capped by uMaxDroppedItems) and tells each client which copies to spawn or delete as items come in and out of range. The first pickup deletes every other copy. The old receiver path that called DropObject with a newly allocated ExtraDataList is removed, along with the Drop flag on inventory messages. The engine matches that list by pointer, so it never worked. Item drops are now enabled by default (bEnableItemDrops). (cherry picked from commit d7a71e7)
(cherry picked from commit 10c113c)
Derive the server's at-rest state from pSimulator, reuse UpdateInterest when an item is dropped, and walk m_knownItems once in RemoveItem. On the client, add SendPickUp and EnsureKeyframed helpers, take IsAtRest from the item in SendMove, flatten UpdateSimulatedItem, and erase tracked items with swap-and-pop. (cherry picked from commit 07cb621)
(cherry picked from commit ae7bef7)
Item drop sync (tiltedphoques#913) looks up the dropped item's base form by its server id. A player-crafted potion or poison only has a temporary id, so other players' copies failed to spawn. The drop already carries the recipe that player trading captures, so recreate the potion from it, the same way traded and synced inventory potions are created. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
The engine won't disarm essential actors and every player is essential, so the shout and the perks silently did nothing. Lift that check only for the local player being disarmed by another player. Perk disarms are sent to the victim's client since they only run on the attacker's, and the weapon is only dropped on the victim's client so it can't be duplicated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
Commanded actors aren't hostile to a remote copy, as it has no combat group of its own to mark them as enemies. Force the target while a fight between the two players lasts. Also stop a remote player's summon from hurting the local player with PvP off, which only remote players themselves were kept from doing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
Mass spells only ran on the caster's client, and hazards like the fire from wall spells skipped the local player since it isn't hostile to a remote player. Send self-cast area attacks to the other clients, and let hazards and explosions from a remote player reach the local player while PvP is on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
A cloak's copy on the victim's client can't be relied on, since a concentration cloak only lasts while it's cast on the wearer's client. Let that client decide on the hits and send them to the victim, whose own checks can still refuse them, and ignore the hits from the copy's cloak. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
Cloak damage only lands on targets hostile to the caster, and some area effects skip the player's base form outright, so hits sent to the victim were refused. Pass those two conditions for an attack from another player, as other conditions like resistances still apply, and send concentration area effects from the caster's client too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
The weapon goes through DroppedItemService here instead of the Drop flag this branch used, so other clients get the same single tracked reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
A hostile spell counts as a hit before the poison and disarm forwards return, so summons react to those too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
Each forward to the victim's client keeps its own return after the hit event. The attack check on the victim's client now covers disarms as well, and poisons still reach it because they never auto-succeed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
It's the same ice form paralysis that has been kept away from players, and its effect refuses them on purpose. Stop overriding that condition for paralysis and stop sending the channeled kind to the victim. The cloak's frost damage and Mass Paralysis, which has no such condition, still land. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
* Make the F2 UI usable on ultrawide screens TiltedUI's D3D11 overlay handler copied and uploaded the whole CEF view on every paint, through a deferred context. At 5120x1440 that is about 30 MB per paint, and paints pile up in each game frame when the game runs below the UI's 60 fps, so the menu barely rendered and ignored clicks, worst outdoors. Details in tiltedphoques/TiltedUI#20 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit ed46728)
A corpse can stand because it was rebuilt for a leveled pick or got health back, and saves keep it that way. Keep those from happening to dead actors, and while connected check nearby bodies for a standing skeleton and ragdoll them, logging BodyStand lines. Also log a respawn caused by a save disagreeing with the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur (cherry picked from commit 879ac28)
RunRemoteUpdates called Kill() on every tick while the owner had an NPC dead and it was alive here. When the game refused the kill, that meant about 100 calls and log lines a second until ownership moved (1300 in 13 seconds in a playtest log). Retry once a second, give up after five tries with one warning, and start over when the body reloads or dies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit faa5fff)
Every character the engine loads is constructed with form ID 0 and gets its ID later, so the FormIdZeroActor constructor checks logged a stack trace for each NPC of every cell load (20 to 60 warnings per load). The before/after move placement lines added two more lines per assigned actor. Log those at debug and skip the stack walk when debug is off. The UpdateReference3D form ID 0 check, the move line and actor added/removed placement lines stay at their levels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The menu bar was fixed at 32vw, so labels that are one long word in some translations (Swedish "Spelarhanterare") couldn't wrap and pushed the last button, Settings, out of the bar. The controls column now grows to the menu's width (at least 32vw, at most the screen) with labels kept on one line, and the chat fills that width without adding its own, so long chat lines still wrap. While trading the column stays at 32vw because the trade dock is placed next to it, and the menu keeps wrapping its labels. Swedish "Player Manager" is now "Hantera Spelare". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit ec2b4b8c287f81ab07330596e95ff14190202fa0)
Fitting the column to its labels made it far too wide at large font sizes and squeezed the trade window. Go back to a fixed column, now 10% wider than 32vw and reaching to one gap from the trade window, with menu labels that wrap between words. Without a trade the buttons can now shrink (they are flex boxes, so they needed min-width: 0); while trading the columns size to their labels, so a short label like Trade leaves room for one long word like Inställningar. The chat keeps the bar's width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 32b8a5a39e2dc5af2ad3e99c47791578bdfb4c16)
At small window widths or large fonts a menu label word that can't be broken (Swedish "Inställningar") still ran into the ornament arrows. The menu is now one grid column per button with the arrows positioned at the button edges, so labels wrap between words, every column is at least its longest word plus padding, and the column grows past its usual width only when that minimum needs it. The chat keeps the bar's width without adding its own, and the trade dock is placed one gap right of wherever the column ends (measured by RootComponent into --app-root-controls-right). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 4001078dd8969b8aa727f8fc0ffe6282f1df18b0)
Add a non-overlapping minimize toolbar, a font-aware popup and compact inventory layout, and scrolling for short screens. Preserve the trade when minimized and update the Angular test mock. (cherry picked from commit 17ac04d8e6d53c59c863710d60bb067e2bb67bad)
…a werewolf/vampire lord (cherry picked from commit e461ce8)
Weredragon fades in its dragon skin armor with PlaySubGraphAnimation, which wasn't synced, so other players saw only the gear floating where the dragon was. Send it from the actor's owner and play it on the other players' copies, then again a moment after the copy's gear or body changes, since the skin can arrive after the event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
The game registers PlaySubGraphAnimation on Actor, not ObjectReference, so the hook never installed and SkinFadeIn was never sent. Checked in the 1.6.1170 binary: the native is at 0x1409e9760 and takes (VM*, stackId, Actor*, BSFixedString*), matching the hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
A duplicated OnEvent header broke the client build. A subgraph event for a copy that isn't loaded yet was also dropped. It's now kept and replayed once the copy loads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
- Shorten the comments and drop the mention of an unmerged feature. - Type the ExecuteCommand callback as void(const FxDelegateArgs*). - Replace the ported CommonLibSSE-NG structs with GFxValue and FxDelegateArgs in IMenu.h, laid out after powerof3's CommonLibSSE, and use them for FxDelegateHandler::CallbackFn. - Keep tcl, tfc, tgm, tm and coc (and their full names) allowed while typed commands are turned off. A reference prefix is still refused. World has no const getter, so World::Get() stays as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxR8LzniCZWnKRhyEtP
tgm makes a player invulnerable, which breaks PvP, and tcl lets a player walk through walls and breaks enemy pathing. Players who are stuck can use fast travel, teleport to a party member, or the Become Ethereal shout instead. tfc, tm and coc stay allowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxR8LzniCZWnKRhyEtP
It only changes the player's own camera field of view (fDefaultFOV, fDefault1stPersonFOV and fDefaultWorldFOV), so it can't affect other players. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
Replaying SkinFadeIn after 1s, 3s and on every gear change restarted the skin's fade each time, so other players lost the transform intro and saw the body appear late. The event is now retried only until the copy's skin graph takes it, then left alone, and retried again for a respawned copy. Info logs cover sending, receiving and taking the events and the beast form respawns, for the second-transform problem. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxQEu7yvi9eRZyKsEur
Include the TESRace definition before accessing its form ID in CharacterService.
…mage Players report that enemies barely hurt remote party members while the leader takes about twice the damage. This adds client-only [DmgDiag] logging on the damage path: every hit involving a player (who applied it, raw and difficulty-scaled damage, armor, health before and after), every health delta sent and received, and the owner's health snapshots for players. Lines that look like a duplicate or mismatch are logged as warnings with a FLAG reason. No protocol or gameplay changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fvQxSwpo46aX4h3sn2BYB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Eric · project thread
Diagnostic build based on tag
v1.8.7-10. Draft so CI compiles it; not meant to merge as is.What
Players report enemies barely hurting remote party members while the leader takes about twice the damage. This adds client-only
[DmgDiag]lines to the normal client log:HIT: everyDamageActorcall involving a player: path taken inHookDamageActor, whether this client applied it or left it to the owner, raw and difficulty-scaled damage, multiplier, health, damage resist, difficulty and party role.APPLIED: health before/after the game applied the hit, next to the delta sent to other clients.SENT/RECV: health deltas sent to and received from the server, matched to the hit.SNAPSHOT: the owner's absolute health for a player (currently ignored for health) compared to the local copy.Warnings carrying
FLAG:DUP_LOCAL_HIT,SENT_VS_APPLIED,DUP_BROADCAST,LIKELY_DOUBLE_DAMAGE,DAMAGE_ON_OWNED_ACTOR,REMOTE_DAMAGE_DIFFERS,HEALTH_DRIFT.No protocol, server or gameplay changes;
HookDamageActorkeeps the same branches and return values.Verified / assumed
Generated by Claude Code