Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
a8688f7
Keep kicked players' names in everyone's player list
absol89 Oct 22, 2026
791a69d
Add player name tags with privacy settings
absol89 Oct 22, 2026
cb87311
Stop redundant nametag settings updates
absol89 Nov 1, 2026
d5b0a52
Add floating subtitle chat and Known chat names
absol89 Oct 22, 2026
ab58084
Add Swedish strings for floating chat and Known chat names
absol89 Oct 22, 2026
103c3d5
Add player-to-player trading
absol89 Oct 22, 2026
03e3d52
Apply Known names and a trade invitation setting to trades
absol89 Oct 22, 2026
86d2013
Fix a crash when an activated object is deleted before its activation…
absol89 Oct 27, 2026
cde31cd
Don't give summons death items
absol89 Oct 24, 2026
83e64b6
Don't equip unresolved spells or shouts on spawned actors
absol89 Oct 18, 2026
2f9ae89
Fix crash when a launched projectile can't be found
absol89 Oct 19, 2026
f1554ea
Fix crash when a remote actor is teleported into an unloaded exterior…
absol89 Oct 21, 2026
78e6d90
Fix remote actors looking crouched when players use different behavio…
absol89 Oct 21, 2026
9a39ecc
fix: hook ShowSubtitle after SKSE plugins so Floating Subtitles can c…
absol89 Oct 21, 2026
0e39db7
Restore the weapon state when a forced draw or sheathe is refused (#810)
sfedev Sep 15, 2026
1dc7827
Allow server settings to be overridden with environment variables
rjmcgee53192 Oct 27, 2026
dd909e5
Sync mod animation events and add a /wave chat command
DJLegends1011 Nov 2, 2026
c72f1d9
Stop remote actors ignoring updates after a missed transfer
miredirex Oct 21, 2026
85f4d77
Fix NPCs losing outfits equipped during their assignment
miredirex Oct 21, 2026
4564405
Keep equipment changes received before an actor's 3D loads
miredirex Oct 21, 2026
0e13b8f
Prevent duplicate owners after the server removes an actor
miredirex Oct 21, 2026
38a304a
Resolve inventory hook ownership on the main thread
miredirex Oct 21, 2026
20ebe7f
Move client ownership epoch checks into `OwnershipSystem`
miredirex Oct 21, 2026
db5555a
fix: clean up stale object replication bindings
sfedev Sep 10, 2026
23fed5b
Advance the shared time when a player sleeps or waits (#467)
sfedev Sep 15, 2026
cd6f667
Overhaul item drops: sync dropped items through server-tracked refere…
miredirex Oct 5, 2026
cf94468
Remove unused Actor::DropObject
miredirex Oct 5, 2026
bcbad81
Simplify DroppedItemService on client and server
miredirex Oct 5, 2026
81973e1
Shorten some comments in the new code
miredirex Oct 5, 2026
21689b4
Spawn other players' dropped crafted potions from their recipe
claude Nov 2, 2026
b24e41e
Don't let a snapshot without worn items strip a living NPC
claude Nov 2, 2026
5230382
fix: restore torso-less remote player bodies in validation build
absol89 Oct 3, 2026
0c4ae79
Fire BeastFormChangeEvent on beast-form leave after the race reverts
GhostWriter42 Oct 6, 2026
d192f7b
Don't party-sync the Riften North Gate guard scene quest
absol89 Oct 17, 2026
bce4a8c
fix: allow validation tags in Windows release builds
absol89 Oct 4, 2026
0b52fc7
fix: build validation release tags on Linux
absol89 Oct 7, 2026
7acbea0
ci: preserve release assets when rebuilding existing tags
absol89 Oct 7, 2026
20d1e58
fix: announce to the server list over IPv4 only
absol89 Nov 2, 2026
7b51e18
ci: tag builds off master keep F6/F7 and the console
absol89 Nov 2, 2026
1d11ed6
Playtest builds: mimalloc secure mode, crash at a bad free
absol89 Nov 2, 2026
9bf18af
The console follows SkyrimTogetherTweaks.ini
absol89 Nov 2, 2026
5de701f
Add a server setting that turns off typed console commands
claude Nov 2, 2026
80cca64
Relay proximity voice chat through the game server
claude Nov 2, 2026
9905e8a
Add built-in proximity voice chat to the client
claude Nov 2, 2026
399e4d7
Add voice chat controls to Settings and the player manager
claude Nov 2, 2026
798b47c
Ship the Opus licence notice with the client
claude Nov 2, 2026
e364022
Qualify MakeUnique in the voice audio device
claude Nov 2, 2026
eb61c3e
Treat death gold settings above one as flat fees
absol89 Nov 2, 2026
a393dcf
Merge death gold penalty fix into API-Together-1.8.7-8
absol89 Nov 2, 2026
da46b37
Sync exterior locks across nearby cells
absol89 Nov 2, 2026
55322b6
Stop sending truncated client messages
absol89 Nov 2, 2026
d78cb2a
Retry Papyrus natives that were unavailable at startup
absol89 Nov 2, 2026
6efb569
Defer actor inventories until their process is loaded
absol89 Nov 2, 2026
1f123dc
fix/CharacterService Dead NPC stands
absol89 Nov 2, 2026
c738d75
README: features in the v1.8.7-8 playtest
claude Nov 2, 2026
fc6bb44
Restore the V1 plugin API with optional nearby channels
absol89 Nov 3, 2026
06ed783
Add a voice master volume slider up to 500 percent
absol89 Nov 3, 2026
aee9dd9
Default the voice master volume to 100% and the noise gate to 5%
claude Nov 3, 2026
64626c7
Hear voices from the character when the camera is far away
claude Nov 4, 2026
443a9e5
Use the fade start as the camera distance limit for voice
claude Nov 4, 2026
9d9cc26
Make voice fall off like real sound, default fade start 2000
claude Nov 4, 2026
68e65f2
Keep the voice fade hint to the two distances
claude Nov 4, 2026
e8b61fc
Centre the leader crown on name tags; widen default tag ranges
claude Nov 4, 2026
c43915c
Don't block in the crash handler
claude Nov 4, 2026
1c32f70
Merge crash handler fix for in-world quit hang into API-playtest
claude Nov 4, 2026
9f6bd00
Don't spawn dead actors that are disabled locally
claude Nov 4, 2026
6e86c91
Merge fix for re-killing disabled dead actors into API-playtest
claude Nov 4, 2026
96eff8c
Don't restart completed run once quests from party updates
claude Nov 4, 2026
fe74794
Merge completed run once quest fix into API-playtest
claude Nov 4, 2026
869aa9e
Cut party UI redraws while a party member is in combat
claude Nov 4, 2026
7aabbf5
Harden actor spawning and add crash diagnostics
absol89 Nov 4, 2026
4940509
Keep remote corpses dead through loading and animation replay
absol89 Nov 4, 2026
6edab7b
Don't teleport remote actors into a worldspace the player isn't in
claude Nov 5, 2026
8d75fd2
Merge remote teleport worldspace fix into API-playtest
claude Nov 5, 2026
e2c45e4
Make offensive perks work against other players in PvP
absol89 Nov 4, 2026
d22821a
Make poisons apply to other players in PvP
absol89 Nov 4, 2026
c09cfa7
Show other players with Detect Life and keep it to the caster
absol89 Nov 4, 2026
df2ebc2
Merge PvP offensive perks into API-playtest
claude Nov 5, 2026
a5f152f
Merge PvP poisons into API-playtest
claude Nov 5, 2026
7cd8d51
Merge PvP Detect Life into API-playtest
claude Nov 5, 2026
f9df222
Make Disarm work on other players in PvP
absol89 Nov 5, 2026
c444763
Make summons attack a player fighting their owner in PvP
absol89 Nov 5, 2026
1ea469f
Make area spells hit other players in PvP
absol89 Nov 5, 2026
f0ec40d
Make cloak spells hit other players in PvP
absol89 Nov 5, 2026
86d67c4
Let spell conditions accept attacks from other players in PvP
absol89 Nov 5, 2026
d472754
Merge PvP disarm into API-playtest
absol89 Nov 5, 2026
64490c6
Merge PvP summons into API-playtest
absol89 Nov 5, 2026
aab48e0
Merge PvP area spells into API-playtest
absol89 Nov 5, 2026
3c923d1
Keep Unbounded Freezing's knockdown off other players
absol89 Nov 5, 2026
2fb9e60
Merge Unbounded Freezing knockdown block into API-playtest
absol89 Nov 5, 2026
65b3e5f
Make the F2 UI usable on ultrawide screens (#945)
jotagalindo Nov 5, 2026
1a11c74
Lay dead bodies back down when they stand up
absol89 Nov 5, 2026
b7d56be
Stop retrying a refused remote death on every tick
absol89 Nov 7, 2026
3a99822
Move per-actor spawn diagnostics to debug level
absol89 Nov 7, 2026
554265a
Fit the F2 menu bar to its labels and match the chat to it
absol89 Nov 7, 2026
4e952a3
Use a wider fixed menu column with word-wrapped labels instead
absol89 Nov 7, 2026
176e9e1
Widen the F2 menu bar for words that can't wrap
absol89 Nov 7, 2026
a5c32ca
Keep trading readable on small screens and large text
absol89 Nov 7, 2026
846084e
Respawn the puppet for any non-playable (beast) race, not just vanill…
NutyoPR Nov 8, 2026
650d661
Sync subgraph animations so other players see a Weredragon
absol89 Nov 12, 2026
f1a0f38
Hook PlaySubGraphAnimation on Actor so the Weredragon skin syncs
absol89 Nov 12, 2026
7828b99
Fix the subgraph sync build and replay events for unloaded copies
absol89 Nov 12, 2026
b0f263f
Address review on the console command guard
claude Nov 4, 2026
d6c44cc
Keep tgm and tcl blocked when typed commands are off
claude Nov 4, 2026
c7c3fa8
Allow fov when typed console commands are off
absol89 Nov 12, 2026
d2e454f
Play the Weredragon skin fade once per body instead of restarting it
absol89 Nov 12, 2026
77728f2
Fix client compilation of race-change logging
absol89 Nov 12, 2026
a91c0c4
Log damage and health sync events to find doubled or missing party da…
claude Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ jobs:
git submodule sync --recursive
git submodule update --init --force --recursive --depth=1

- name: Checkout master on tag push
- name: Prepare tagged release checkout
if: github.ref_type == 'tag'
run: |
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/master)"
# Build the tagged commit; the local branch name enables release packaging.
git checkout -B master HEAD

- name: Cache xmake dependencies
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
upload-build-for-next-job: true

publish:
# Retagging builds CI artifacts without changing an existing release or its assets.
if: ${{ github.event.created }}
needs: [build-windows, build-linux]
runs-on: ubuntu-24.04
permissions:
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,16 @@ jobs:
git submodule sync --recursive
git submodule update --init --force --recursive --depth=1

- name: Checkout master on tag push
- name: Prepare tagged release checkout
if: github.ref_type == 'tag'
run: |
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/master)"
git checkout -B master HEAD
# Build the tagged commit. On master the local branch name makes it a release build (IS_MASTER: no
# debug keys or console). A tag on any other branch builds like that branch, keeping F6/F7 and the console.
if git merge-base --is-ancestor HEAD origin/master 2>/dev/null; then
git checkout -B master HEAD
else
git checkout -B "tag/${GITHUB_REF_NAME}" HEAD
fi

- name: Resolve artifact version
if: ${{ inputs.upload-build-for-next-job }}
Expand Down
1 change: 1 addition & 0 deletions Code/client/Components.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <Components/FaceGenComponent.h>
#include <Components/CacheComponent.h>
#include <Components/WaitingFor3D.h>
#include <Components/PendingInventoryComponent.h>
#include <Components/ActorValuesComponent.h>
#include <Components/ObjectComponent.h>
#include <Components/PlayerComponent.h>
Expand Down
22 changes: 22 additions & 0 deletions Code/client/Components/PendingInventoryComponent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#ifndef TP_INTERNAL_COMPONENTS_GUARD
#error Include Components.h instead
#endif

#include <Structs/Inventory.h>
#include <Messages/NotifyInventoryChanges.h>
#include <Messages/NotifyEquipmentChanges.h>
#include <variant>

struct PendingInventoryComponent
{
PendingInventoryComponent(const Inventory& acInventory, uint32_t aOwnershipEpoch)
: Content(acInventory), OwnershipEpoch(aOwnershipEpoch)
{
}

Inventory Content;
uint32_t OwnershipEpoch;
Vector<std::variant<NotifyInventoryChanges, NotifyEquipmentChanges>> Updates;
};
5 changes: 5 additions & 0 deletions Code/client/Components/RemoteComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ struct RemoteComponent
uint32_t Id;
uint32_t CachedRefId;
uint32_t OwnershipEpoch;
// Canonical death state survives loading and prevents living animation replay on corpses.
bool IsDead{false};
// Retries of a death the game did not take, see CharacterService::RunRemoteUpdates.
uint8_t KillAttempts{0};
double NextKillAttempt{0.0};
};
3 changes: 3 additions & 0 deletions Code/client/Components/WaitingFor3D.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#endif

