Wubarrk-Valheim10Compatibility icon

Valheim10Compatibility

Lets pre-1.0 mods load and run on Valheim 1.0 without recompiling: sector API, console commands, renamed hooks, PieceTable lists, by-name Harmony patches steered past 1.0's overloads, a Hoverable default, and a shield for item-name collisions.

Last updated 2 days ago
Total downloads 1420
Total rating 0 
Categories Mods Misc Tools Server-side Client-side Utility Deep North Update
Dependency string Wubarrk-Valheim10Compatibility-1.4.1
Dependants 0 other packages depend on this package

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

โš’๏ธ Valheim 1.0 Compatibility (MigrationStation)

The Tenth World turned over, and half the realm's mods forgot how to speak to it. These bridges teach them the new tongue.

๐Ÿฐ Wubarrk lives on Hexium โ€” don't miss out

The Wubarrk and RavenIron Studios mods are maintained on valheim.hexium.gg, and that is where their current builds live โ€” the Wubarrk mods all tagged Valheim 1.0 there. The copies under the Wubarrk name here on Thunderstore are mostly the old pre-1.0 lines, several of them deprecated, and they are not kept current. If you run any of these, get them from Hexium:

Wubarrk โ€” Njord ยท Shadows of Midgard ยท Wings of the Valkyrie ยท Fatty ยท StackIT ยท TortalPortal ยท AwayFromHome ยท Let It Grow ยท RuneboundRest ยท Blighted World Heart ยท Dvergr Allies ยท Mists of Avalor ยท VikingOS ยท Get Off My Lawn ยท Wonderland ยท The Eye

RavenIron Studios โ€” WhereTheCrowFlies ยท Valkyries Cargo ยท Cairn ยท FireFront ยท Ragnarok's Wrath ยท The Raven's Call ยท Undertow ยท RavenEye

This patcher is published in both places so a Thunderstore-only profile can still carry its pre-1.0 mods across. The Wubarrk mods above don't need it โ€” they have real 1.0 builds waiting. Questions, bug reports, the clan: Discord. ๐Ÿ—

Valheim10Compatibility is a BepInEx preloader patcher for Valheim 1.0.x+ (Deep North; built against 1.0.7, verified unchanged through the 1.0.12 hotfix โ€” see CHANGELOG.md). It rewrites the game's assemblies in memory, before a single line of game code runs, and restores the method shapes that 1.0 took away โ€” so plugins compiled against 0.221.12 and earlier load and run without being recompiled. It also hooks Harmony's own lookups, so a plugin that patches a method by name survives both the bridges and the overloads vanilla added in 1.0, and gives one interface method a default body, so a plugin class that never implemented it still passes the runtime's own VTable check. Its companion runtime plugin keeps a mod's item-name collision with new 1.0 content from aborting startup on client and server alike.

This is not a gameplay mod. It changes nothing you can see. It exists for one week of the year: the week a major Valheim release lands and the mod list you depend on has not caught up yet. ๐Ÿ›ก๏ธ


โš ๏ธ Read This First

Install it when your admin tools and utility mods break on 1.0 and their authors have not shipped 1.0 builds yet. Remove it when they have. A source fix in the mod itself is always better than a bridge โ€” it survives the next update, and it does not depend on a preloader being present and correctly ordered.

Three things it will never do:

  • ๐Ÿšซ It will not guess. If a bridge cannot carry the old meaning across, it is not injected. An unresolved reference kills one method when it is first called. A wrong answer corrupts a world quietly, at three in the morning, and nobody knows why.
  • ๐Ÿšซ It will not trade a call site for a plugin. See The One Law below โ€” this is the rule that shapes the whole tool.
  • ๐Ÿšซ It will not lie about what it did. Every bridge reports exactly one outcome in the log: Injected, present natively, SKIPPED or BLOCKED. A run against a build it does not fully match is loud, not quietly thin.

