You are viewing a potentially older version of this package. View all versions.
Ezomic-Longhouse_Core-1.2.4 icon

Longhouse Core

Shared plumbing for the Ezomic mods. Installed for you as a dependency.

Date uploaded 12 hours ago
Version 1.2.4
Download link Ezomic-Longhouse_Core-1.2.4.zip
Downloads 95
Dependency string Ezomic-Longhouse_Core-1.2.4

This mod requires the following mods to function

denikson-BepInExPack_Valheim-5.4.2350 icon
denikson-BepInExPack_Valheim

BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.

Preferred version: 5.4.2350

README

Longhouse Core

Shared library for the Ezomic Valheim mods. It handles the parts that only make sense once, per machine rather than per mod: checking that a client and a server are running the same mods, applying the host's settings while you are connected, and arbitrating extra player inventory rows between mods that each want some.

You normally get it as part of the Longhouse pack. It is a single DLL with no assets, built against Valheim 1.0.7, Unity 6000.0.75, BepInEx 5.4.23.5 and Harmony 2.9.

Features

  • Version check. Each Ezomic mod registers its guid, version and build id. When a client connects, both ends exchange that list, and the server rejects a client whose list does not match. The reason is written to both logs and appended to the client's "Incompatible version" screen, which otherwise says nothing useful.
  • Host config. The server sends its settings for every registered mod, and the client uses them for as long as it is connected. Nothing is written to the client's config file, and the client's own values come back on disconnect.
  • Shared inventory height. Mods claim a number of extra player inventory rows instead of writing Inventory.m_height themselves. Core sums the claims and writes the field once, so two mods that both want rows get both, and it protects the contents of those rows during loads.

All three can be turned off in the config file.

This repository also carries shared/Prefabs.cs and shared/BiomeIndex.cs, which are source files mods link into their own projects. They are not part of the DLL. See For mod authors.

Installation

Install BepInEx 5.4.2350 first. These mods use the BepInEx 5 API and will not load on BepInEx 6.

A mod manager handles it if you install the Longhouse pack, which pins Core along with every other member. To install it on its own, drop EzomicCore.dll in BepInEx\plugins\Core\.

Install it on the dedicated server as well as on clients. The server is the only side that can actually refuse a connection, so a server without Core does not check anything, whatever its clients are running.

Core is an optional soft dependency for the individual mods. Each one checks whether Core is loaded and, if it is not, logs a line saying it is running standalone and carries on without the version check and the host config. The exception is Delve, which requires it outright.

Configuration

BepInEx\config\ezomic.valheim.core.cfg, all under a Multiplayer section.

Key Default Effect
EnforceVersions true Refuse a connection when the two ends disagree about which Ezomic mods are installed or about their versions. Turning it off does not make a mismatch safe, it makes it silent
EnforceBuilds true Also refuse when both ends claim the same version but were built from different source. Turn this off if you build the mods yourself on more than one machine: the build id depends on source paths, so the same commit in a different folder produces a different id
EnforceConfig true Apply the host's settings while connected. On a server this decides whether it sends them, on a client whether it accepts them

Multiplayer

The mod list goes out in ZNet.OnNewConnection, before either side sends PeerInfo, so it has arrived by the time the check runs. Every disagreement is reported at once rather than one per reconnect attempt.