#include <Messages/CharacterSpawnRequest.h>
#include <Messages/NotifyEquipmentChanges.h>

struct WaitingFor3D
{
Expand All @@ -14,4 +15,6 @@ struct WaitingFor3D
}

CharacterSpawnRequest SpawnRequest{};
// Equipment changes relayed before the 3D loaded, replayed after the spawn request's inventory is applied
Vector<NotifyEquipmentChanges> PendingEquipmentChanges{};
};
17 changes: 7 additions & 10 deletions Code/client/CrashHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "CrashHandler.h"
#include <DbgHelp.h>
#include <Windows.h>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <iomanip>
Expand All @@ -23,16 +24,12 @@ std::string SerializeTimePoint(const time_point& time, const std::string& format

LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
{
static int alreadyCrashed = 0;
static std::atomic_bool s_alreadyCrashed = false;
auto retval = EXCEPTION_CONTINUE_SEARCH;

// Serialize
static std::mutex singleThreaded;
const std::lock_guard lock{singleThreaded};

// Check for severe, not continuable and not software-originated exception
// Handle only the first access violation. Later or nested ones pass through instead of blocking.
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION &&
alreadyCrashed++ == 0)
!s_alreadyCrashed.exchange(true))
{
spdlog::critical (__FUNCTION__ ": crash occurred!");

Expand Down Expand Up @@ -115,13 +112,13 @@ LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
{
spdlog::critical(__FUNCTION__ ": UnhandledExceptionFilter() workaround triggered.");

singleThreaded.unlock(); // Might reenter, but is safe at this point.
if ((*pCurrentUnhandledExceptionFilter)(pExceptionInfo) == EXCEPTION_CONTINUE_EXECUTION)
retval = EXCEPTION_CONTINUE_EXECUTION;
singleThreaded.lock();
}

spdlog::shutdown();
// The exception may still be handled, and logging after spdlog::shutdown() crashes
if (auto pLogger = spdlog::default_logger())
pLogger->flush();
}
return retval;
}
Expand Down
7 changes: 7 additions & 0 deletions Code/client/Events/ActivateEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ struct TESBoundObject;