๐Ÿช“ The Ritual of Installation

  1. Install BepInExPack Valheim 5.4.2350 or newer โ€” 1.0 runs on Unity 6, and older packs die when Unity's log callbacks are stripped.
  2. Install this package from valheim.hexium.gg with your mod manager, or unzip it over your profile by hand.
  3. Start the game or dedicated server and read BepInEx/LogOutput.log.

The zip carries patchers/Valheim10Compatibility.Patcher.dll and plugins/Valheim10Compatibility.Adapter.dll at its root; your manager maps each top-level folder onto the profile's BepInEx/ folder of the same name. The two DLLs land in different folders, and neither works from the other's:

File Goes to Why
Valheim10Compatibility.Patcher.dll BepInEx/patchers/ BepInEx runs preloader patchers only from patchers/, before the game assemblies load. Dropped in plugins/ it never executes, and nothing will tell you so.
Valheim10Compatibility.Adapter.dll BepInEx/plugins/ An ordinary chainloaded plugin. Dropped in patchers/ it is never loaded.

Unpacking by hand? Copy the .dll out of patchers/ into BepInEx/patchers/ and the one out of plugins/ into BepInEx/plugins/. Do not flatten them together. ๐Ÿ“ฆ

Server and clients: install it on whichever side runs the broken mod. It touches no network protocol and changes no save format, so a patched server and an unpatched client talk to each other exactly as they did before.


โš–๏ธ The One Law

A bridge is only worth injecting if it leaves the plugin better off.

Here is the trap that makes this tool different from every naive "just add the old overload back" patcher.

When a plugin writes [HarmonyPatch(typeof(Inventory), "Changed")] with no argument types, Harmony resolves that target by name alone. Add a second method with that name โ€” exactly what a compatibility bridge does โ€” and the lookup becomes ambiguous. HarmonyX throws out of PatchAll(), and the entire plugin dies at load, every patch in it, not just that one.

That is far worse than the single unresolved call site the bridge was meant to fix.

So before injecting anything, the patcher scans your real BepInEx/plugins folder, reads every [HarmonyPatch] attribute in every Harmony plugin you have installed, and records which game methods are patched by name. Without the hook below, any bridge that would collide with one is refused outright โ€” on the live server list below that blocks 10 bridges to protect 17 plugins, including ProceduralRoads and Venture LocationReset, both of which an earlier release of this very patcher broke by injecting ZoneSystem.SpawnZone(Vector2i, ...) on top of their by-name patches. ๐Ÿฉน

That protection was right, and it was also a ceiling: EffectList.Create was wanted by 11 plugins and stayed blocked because Infinity Hammer patches it by name โ€” and still does. And vanilla itself creates the same ambiguity with no bridge in sight: 1.0 gave Inventory.Load, PieceTable.SetCategory, Attack.GetAttackEitr and fifteen other methods a second overload, so pre-1.0 plugins patching those by name die on plain 1.0.7.

๐Ÿช The Harmony resolution hook