What gets compared, per registered mod: the version string, then the build id (the assembly's module version id, which the compiler derives from the compilation inputs), then a hash of the mod's data file if it declared one. A missing build id or data hash from an older Core on the far end is treated as unknown, not as a mismatch.

A mod registers as Everyone or HostOnly. Everyone means both ends need it at the same version. HostOnly means clients without it are let in, but a client that does have it is still checked against the host, in both directions. Skaft is HostOnly, for example: it is client-side hammer repair and the server neither gains nor loses by a client having it.

The client checks too, but only to write a readable log and put the detail on the refusal screen. There is one place a connection is actually closed, and it is on the server.

Config sync. Registering a mod syncs its whole config file, minus keybinds and minus anything the mod held back with Suite.Local. Values are swapped in memory on the client, the originals are kept, and they are restored when ZNet.Shutdown runs, which covers quitting to the menu, being kicked and the connection dropping. If ConfigurationManager is installed, an imposed entry is greyed out, and editing one anyway puts the host's value straight back.

Keybinds (KeyCode and KeyboardShortcut entries) are never imposed on a client unless the mod that owns them calls Suite.Sync on them explicitly.

A client can set EnforceConfig = false and keep its own settings. The version check is what the server enforces; the config sync is cooperative.

Inventory rows

Valheim 1.0 has its own inventory row feature, and it evicts anything below the row count it believes in. Core works with that rather than against it.

  • The grid is capped at 9 rows. That is vanilla's clamp in Player.SetInventorySize and there is no way past it.
  • Rows bought from the trader win. A character that has bought rows arrives with a higher baseline, and mod claims get whatever is left up to 9. When claims are truncated, Core logs a warning naming the numbers. Nothing is dropped.
  • The character's invrows key is written back to vanilla's own count, not the inflated one, so it does not compound across logins and so uninstalling Core leaves a normal character. Vanilla will then drop whatever was in the extra rows on the ground, which is the honest outcome for rows nothing is providing any more.
  • Any inventory is widened while it is being read from disk and trimmed back afterwards, never below what the items occupy. Without this, an item saved in a row below the grid it loads into is silently destroyed. That applies to graves as well as to the player, which is why it covers every container rather than only the player's.
  • The wooden panel behind the inventory grid is resized to match, measured against the grid on screen so it covers rows from any source. The container window is pushed down, and lifted back on screen if it would fall below it, overlapping the inventory instead.

If the row patches do not all apply, Core does not drive rows at all and logs an error. Rows claimed without the load protection is the one combination that destroys items.

For mod authors

Register from Awake, after binding config:

private void Awake()
{
    YokeConfig.Bind(Config);

    Suite.Register(PluginGuid, PluginName, PluginVersion, Config);
}

Suite.Register(guid, name, version, config, requirement = Requirement.Everyone, owner = null) puts the mod on the version check and absorbs its config for syncing. Requirement.HostOnly is the other option; use Everyone for anything that registers a prefab or changes item data, because a client that cannot resolve a prefab hash discards the ZDO rather than erroring.

The rest of the API:

  • Suite.Sync(entry, ...) forces an entry into the synced set. Rarely needed, since registering syncs everything already, except to insist that a keybind really must match.
  • Suite.Local(entry, ...) keeps an entry out of the host's hands: UI scale, colours, hover text, preferred units, anything a mismatch cannot desync.
  • Suite.Data(contents, guid = null) folds a data file into the version check, for a mod that is a DLL plus a text file that decides what it does. It hashes the contents with line endings normalised, so a CRLF difference is not a mismatch.
  • Suite.ExplainRefusal(reason) sets the text appended to the next connection-failure screen. Call it just before dropping someone.
  • Suite.Display(order, advanced, name) returns a ConfigurationManager attributes object for a ConfigDescription tag, so the in-game settings window is ordered rather than alphabetical.
  • InventoryRows.Claim(PluginGuid, 3) asks for three extra player rows, replacing whatever that guid asked for before. Claim(PluginGuid, 0) gives them back. Cheap to call every frame. InventoryRows.Total is what everyone claimed; InventoryRows.Extra is how much taller the grid actually is, which can be more.

Keep Core a soft dependency. Check Chainloader.PluginInfos for ezomic.valheim.core and put the Suite call in a separate method marked [MethodImpl(MethodImplOptions.NoInlining)]. The JIT resolves the assemblies a method needs when it first compiles that method, so a Suite call sitting directly in Awake pulls in the assembly before the check can prevent it, and the missing-assembly exception lands during plugin load.

Shared source

shared/Prefabs.cs and shared/BiomeIndex.cs live in this repository and are excluded from the DLL. Mods link them:

<Compile Include="..\core\shared\Prefabs.cs" Link="shared\Prefabs.cs" />
<Compile Include="..\core\shared\BiomeIndex.cs" Link="shared\BiomeIndex.cs" />

They are source rather than classes in the DLL because a mod that cannot register its prefab or classify an item does not degrade, it does nothing, and putting them in Core would make Core mandatory for every mod that uses them. A runtime fallback would mean a second code path that only runs where nobody tests.

Prefabs is the runtime prefab registry. Prefabs.Keep(name, build, item, buildTool) plus Prefabs.Tick() from the plugin's Update builds the prefab once and re-registers it into every world: both of ZNetScene's lookups, ObjectDB when it is an item, and a tool's build menu when it is a piece. It checks the live scene each time rather than a flag, which matters because ZNetScene and ObjectDB are rebuilt on every world load, including a trip to the menu and back. A registry that answers "already done?" from a static bool early-returns into a scene that has never heard of the prefab, and every ZDO of that prefab is then discarded with nothing written to any log. The steps are also available individually: Known, Holder, Clone, Donor, Register, RegisterItem, ToolPieces, InTool, AddToTool, and Drop to stop keeping one. Set Prefabs.Log to the plugin's own logger.

BiomeIndex answers which biome an item comes from, derived from the game's own tables: ZoneSystem.m_vegetation, the spawn lists and their CharacterDrops, smelter and cooking recipes, plus a small override string for the handful none of those reach (iron scrap, for one, which only exists inside Sunken Crypts). Earliest biome wins. BiomeIndex.BiomeOf(name) is the lookup; BiomeForKey and Overrides are the delegates the host mod fills in. Yoke and Hirsla link it.

Fixing either file means rebuilding every mod that links it.

Troubleshooting

A mod's log line says ** NOT ENFORCED, Core's version gate failed to apply **. One of Core's patch groups did not apply, usually after a game update. Core logs came up DEGRADED with the list of what is missing instead of its normal ready. line. Read the errors above it.

Clients are refused with "different build". Both ends have the same version number but different binaries. Rebuild whichever is behind, or set EnforceBuilds = false if you build from source in more than one working folder.

A mod in the plugins folder is not being checked. The version check only sees mods that call Suite.Register. Anything else, including third-party mods, is invisible to it.

Settings on a client are not following the server. Check EnforceConfig on both ends, and check that the mod in question registers with Core at all. Keybinds are excluded by design.

Bug reports

The Discord is the fastest route, and the right one if you are not sure whether what you are seeing is a bug. Issues on the repository work too and suit anything long.

Attach BepInEx\LogOutput.log, and say whether you were on a dedicated server, hosting, or in single player. For a connection that was refused, the log from both ends is worth far more than either alone. If a vanilla mechanic broke rather than a mod feature, check AppData\LocalLow\IronGate\Valheim\Player.log as well: exceptions thrown mid-frame land there and not in the BepInEx log.

Discord

discord.gg/hJzAVaZ5wb is where mod information, updates, support, bug reports and compatibility questions go.

Server

There is also a small EU server running the pack if you want somewhere to play: hard combat difficulty, resources at 1x, everything else vanilla. Connection details are in the Discord.

Licence

MIT. Robbin Thijssen, Thijssen Software. See LICENSE, and CHANGELOG.md for what shipped when.

Part of Longhouse

Core is a member of the Longhouse pack, which pins one set of versions that a server will accept. You do not need the pack to use Core, and it behaves the same on its own.

CHANGELOG

Changelog

Notable changes to Core. Format follows Keep a Changelog, and the mod uses semantic versioning.

[1.2.4] - 2026-09-12

Changed

  • Rewritten README. Same mod, clearer documentation: what it does and how to install it come first, then configuration, multiplayer behaviour, compatibility and troubleshooting. Every config table was checked against the plugin's own Config.Bind calls, so the settings, sections and defaults listed are the ones actually bound. No code changed in this release.

[1.2.3] - 2026-09-11

Reported by a player who bought an inventory row from Haldor and found its slots drawn over open water with no wood behind them.

Fixed

  • A row bought from the trader had no panel behind it. The backdrop grew the wooden panel by the rows mods had claimed above the vanilla baseline, and that baseline is learned from the invrows key - so buying a row moved the baseline up with it and the count stayed at zero. Vanilla sizes the panel when the window is built and never resizes one already open, and Core is the only other thing that writes that rect, so nothing corrected it for the rest of the session.

    The panel is now sized from the grid on screen against what the captured art already fits, both measured rather than assumed. That covers all three reasons the wood has to move at once - rows a mod claimed, rows the player bought, and the tick's own clamp up to whatever the items occupy - where only the first was counted.

  • Every chest window was a row of wood too tall, top and bottom. The container window is a child of the player window, so the search for the backdrop reached the chest panel's own background and frame: same sprite, near enough the same width, and both filters waved them through. Skipped by position now, because nothing about the art separates them.

  • The chest window could end up below the screen. It is pushed down by what the inventory panel gained, which is right for a row or two and puts it under the taskbar once a character has bought every row a trader sells. It is lifted back on now, overlapping the inventory instead - at that height the two cannot both fit, so the choice is which one is reachable, and it says in the log when it happens.

Changed

  • The backdrop runs whether or not Core's own row claims are safe to apply. It used to sit behind that guard, which was right when the panel was sized from claims; sized from the grid it cannot outrun the slots it is drawn behind. The player who reported this claims no rows at all, so the guard would have kept the fix from the people who need it.

  • The panel work says what it measured - which rects it grabbed, what the art fits, how far the container was lifted. Two of the three bugs above were found by reading those numbers after reasoning about the hierarchy had produced a confident wrong answer.

[1.2.2] - 2026-09-10

Fixed

  • A character logging into 1.0 for the first time came up with a fifteen row inventory. Valheim 1.0.7's Player.OnSpawned only calls SetInventorySize when the character already carries an invrows key, and otherwise just writes the key and returns. Every character made before 1.0 takes that second branch exactly once, on its first 1.0 login, so the prefix that learns vanilla's row count never fired and the baseline stayed at -1. The load-time widening then added its sixteen rows of working space to that -1 and the grid came up fifteen tall, after which the fallback measured the widened grid and adopted fifteen as vanilla's own height.

    Two changes. Player.OnSpawned now has a postfix that reads invrows back and learns the baseline from it, which is correct on both branches rather than only the broken one: on the else branch it returns the 4 vanilla just wrote, and on the other one Restore has already put the true base back into that key, so it is a no-op. And the widening now works from a height that exists rather than from an unlearned baseline, with the pre-widening height kept so the fallback can no longer measure the working space and mistake it for the inventory.

    No items were ever at risk - the eviction fence in VanillaRowsDrop holds the grid at whatever rows are occupied, and it did. The condition also clears itself on the next login, because vanilla wrote the key on the way through. It was wrong and alarming rather than destructive.

    Found on the live server an hour after updating it, reported as "i logged in and i have a 15 row inventory". Fifteen is LoadSlack - 1, which is what identified it.

    Player.OnSpawned is now in CorePlugin.Verify's list, so a future failure to attach that patch is named at startup instead of showing up as a strange row count.

[1.2.1] - 2026-09-10

1.2.0 shipped the wrong binary. If you have it, replace it. The package published under that number contains the 1.1.0 assembly, built 2026-08-28, so none of the fixes listed below under 1.2.0 are in it - including the inventory row protection, which is the one that stops items being destroyed. There is no code change between 1.2.0 and 1.2.1; this release exists because a Thunderstore version number cannot be reused.

Fixed

  • The published package now contains the code the version number claims. tcli build assembles a package from core/package/BepInEx/plugins/Core, a staging folder that nothing refreshed or checked. It held an August build. Core was the only mod published that way - the other nine went up from package.ps1 zips, and all nine were verified correct by installing the actual Thunderstore downloads and reading the versions they registered.

    Packaging now refuses to build when a staged assembly's version disagrees with manifest.json, so this cannot be published silently again.

    Found by updating the live server to Longhouse 2.0.0 and watching it come up announcing Registered Core 1.1.0, alongside the Ambiguous match for Inventory.Load that 1.2.0 fixed - which is to say the row protection was not merely absent from the package, it was visibly broken on a server the pack had just been installed on.

[1.2.0] - 2026-09-09

Rebuilt for Valheim 1.0. This version does not run on pre-1.0 Valheim, and the previous one does not run on 1.0.

Fixed

  • A failure in one patch group no longer takes the others with it, and the inventory patches apply first. They used to be five bare PatchAll calls with the ZNet handshake leading. A throw there meant the last two never ran - while the component was still added and Update still ticked, so the extra inventory rows kept being claimed with the Player.Load protection that keeps items in them absent. That combination destroys the bottom row on every relog and at every grave. Protect the data first, then wire the network.
  • The inventory load protection applies again on Valheim 1.0. 1.0 added a second Inventory.Load overload, so naming the method alone became an ambiguous match and Harmony refused it. Both overloads are named now. This is exactly the failure the reordering above was written for, and it arrived on the first launch against 1.0 - the rows were refused and the reason was on screen, instead of items going quietly missing.
  • A HostOnly mod is now allowed to be absent on either end. The manifest always sent each mod's requirement and the reading end threw it away, so HostOnly only ever protected the direction where the client lacked the mod. A Core server without Skaft refused every client that had it - which is why Skaft shipped standalone and stayed out of the pack for a week. It is in the pack now.

Changed

  • The log says when the gate is not actually there. Every mod's Registered ... line carries ** NOT ENFORCED ** when the handshake patches did not apply, and Core prints came up DEGRADED naming what is missing rather than its usual ready. line. Twelve confident lines describing a gate that was never wired is how this used to read.

[1.1.0] - 2026-08-23

Two changes, both about who owns what. Neither touches a prefab name or a saved value, so nothing already in a world is at stake.

Not in this release: the save-on-inventory-change guard. It is written and it works, and it is held back on the saveguard branch rather than shipped, because it changes when every player's character is written to disk and this release is not the one to find that out in.

Fixed

  • A host no longer takes your keybinds. Registering a mod syncs its whole config file, which is right for anything a mismatch could desync and wrong for the one class of setting the readme had already warned against in as many words. Three mods here bind a key - Tether, Vaettir's stow and Devkit - so for as long as this stood, joining a server moved your keys to whatever the host had chosen, greyed the entry out, and put the value back if you tried to change it. Keybinds are now held back by type. A mod that genuinely needs one to match can still say so with Suite.Sync.

Added

  • Suite.Local, the other half of that. Keybinds are the only thing Core can recognise on its own, and they are not the only thing a player would resent losing - a UI scale, a colour, a hover-text toggle. Anything a mismatch cannot desync belongs to the player, and only the mod knows which of its settings those are.

  • shared\Prefabs.cs: one runtime prefab registry for the suite, as shared source rather than as part of this DLL. Five mods have their own copy, and the copies are not the point: the wrong version of this destroys saved objects in silence. ZNetScene and ObjectDB are rebuilt on every world load, including a trip to the menu and back, so a mod answering "registered yet?" from a static bool says yes to a scene that has never heard of the prefab, registration early-returns, and every ZDO of it is discarded as junk with nothing written to any log. Stow lost a built piece that way on 2026-08-16. Everything here asks the live scene instead. Prefabs.Keep takes a name and a builder and holds the thing registered for whatever world is loaded - both of ZNetScene's lookups, ObjectDB when it is an item, a tool's build menu when it is a piece.

    It is linked into each mod's csproj and excluded from this project, so Core gains no new responsibility and the mods gain no new dependency. That is the whole reason it is a file and not a class in here: Core is soft everywhere, and a mod that could not register its prefab would load, patch nothing into the world and look broken - so owning this would have made Core mandatory for five mods to do anything at all. A runtime fallback was considered and rejected for costing two code paths, the second of which only ever runs where nobody tests, which is how the bug above survived in the first place.

    Linked so far by Taum; the five mods carrying their own copy are a change each.

[1.0.2] - 2026-08-19

Dying no longer eats what was on the extra rows. One fix, and nothing else is in this release: it is 1.0.1 with a single patch class added, so that the mod every player is required to have moves as little as possible.

Fixed

  • A grave loses the rows it was buried with. InventoryRows already widened the player's grid before Player.Load, which fixed relogging and left the worse half of the same bug standing - the items it ate were the ones you died holding.

    Vanilla's own path, in order. Player.CreateTombStone copies the player's width and height onto the grave, so the grave is born the right size and nothing is lost yet - which is exactly why looting your own grave straight away looks fine. But a grave is a Container, so its inventory round-trips through the ZDO, and Inventory.Save writes a version, a count and the items and not the height. The grid is rebuilt from the tombstone prefab's own height, which is vanilla's. Then Inventory.Load re-adds each item at its saved position through a private AddItem that ends:

    AddItem(component.m_itemData, component.m_itemData.m_stack, pos.x, pos.y);
    UnityEngine.Object.Destroy(gameObject);
    return true;
    

    The positional AddItem starts with a bounds check and returns false when y >= m_height

    • and that result is thrown away. The item is instantiated, refused, never added, and destroyed, and the method returns true regardless. Then the grave saves again without it.

    So the loss is silent and delayed. Loot the grave before its zone unloads and everything is there; relog or walk away first and the bottom row is gone. That is what made it read as random rather than as a rule.

    The fix is the shape the player one already had: open the grid up for the duration of the load, let the items land where they were, then let the contents decide the height. It is applied to every inventory rather than only to graves, because the defect is not specific to graves - any container read into a grid shorter than the one that wrote it deletes the difference, and no caller can be told apart at this level. Widening can only ever keep an item that would otherwise have been destroyed; being wrong here costs a container that draws one row too many until it is emptied.

Not in this release

Core's main branch also carries the keybind change, the move of Prefabs.cs out of this DLL into shared source, and a save-on-inventory-change guard. None of them are here. This release was cut from the 1.0.1 commit with the grave fix alone applied on top, because Core is the one mod every player must have at the same build, and a version gate is not the place to ship four things when one was asked for.

[1.0.1] - 2026-08-18

Documentation only. No code changed, and the DLL differs from 1.0.0 only in the version it reports.

Added

  • The readme says where to report a bug. It did not, in any mod here, so anyone who installed this from Thunderstore had the comment section and nothing else - which is not a route for anything that needs a log file attached. Discord first, because the common case is a player who cannot tell whether what they are seeing is a bug, a config value or vanilla, and that is a conversation rather than an issue.

[1.0.0] - 2026-08-18

Core is what the suite actually needs from it and nothing else.

Removed

  • The deed registry and the soft-reference asset loader are gone. Both were written for mods that are not in this release, and shipping the plumbing for something nobody can install is how a shared library turns into a junk drawer. They live on the deeds-and-softref branch and come back with the mods that use them, not before.
  • The SoftReferenceableAssets reference goes with them, so Core now builds against assembly_valheim, assembly_utils, four Unity assemblies, BepInEx and Harmony.

Changed

  • The README describes all three of the things this does. It described two, and the inventory height had never been written down anywhere a person would look.
  • EnforceBuilds is in the config table. It has been in the config file since 0.2.0 and missing from the documentation for exactly as long.

[0.2.0] - 2026-08-16

First published release. Earlier numbers were development only and never went out.

The version gate

  • Refuses a connection the two ends disagree about, before you have played an hour into stacks that only exist on one machine. The log names the mod and both versions, because the game's own rejection screen has no room to.
  • Compares builds, not just version strings. Two ends can both claim 1.0.0 and be running different compilations, and that is the mismatch that gets missed. The number matches perfectly and the connection is allowed.
  • Mods declare Everyone or HostOnly. Everyone is the default and the safe answer: anything registering a prefab is Everyone whether it looks like it or not, because a client that cannot resolve a prefab hash discards the ZDO as junk rather than failing loudly.
  • Core is on its own gate. It was the one mod every other mod depends on whose mismatch went unreported, and a Core mismatch is worse than any of theirs because it is the handshake itself.

Host settings

  • While you are on someone's world you play by their numbers. Your own config file is never written and never overwritten, and your values come back the moment you disconnect.
  • Mods choose which entries are synced rather than Core guessing.

Elsewhere

  • Loads on dedicated servers. It did not, which meant the one branch that can actually refuse a connection was unreachable on the only setup where it matters.
  • Owns the inventory height, so two mods can both add rows without cutting each other's off or writing before anything has claimed space.
  • Extra rows survive a reload. They did not, and the failure was total and silent: the grid is still its vanilla height when a character is read off disk, and Inventory.AddItem drops any stack whose saved position is outside the current grid, with no log and no error, after which the next save writes the inventory back without it. Rows are applied from Core's update, which cannot run until the player exists, and that is after the load. So the bottom row was destroyed on every single relog, for any item, from any mod. The grid is now opened wide before the load and trimmed back afterwards, never below the rows the items themselves occupy.
  • Runtime prefabs can be soft-referenced, matching how the game now loads its own.
  • Both behaviours are off-switchable. Neither is on by accident.

Known limits

  • The gate only sees mods that call Suite.Register. A mod in the profile that does not is invisible to it, which is by design, but it means the gate answers for this suite, not for the whole plugin folder.