/**
* @brief In-game activation (i.e. interacting with an object).
*
* Handled after a queue, when the game may have deleted the object or the activator: check that ObjectId and
* ActivatorId still name them (TESForm::GetById) before using the pointers.
*/
struct ActivateEvent
{
ActivateEvent(TESObjectREFR* apObject, Actor* apActivator, TESBoundObject* apObjectToGet, int32_t aCount, bool aDefaultProcessing, uint8_t aUnk1, TESObjectREFR::OpenState aPreActivationOpenState, bool aActivateFlag = false)
: pObject(apObject)
, pActivator(apActivator)
, ObjectId(apObject ? apObject->formID : 0)
, ActivatorId(apActivator ? apActivator->formID : 0)
, pObjectToGet(apObjectToGet)
, Count(aCount)
, DefaultProcessing(aDefaultProcessing)
Expand All @@ -23,6 +28,8 @@ struct ActivateEvent

TESObjectREFR* pObject;
Actor* pActivator;
uint32_t ObjectId;
uint32_t ActivatorId;
TESBoundObject* pObjectToGet;
int32_t Count;
bool DefaultProcessing;
Expand Down
2 changes: 2 additions & 0 deletions Code/client/Events/AddTargetEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ struct AddTargetEvent
bool IsDualCasting{false};
bool ApplyHealPerkBonus{false};
bool ApplyStaminaPerkBonus{false};
// A hit on another player that the victim's client can't work out by itself, even for a concentration spell.
bool IsPvpForwarded{false};
};
19 changes: 19 additions & 0 deletions Code/client/Events/DropItemEvent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include <Structs/Inventory.h>