Every Harmony lookup โ€” attribute patches, AccessTools.Method(...) in code, ___field injection โ€” goes through four AccessTools entry points. The patcher installs MonoMod detours on all four from the preloader, before any plugin loads, and resolves the ambiguity the way the plugin author meant:

  • ๐Ÿšซ A by-name lookup never returns a bridge. Every bridge is tagged [Obsolete("Valheim10Compatibility bridge ...")]; the patch lands on the native method vanilla actually calls. Bridges and by-name patches coexist.
  • ๐Ÿ•ฐ๏ธ Between two native overloads, the pre-1.0 one wins. The table was generated from a Cecil diff of the last pre-1.0 build against 1.0.7: 18 names whose overload count went from one to several with the old shape still present. For the three this list hits, the decompile confirms vanilla still calls the old shape, so the patch is live, not an anchor.
  • โ†ช๏ธ A typed lookup that names a bridge's exact parameter list is redirected to the native method the bridge forwards to โ€” when the bridge merely appends defaulted parameters and Harmony's own patch machinery is the one asking (a [HarmonyPatch] attribute, a TargetMethod()). The patch then fires on vanilla's own calls. A plugin's own typed lookup gets the bridge it named, so MethodInfo.Invoke, delegates and Traverse keep the pre-1.0 arity (1.4.1 โ€” before that, Historical Heritage's AttachArmor invoke threw TargetParameterCountException); a bridge handed straight to Harmony.Patch is swapped for the native at patch time through a hook on PatchProcessor. Bridges that transform their arguments (Vector2i โ†’ Vector2s) are never redirected: Harmony binds patch parameters by name with no type check, and a Vector2i prefix parameter on a Vector2s method would be invalid IL.
  • ๐Ÿงพ A field lookup that finds an alias (see PieceTable.m_availablePieces below) is answered from the caller's own IL: a plugin compiled against the old shape gets the alias, everyone else the native field.

Anything the hook does not understand falls through to HarmonyX's own behaviour, exception included. The guard is still built at every boot: with the hook in it is advisory โ€” the log names every plugin whose by-name patch now relies on the hook โ€” and if the hook cannot be installed it blocks exactly as it would without the hook:

[Info   :Valheim10Compatibility]   by-name Harmony patch on Inventory.Changed in AzuAntiArthriticCrafting.dll,
AzuAutoStore.dll, Recycle_N_Reclaim.dll, ServerCharacters.dll: the resolution hook steers it to the native
overload, so this bridge is injected anyway.
[Info   :Valheim10Compatibility] Harmony hook: AccessTools.DeclaredMethod(Inventory, "Load") -> Void
Inventory.Load(ZPackage)  [vanilla added an overload in 1.0; the pre-1.0 shape is taken]  <- CoreWoodExtras

What the hook does not cover, on purpose: plain Type.GetMethod / Type.GetField callers. That is the lookup that killed Wonderland under an earlier release of this patcher, which is why the next rule is untouched.

๐Ÿ” The return-type rule

A second, categorical refusal, and it is worth understanding because it costs real coverage.

A pre-1.0 call site is resolved on the full IL signature, return type included, so restoring Vector2i ZoneSystem.GetZone(Vector3) next to the native Vector2s one is both legal and exactly what those call sites need. But that pair is not expressible in C#, and reflection cannot tell them apart: Type.GetMethod(name, flags, binder, types, null) selects on name and parameter types only. The moment both exist, every plugin that reflects on that name throws AmbiguousMatchException โ€” in its own lookup, at startup, taking the whole plugin with it.

That is not theoretical. An earlier release of this patcher injected ZDO.GetSector() and ZoneSystem.GetZone(Vector3) in exactly this shape, and Wonderland 0.2.0 died in Awake() on a live 1.0.7 server before initialising a single subsystem, because its compatibility probe does that lookup for GetZone.

So the patcher refuses any bridge that would differ from an existing method by return type alone. The cost is real and measured: ZDO.GetSector() and ZoneSystem.GetZone(Vector3) call sites stay broken (Procedural Roads, on this list). The alternative was those plugins not starting at all.

Everything the guard decides is decided per install, from your plugin folder. A different mod list gets a different answer.


๐Ÿ“Š Tested Against Live Modded Server Lists

Measured against the plugin sets of two real, running modded Valheim servers โ€” not curated samples: four Azumatt inventory mods, Jotunn dependants, JereKuusela admin toolkits and fifteen-plus build-piece packs, warts and all.

Static โ€” every game reference and every Harmony target in 74 real third-party plugins, re-resolved against the patched assemblies (libs-Tools/refcheck, with the hook modelled). The server's profile was only ever read.

Plugins fully clean Unresolved game references Broken Harmony patch targets
๐Ÿ”ด Vanilla, no patcher 37 / 74 139 36
๐ŸŸข With the patcher 65 / 74 37 5

Live โ€” the same real plugin lists, booted on the real 1.0.7 Linux dedicated server, patcher out and patcher in:

Log signature ๐Ÿ”ด Vanilla ๐ŸŸข With the patcher
Could not find method (a typed patch target is gone) 10 0
A patch lost at PatchAll() (Failed to patch / HarmonyException) 2 2, different ones
TypeLoadException: VTable setup (an interface gained a method the plugin's own class never implemented) 36 lines / 14 types / 6 plugins 0
AmbiguousMatchException actually thrown 0 0

Not one plugin is worse off than with no patcher at all. โœ… The two lost patches without the patcher are Mists of Avalor's no-build protection (the bridge plus the redirect bring it back) and Advanced Terrain Modifiers' PaintCleared prefix (names three parameters 1.0 removed โ€” identical either way). The one that appears with the patcher is a SearsCatalog transpiler whose IL pattern no longer exists in Player.UpdateBuildGuiInput โ€” SearsCatalog never got that far before, because its SetCategory postfix was ambiguous and the plugin applied nothing, silently. The TypeLoadException row is CoreWoodExtras, RustyBuildPieces, SeedBed, Historical Heritage, Ravenwood Random Relics and Procedural Roads, isolated with an A/B on one fixed mod list and binary (only the fix's own config switch toggled) so the count is attributable to the fix alone. Full per-plugin detail and methodology: CHANGELOG.md. 1.4.1 closed the one exception found since: 1.3.0โ€“1.4.0 redirected Historical Heritage's typed AttachArmor lookup to the 3-parameter native and its Invoke threw where plain 1.0 returned null; the hook now hands a plugin's own reflection the bridge it named.


๐Ÿ“œ What the Bridges Restore

54 bridges, injected into assembly_valheim.dll and assembly_utils.dll, each tagged [Obsolete("Valheim10Compatibility bridge -> Target(...)")] so other tooling can tell a bridge from a native method by reflection, and so the hook can find the native method a typed lookup belongs on. (One of the 54 โ€” the Hoverable default interface method below โ€” isn't a forwarding bridge in the usual sense, but it is counted in the patcher's own summary line the same way.)

๐Ÿงญ The sector migration (Vector2i โ†’ Vector2s)

1.0 moved zones and sectors from 32-bit Vector2i to 16-bit Vector2s. Bridged as conversion wrappers: ZoneSystem.GetZonePos, PokeLocalZone, IsZoneGenerated, IsZoneLoaded, ZNetScene.HaveInstanceInSector.

ZDO.GetSector() and ZoneSystem.GetZone(Vector3) are not bridged, because their old and new forms differ by return type alone โ€” see the return-type rule below.

๐ŸŒ The sector sweep โ€” the one that matters most

ZDOMan.FindSectorObjects is what AwayFromHome, Let It Grow, TortalPortal, HearthBelow and Wonderland all call to find what exists near a point. 1.0 collapsed its two int distance parameters into a SimulationDistance struct and deleted ZoneSystem.m_activeArea outright. The bridge rebuilds the struct from the old arguments with classic: true โ€” that flag is not optional: with it false, vanilla filters each ring through ZoneSystem.ZonesWithinRadius and the swept set becomes a disc instead of the square ring every pre-1.0 caller is written against.

FindObjects and FindDistantObjects gained a visit-tracking HashSet. The bridges pass the game's own set and clear it first, exactly as vanilla does โ€” miss that and the second call in a session returns nothing at all, because every sector is already marked visited.

๐Ÿงฎ ZDOMan.SectorToIndex โ€” re-examined and recovered

Previously written off as unbridgeable, on the reasoning that it indexed m_objectsBySector through the removed m_halfWidth. Re-checked against 1.0.7: ZDOMan is still built as new ZDOMan(512) with m_objectsBySector = new List<ZDO>[512 * 512], and ZoneSystem.SectorToIndex computes (y + 256) * 512 + (x + 256) โ€” byte-for-byte the arithmetic the old method did. The only behavioural difference is the out-of-range answer: the old contract returned -1, the new one returns index 0, which is also a legal sector. The bridge reproduces the old contract exactly, -1 included, so callers that range-check against m_objectsBySector.Length keep working. This is what brings back resetdungeon.

๐Ÿ–ฅ๏ธ The console-command surface

The admin-tool bridge. 1.0 inserted bool hideBehindDevCommands into Terminal.ConsoleCommand's constructor and gave ConsoleEventArgs a third parameter. Every command every admin mod registers goes through these two, which is why breaking them takes whole toolkits down at once. Both constructors bridged.

๐Ÿ”€ Reordered, not just retyped

ZNetScene.InActiveArea went from (Vector2i zone, Vector3 refPoint) to static (Vector3 point, Vector2s centerZone). The first argument used to be a zone and is now a world point โ€” swapping the arguments would compile, run, and answer confidently wrong. The bridges convert properly, through ZoneSystem.GetZonePos.

โž• The appended-parameter drift

C# bakes default-argument values in at the call site, so a method that merely gained an optional parameter still breaks every plugin compiled before it. This is the most common breakage on the list, and the quietest:

Character.Message (43 of 87 plugins) ยท MessageHud.ShowMessage ยท SEMan.AddStatusEffect ร—2 ยท EffectList.Create (11 plugins) ยท four Inventory.AddItem overloads ยท Inventory.Changed ยท ItemDrop.SaveToZDO / LoadFromZDO / OnCreateNew ร—2 ยท ItemData.GetTooltip ยท Heightmap.Poke ยท TerrainComp.Save ยท WorldGenerator.GetBiomeHeight ยท YesNoPopup..ctor ยท Piece.SetCreator ยท Player.PlacePiece ยท ZoneSystem.SpawnLocation ยท VisEquipment.AttachArmor ยท Humanoid.IsTeleportable ยท ZInput.mousePosition

Also Inventory.IsTeleportable() ยท CensorShittyWords.FilterUGC ยท Game.SavePlayerProfile ยท CookingStation.SetSlot / GetSlot (1.0's new out bool cheated is discarded into a local) ยท SlowUpdate.SUpdate(float, Vector2i) (callvirt, so Plant and StaticPhysics overrides win) ยท and PlayerProfile.GetCharacterFolderPath(FileSource), which moved to SaveSystem and whose argument changed numbering โ€” FileHelpers.FileSource went from 0/1/2/3 to flags 1/2/4/8, so the bridge computes 1 << old before forwarding rather than answering the Cloud path for a Local request.

ZRoutedRpc.Everybody is de-literalized from a const to a plain static field, so ldsfld int64 ZRoutedRpc::Everybody instructions from ServerSync and older mods (AzuAntiCheat, SeedBed) resolve without throwing MissingFieldException.

Hoverable.GetHoverOffset() is given a default body โ€” see the default interface method below.

๐Ÿงฑ The PieceTable.m_availablePieces alias

1.0 renamed the per-category piece lists to private m_availablePiecesByCategory and reused the old name for a flat HashSet<Piece>. Fifteen build-piece mods on the list read the old field โ€” to count tabs, index them, or add a list for a custom category. The alias is a second field with the old name and type, assigned in the constructor to the same list object, so the two views cannot diverge. IL binds fields on name and signature, so vanilla and 1.0-built plugins keep the HashSet and pre-1.0 plugins get the lists.

Reflection is the hazard โ€” two fields, one name. The field hook handles AccessTools and ___field consumers; a guard scan refuses the alias outright if any installed plugin passes the literal "m_availablePieces" to Type.GetField or Traverse.Field.

โš ๏ธ The caveat: 1.0 inserted PieceCategory.DeepNorth = 5, pushing Feasts/Food/Meads to 6/7/8. A pre-1.0 mod bakes those numbers at compile time, so a piece it files under old Feasts lands in the DeepNorth tab, and a custom tab created at old Max (8) shares the Meads list. Wrong tab, not a crash. If your build menu looks wrong, turn the alias off (see Configuration).

๐ŸŽญ The default interface method

How it works. Hoverable is an interface, not a class, and 1.0 added float GetHoverOffset() to it โ€” pre-1.0 it only declared GetHoverText() and GetHoverName(). The CLR requires every interface member to have a filled VTable slot before a type can be constructed, so a pre-1.0 class implementing Hoverable without that member fails TypeLoadException: VTable setup the instant its assembly is scanned โ€” not when the missing method is called, but the moment Harmony (or anything else) reflects over that assembly at all, taking every patch in it down together.

Every vanilla call site already reads the interface defensively โ€” component?.GetHoverOffset() ?? 0f โ€” so instead of leaving the member abstract, the patcher gives Hoverable.GetHoverOffset() itself a body: return 0f;. That is a default interface method, the same feature C# 8 formalized (ECMA-335 6th edition), applied at the raw IL level with Cecil rather than through the compiler. A class that never overrides the member is no longer required to โ€” it inherits the default and passes VTable setup. A class that does implement it (every 1.0-built one) is untouched; its own override still wins, exactly as with any other interface method.

What we tested it with. The uncertainty here wasn't the IL โ€” it was whether Valheim's embedded Mono runtime (Unity 6000.0.75f1) actually honours a default interface method injected this way, since nothing in this patcher had exercised that CLR feature before. Confirmed live with a controlled A/B on a real dedicated-server plugin list (91 third-party plugins), same binary, only the config switch toggled between boots โ€” chart in Tested Against Live Modded Server Lists above, full methodology in CHANGELOG.md. Config: [Bridges] HoverableGetHoverOffsetDefault, default true.

โš ๏ธ Procedural Roads is a partial case. Its VTable crash is gone โ€” it now loads and applies its own patches โ€” but it separately calls ZoneSystem.GetZone(Vector3) expecting the pre-1.0 Vector2i return, which stays refused under the return-type rule. Better off than before, not fully fixed.

๐Ÿ•ฏ๏ธ The three patch anchors โ€” read this one

Container.RPC_OpenRespons, Container.RPC_TakeAllRespons and Minimap.OnMapRightClick were renamed or replaced by a lambda in 1.0, and plugins patch all three by name, so their absence kills those plugins outright at PatchAll().

These three bridges restore the name so the plugin loads. They do not restore the behaviour: vanilla calls the renamed method directly, so a prefix attached to an anchor never fires. The plugin works; that one feature is inert.

This is the only place in the entire patcher where a name comes back without its meaning, it is a deliberate trade of one dead feature against one dead plugin, and each anchor says so in its own log line. ๐Ÿ•ฏ๏ธ


๐Ÿšซ What Is Deliberately Not Bridged

Absence here is a decision, not an oversight.

  • Field type and shape changes โ€” ZoneSystem.m_zones and m_locationInstances (generic arguments changed), VisEquipment's eleven m_*Item slots (string โ†’ int hash), ItemStand.m_visualName, Minimap.m_explored (bool[] โ†’ BitArray), Player.m_knownBiome, Ship.m_sailCloth, VisEquipment.m_clothColliders, PlayerProfile.m_playerStats. A field's declared type cannot be patched around โ€” the one exception is PieceTable.m_availablePieces, where the old data still exists under a new name and an alias can point at it. The rest have to recompile.
  • Enum renumbering โ€” FileHelpers.FileSource (0/1/2/3 โ†’ flags 1/2/4/8) and Piece.PieceCategory (DeepNorth inserted at 5). A pre-1.0 plugin bakes the old numbers into every call site, and the methods it passes them to still exist, so nothing fails to resolve and nothing can be intercepted. Only the one method that moved (GetCharacterFolderPath) gets a remapping bridge.
  • ZoneSystem.m_activeArea / m_activeDistantArea โ€” deleted in favour of SimulationDistance, which is now player-configurable and synced from the server. A field could be injected, but it would read zero forever, and a plugin sweeping a radius of zero silently does nothing. That is worse than the load-time failure it gets now.
  • ZDO.SetSector, ZDOMan.GetSaveClone, ZDOMan.GetPortals โ€” the storage model behind all three changed (SectorIndex, chunked saves). 1.0 still has a parameterless GetPortals returning a different type, so re-adding the old one would differ by return type alone: legal IL, not expressible in C#, and ambiguous for every AccessTools.Method consumer that touches it. The previous release's GetSaveClone stub returned an empty list โ€” that is, it told any mod that asked that the world contains zero ZDOs โ€” and it is gone.
  • ZNetScene.InActiveArea(โ€ฆ, int) and OutsideActiveArea(โ€ฆ, int) โ€” both took an explicit ring radius that 1.0 removed entirely. No forwarding preserves the caller's intent.
  • ZDO.GetSector() and ZoneSystem.GetZone(Vector3) โ€” refused under the return-type rule. Bridging them breaks reflection for every plugin that looks either name up.
  • Transpilers whose IL pattern is gone โ€” a transpiler searches the target's body for a specific instruction sequence; when 1.0 rewrote that body the search fails inside the plugin (SearsCatalog's UpdateBuildGuiInput, on this list). Nothing outside the plugin can restore an IL pattern.
  • ZDOExtraData's seven Get*(ZDOID) accessors โ€” replaced by a single GetData(ZDOID, out โ€ฆ). Restoring them needs a generic-method Cecil bridge per type; only World Edit Commands uses them, and it is broken for other reasons anyway.

๐Ÿ” Reading the Log

[Info   :Valheim10Compatibility] Ambiguity guard: scanned 84 Harmony plugins under '.../BepInEx/plugins' in 1892 ms; 779 by-name patch targets recorded; 17 plugin(s) reference a pre-1.0 field shape this patcher aliases.
[Info   :Valheim10Compatibility] Harmony resolution hook installed on HarmonyX 2.9.0.0 (AccessTools.DeclaredMethod, Method, DeclaredField, Field; PatchProcessor..ctor); ...
[Info   :Valheim10Compatibility] Injected ZDOMan.FindSectorObjects(Vector2i, int, int, List<ZDO>, List<ZDO>) -> SimulationDistance(area, distantArea, classic: true)
[Info   :Valheim10Compatibility]   by-name Harmony patch on EffectList.Create in InfinityHammer.dll, MoreVanillaBuildPrefabs.dll: the resolution hook steers it to the native overload, so this bridge is injected anyway.
[Info   :Valheim10Compatibility] Injected EffectList.Create(Vector3, Quaternion, Transform, float, int)
[Warning:Valheim10Compatibility] BLOCKED ZoneSystem.GetZone(Vector3) -> Vector2i: this would differ from the existing Vector2s GetZone(...) by RETURN TYPE ALONE ...
[Info   :Valheim10Compatibility] assembly_valheim: 52 bridge(s) injected (12 of them beside a by-name Harmony patch that the resolution hook steers past them), 0 already native, 0 skipped (no target on this build), 2 blocked (0 would break a by-name Harmony patch or a reflective field lookup, 2 would be a return-type-only overload).
[Info   :Valheim10Compatibility] Harmony hook: AccessTools.DeclaredMethod(Inventory, "Changed") -> Void Inventory.Changed(Boolean, Boolean)  [1 bridge overload(s) excluded]  <- AzuAutoStore

The summary line is the one to read.

Outcome Means
Injected The bridge is in. Callers resolve.
present natively The running 1.0.x build already has this shape. Nothing to do.
SKIPPED โš ๏ธ The target shape was not found. On 1.0.7 through 1.0.12 this is zero โ€” anything else means the game changed again and this tool needs re-checking against the new build.
BLOCKED Refused on purpose: it would differ from an existing method by return type alone, a plugin reflects on the aliased field by string, or โ€” only when the hook is not installed โ€” it would break a by-name Harmony patch. The line names the plugins it protects.
by-name Harmony patch on ... Advisory: that plugin's patch now depends on the hook. If you ever turn the hook off, these become BLOCKED.
Harmony hook: ... One line per resolution the hook made, saying what was asked, what was answered, why, and by whom.

If the plugin folder cannot be found, the guard says so plainly and every overload bridge proceeds unprotected. Set VALHEIM10COMPAT_PLUGIN_PATH for nonstandard layouts. If the hook line reads NOT installed, the patcher has fallen back to refusing every colliding bridge outright, and says so.


โš™๏ธ Configuration

BepInEx/config/Valheim10Compatibility.cfg is written on first run. Five switches, all on by default; each exists so you can turn a whole mechanism off from a text file if it misbehaves on your list, without removing the patcher. The first four are the patcher's; the fifth is read by the runtime adapter from the same file:

Key Default Off means
[Harmony] ResolutionHook true Every bridge beside a by-name patch is refused, and pre-1.0 plugins patching Inventory.Load / SetCategory / GetAttackEitr by name die at PatchAll() as on plain 1.0.7.
[Bridges] PieceTableAvailablePiecesAlias true The fifteen build-piece mods lose their piece lists again; use it if the category caveat above bites.
[Bridges] ZRoutedRpcEverybodyStatic true ServerSync-based mods (AzuAntiCheat, SeedBed, ...) that read ZRoutedRpc.Everybody throw MissingFieldException again.
[Bridges] HoverableGetHoverOffsetDefault true Pre-1.0-compiled classes implementing Hoverable without GetHoverOffset() throw TypeLoadException: VTable setup again, taking their whole assembly's Harmony patches down with them.
[Shields] ObjectDBDuplicateItems true A mod whose custom item name collides with new 1.0 content (More Ore Deposits' GoldOre) aborts FejdStartup.Start again โ€” server never loads a world, client menu spams NullReferenceException โ€” exactly as on plain 1.0.

๐Ÿ›ก๏ธ The Runtime Adapter

A small companion plugin (BepInEx/plugins/) handling what a preloader cannot:

  • ๐Ÿ’พ Pre-migration world backups โ€” copies <World>.db and <World>.fwl before 1.0 converts a monolithic save into chunked format. It converts once and does not convert back.

  • ๐Ÿงฑ PieceTable category capacity shield โ€” vanilla uses fixed selection arrays sized to PieceCategory.Max and throws IndexOutOfRangeException when custom categories push past it. The adapter resizes them, reading m_availablePiecesByCategory directly.

  • ๐Ÿ–ผ๏ธ Hud.UpdatePieceList finalizer โ€” keeps one outdated build-menu mod from taking the HUD down with it.

  • ๐Ÿ“ฆ ObjectDB content-collision shield โ€” ObjectDB.UpdateRegisters builds its item table with Dictionary.Add, so two item prefabs sharing a name throw out of FejdStartup.SetupObjectDB and startup aborts on client and server alike. Vanilla's own list is unique; the duplicate comes from a mod registering an item under a name 1.0 now uses itself, and Jotunn lets it through (its AddItem checks the menu ObjectDB's hash table before the Jotunn hook that fills it has run). The adapter drops the later duplicate before vanilla's loop โ€” the first registration wins, which is vanilla's whenever vanilla owns the name โ€” and logs which item, which mod token, and why. It only ever acts where vanilla would have thrown.

    Measured on the 91-plugin dedicated-server list with More Ore Deposits 1.3.5 enabled, same binary, only [Shields] ObjectDBDuplicateItems toggled: off โ€” ArgumentException: An item with the same key has already been added. Key: 205000016 at SetupObjectDB, no world ever loads; on โ€” the world scene comes up, the shield fires exactly once (GoldOre: kept vanilla's $item_goldore, dropped the mod's $GoldOre_warp), Jotunn injects the mod's five deposits, and the only [Error] lines are the same two every release has had. The shield keeps the game up; it does not make the colliding mod right for 1.0. More Ore Deposits' gold deposits then drop vanilla's Deep North gold ore, and its Smelter.Spawn ร—20 prefix applies to vanilla's blast furnace (GoldOre โ†’ Gold) too. Read the warning line, then ask that mod's author for a 1.0 build.


๐Ÿ“š Changelog

Every release, what it changed and what was measured, newest first: CHANGELOG.md, shipped alongside this file.

โš–๏ธ License

MIT โ€” see LICENSE.md. The preloader modifies Valheim's assemblies in memory only; no game file on disk is altered, and none is redistributed here.


Forged in the Wubarrk halls. Every Wubarrk mod lives on valheim.hexium.gg.

May your mod list survive the update. ๐Ÿ—