/**
* @brief Dispatched when the local player drops an item into the world.
*/
struct DropItemEvent
{
DropItemEvent(const uint32_t aFormId, Inventory::Entry aItem)
: FormId(aFormId)
, Item(std::move(aItem))
{
}

// Form id of the reference the engine placed in the world.
uint32_t FormId{};
Inventory::Entry Item{};
};
2 changes: 0 additions & 2 deletions Code/client/Events/EquipmentChangeEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
struct EquipmentChangeEvent
{
uint32_t ActorId = 0;
uint32_t ServerId = 0;
uint32_t OwnershipEpoch = 0;
uint32_t ItemId = 0;
uint32_t EquipSlotId = 0;
uint32_t Count = 0;
Expand Down
17 changes: 3 additions & 14 deletions Code/client/Events/InventoryChangeEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

/**
* @brief Dispatched when the contents of an object or actor inventory changes locally.
*
* The event has a Drop member variable, since dropped items need to be handled differently.
*/
struct InventoryChangeEvent
{
Expand All @@ -16,25 +14,16 @@ struct InventoryChangeEvent
{
}

InventoryChangeEvent(const uint32_t aFormId, Inventory::Entry arItem, bool aDrop)
InventoryChangeEvent(const uint32_t aFormId, Inventory::Entry arItem, bool aUpdateClients)
: FormId(aFormId)
, Item(std::move(arItem))
, Drop(aDrop)
{
}

InventoryChangeEvent(const uint32_t aFormId, Inventory::Entry arItem, bool aDrop, bool aUpdateClients)
: FormId(aFormId)
, Item(std::move(arItem))
, Drop(aDrop)
, UpdateClients(aUpdateClients)
{
}

uint32_t FormId{};
uint32_t ServerId{};
uint32_t OwnershipEpoch{};
Inventory::Entry Item{};
bool Drop = false;
bool UpdateClients = true;
// The item moved to or from the local player, e.g. looting or pickpocketing an actor owned by someone else
bool IsPlayerTransfer = false;
};
14 changes: 14 additions & 0 deletions Code/client/Events/ReferencePickUpEvent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

/**
* @brief Dispatched when a local actor picks up a temporary reference, such as a dropped item.
*/
struct ReferencePickUpEvent
{
explicit ReferencePickUpEvent(const uint32_t aFormId)
: FormId(aFormId)
{
}

uint32_t FormId{};
};
7 changes: 6 additions & 1 deletion Code/client/Events/ScriptAnimationEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
*/
struct ScriptAnimationEvent
{
ScriptAnimationEvent(uint32_t aFormID, String aAnimation, String aEventName)
ScriptAnimationEvent(uint32_t aFormID, String aAnimation, String aEventName, bool aIsSubGraph = false)
: FormID(aFormID)
, Animation(aAnimation)
, EventName(aEventName)
, IsSubGraph(aIsSubGraph)
{
}

// Local form id of the animating reference; ObjectService::OnScriptAnimationEvent
// translates it to a server id before it goes on the wire.
uint32_t FormID;
String Animation;
String EventName;
// Sent to the graphs of what the actor wears (Actor.PlaySubGraphAnimation), not its own.
bool IsSubGraph;
};
8 changes: 8 additions & 0 deletions Code/client/Events/WaveCommandEvent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#pragma once

/**
* @brief Dispatched when the local player uses the /wave chat command.
*/
struct WaveCommandEvent
{
};
5 changes: 5 additions & 0 deletions Code/client/Games/Animation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ static TPerformAction* RealPerformAction;

// TODO: make scoped override
thread_local bool g_forceAnimation = false;
// Result of the last action the game performed while g_forceAnimation was set (-1: none).
thread_local int g_forcedActionResult = -1;

uint8_t TP_MAKE_THISCALL(HookPerformAction, ActorMediator, TESActionData* apAction)
{
Expand All @@ -40,6 +42,9 @@ uint8_t TP_MAKE_THISCALL(HookPerformAction, ActorMediator, TESActionData* apActi

const auto res = TiltedPhoques::ThisCall(RealPerformAction, apThis, apAction);

if (g_forceAnimation)
g_forcedActionResult = res;

// spdlog::debug("Action event name: {}, target name: {}", apAction->eventName.AsAscii(), apAction->targetEventName.AsAscii());

// This is a weird case where it gets spammed and doesn't do much, not sure if it still needs to be sent over the network
Expand Down
40 changes: 40 additions & 0 deletions Code/client/Games/Memory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,49 @@ void* Hook_aligned_malloc(size_t aSize, size_t aAlignment)
return mi_malloc_aligned(aSize, aAlignment);
}

// Playtest builds, not meant for upstream. mimalloc's secure mode reports a double free (EAGAIN), a corrupted free list
// (EFAULT) or an invalid pointer (EINVAL) where it is detected; the handler then crashes on the spot, so CrashLogger
// records the stack of the code that freed the block instead of a hang somewhere later. Messages go to
// logs\mimalloc_errors.log through the Windows file API, as the mimalloc heap cannot be trusted at that point.
static constexpr DWORD kMimallocErrorException = 0xE04D494D; // "MIM"

static void WriteMimallocLog(const char* acpText) noexcept
{
const HANDLE cFile = CreateFileA("Data\\SkyrimTogetherReborn\\logs\\mimalloc_errors.log", FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (cFile == INVALID_HANDLE_VALUE)
return;

DWORD written = 0;
WriteFile(cFile, acpText, static_cast<DWORD>(strlen(acpText)), &written, nullptr);
CloseHandle(cFile);
}

static void OnMimallocOutput(const char* acpMessage, void*) noexcept
{
OutputDebugStringA(acpMessage);
WriteMimallocLog(acpMessage);
}

static void OnMimallocError(int aError, void*) noexcept
{
char line[96];
sprintf_s(line, "mimalloc error %d on thread %lu\r\n", aError, GetCurrentThreadId());
WriteMimallocLog(line);

// An invalid pointer is a free of memory mimalloc never handed out. No playtest session logged one by mistake, so it
// crashes too.
if (aError == EAGAIN || aError == EFAULT || aError == EINVAL)
RaiseException(kMimallocErrorException, EXCEPTION_NONCONTINUABLE, 0, nullptr);
}

static TiltedPhoques::Initializer s_memoryHooks(
[]()
{
mi_register_output(&OnMimallocOutput, nullptr);
mi_register_error(&OnMimallocError, nullptr);
mi_option_enable(mi_option_show_errors);

POINTER_SKYRIMSE(TFormAllocate, s_formAllocate, 68115);

POINTER_SKYRIMSE(TFormFree, s_formFree, 68117);
Expand Down
Loading