Releases: SphereII/SphereII.Mods
Release list
3.1.9.1528
`Version: 3.1.9.1528
Game Version: v3.1.0 (b13)
[ EntitySyncUtils - ItemValue Stats And Mod Serialization ]
- ItemValue gained a Stats array (ItemValue.Stat[]) holding special
modifiers. EntitySyncUtils serializes an NPC into the pickup item's
metadata when a companion is collected, so anything it does not write
is lost when that NPC is put back down. Stats were not written, so a
collected NPC's gear came back stripped of them.
- ItemValue.Stat is a struct of (PassiveEffects type, bool isBoosted,
short value). Its constructor takes (_type, _base, _added) but stores
only the sum and "was anything added", so the base/added split cannot
be recovered. The three fields are round-tripped directly rather than
through the constructor, which would have meant inventing a split.
- Mods were serialized as a bare item name, so a modded weapon returned
with default mods: the mod's own quality, durability and Stats were
all discarded. A mod now carries all four.
- Mods are also written positionally. Empty slots used to be skipped,
so [scope, empty, silencer] came back as a two element array with the
silencer at index 0. An empty slot is now an empty entry, preserving
both array length and mod indexes. An item whose slots are all empty
still serializes as an empty field, exactly as before.
- Mod ItemValues are rebuilt with the (id, _bCreateDefaultParts: false)
constructor so the restored state is not overwritten by default parts.
[ Slot string format ]
- Each slot gained a sixth comma separated field:
name,count,quality,useTimes,mods,stats
- Separator hierarchy, outermost first. Mods nesting their own stats
means every level needs its own character:
';' slots
',' fields within a slot
'|' mods within the mods field
'@' fields within one mod: name@quality@useTimes@stats
'~' stats within any stats field (a slot's own, or a mod's)
':' fields within one stat: type:isBoosted:value
- Backward compatible in both directions. Strings written before this
change have five fields and a name-only mods list; every field past
the first is optional on read, and Stats is left null, which is the
state a freshly constructed ItemValue already carries. An older build
reading a new string ignores the extra field.
- An effect name this build no longer knows is dropped rather than
parsed loosely: a failed Enum.TryParse would leave the type at
PassiveEffects 0, which is a real effect, and would silently apply
the wrong modifier to the restored gear.
[ Bug fixed while extending the format ]
- UseTimes and Quality were written and parsed at the current culture
inside a COMMA delimited field. On any locale that formats decimals
with a comma (de-DE, fr-FR, most of Europe), a UseTimes of 1.5 was
written as "1,5", which split into two columns and corrupted every
field after it in that slot - mods included. All numeric writes and
parses in EntitySyncUtils are now CultureInfo.InvariantCulture. This
affected the existing item level fields, not only the new mod ones.
[ LockPicking - A Successful Pick On A Cop Car Set Off The Alarm ]
- Picking a cntPoliceCar01 open left the alarm variant behind. The car
became cntPoliceCar01AlarmLocked - jammed, loot list policeCarsBonus,
policeCarAlarmPrefab - instead of the cntPoliceCar01PickedLockBonus
it had correctly been downgraded to a moment earlier. Nothing was
wrong with the block definition; it is what vanilla ships. The alarm
was also not DowngradeEvent firing, which only runs from
Block.OnBlockDamaged. The alarm block was simply being placed.
- XUiC_PickLocking.OnClose ran twice per pick. Its own last lines call
windowManager.Close(ID), and GUIWindowManager.Close ->
XUiWindowGroup.OnClose -> Controller.OnClose re-enters the method
that called it.
- The second pass still saw the lock as solved. SphereLocks.Disable
only did SetActive(false); Keyhole.LockComplete() was untouched, and
GetComponent still resolves on an inactive object, so IsLockOpened()
kept returning true. ResetLock() only ever ran from Enable().
- By then LockedFeature had been cleared, so the guard that keeps the
generic block level downgrade out of the TEFeatureLockPickable path
no longer applied, and that downgrade overwrote the block the
feature had just placed. onSelfLockpickSuccess fired twice as well.
- Two fixes. SphereLocks.Disable now calls Keyhole.ResetLock() before
deactivating, so a closed window never reports a solved lock; the
null check on the component matters, because Init() calls Disable()
before the Keyhole is attached. XUiC_PickLocking gained a `closing`
flag around OnClose's body, in a try/finally so the non main thread
early return still clears it - the re-entrant call now runs
base.OnClose() and returns.
[ Block.LockpickDowngradeBlock is dead in V2 ]
- That downgrade preferred currentBlock.Block.LockpickDowngradeBlock
and fell back to DowngradeBlock. The field still exists on Block,
but nothing parses it from XML any more - the only
"LockPickDowngradeBlock" string left in Assembly-CSharp belongs to
TEFeatureLockPickable - so it is always air, and the fallback could
only ever resolve to DowngradeBlock. On a block whose DowngradeBlock
is a damage state (cars, safes) that is the wrong block to place on
success. The dead branch has been removed.
- What remains of that swap now only runs on the ILockable path,
secure doors and containers, where SetLocked(false) has already done
the unlocking. It is A21 era leftover and worth revisiting - a
secure wood door's DowngradeBlock is its damaged variant - but it
was left alone rather than changing door behaviour as part of a cop
car fix.
[ Config - Two Region File Guards Now Ship Off ]
- RegionFileOptimizeLayoutLock and WarnRenderMapLiveServer now default
to false in Config/blocks.xml. Neither patch changed; they are opt
in now, and setting either back to true restores the previous
behaviour exactly.
- RegionFileOptimizeLayoutLock serializes RegionFileV2.OptimizeLayout
against chunk reads and writes. Vanilla runs that compaction without
the region file's instance lock, so a chunk load overlapping it can
read garbage ("EXCEPTION: In load chunk" / "Wrong chunk header!")
and the chunk is then deleted and regenerated. Off by default now
puts it in line with RegionFileLoadChunkLock, the other region file
lock, which has always shipped off.
- WarnRenderMapLiveServer is dedicated server only and log only. It
warns when rendermap or the web map's full render runs while a game
is loaded, which opens a second region file reader over the live
save and can tear chunk reads. Turn it on when diagnosing chunks
that keep coming back regenerated on a server that renders maps.
`
3.1.8.1459
*** MIGRATION NOTICE - POI DESIGNERS - PATHING AND SPAWN CUBE CONFIGURATION ***
Existing pathing cube, spawn cube and sign portal configuration is NOT
carried forward. Plan on re-entering it.
These blocks keep their programming (task=, buff=, pc=, ec=, eg=, Location)
in their sign text, and the sign text lives in the block's tile entity.
Through 3.0 they extended BlockSign, which creates no tile entity at all, so
there was nowhere for that text to be stored or saved. Any cube placed - or
any POI opened and re-saved - while that was the case has no sign data left
to recover. Nothing in SCore can reconstruct it; the text was never written.
What this means in practice:
- Walk your POIs, re-enter the configuration on each cube, and re-save
the prefab. A blank cube is silent: it now loads cleanly and applies
nothing, so NPCs spawn with no orders rather than logging an error.
- One case does survive automatically. A prefab still carrying
pre-3.0 legacy sign data has that data migrated into the new
composite tile entity on load, which the old block class refused to
do. If a POI has not been re-saved since before 3.0, check it before
replacing anything - its cubes may already be intact.
- Check the cubes themselves as well as their text: off the BlockSign
base they no longer stamp a phantom duplicate one cell above, so a
cube declared "1,1,1" now genuinely occupies one cell.
Version: 3.1.8.1454
Game Version: v3.1.0 (b13)
[ ErrorHandling - FixOrphanedPoweredTileEntities Deleted Working Power Sources ]
- Field report: placing a generator and switching it on logged
"dropped an orphaned powered tile entity ... (block is not powered)",
then "TileEntityPowerSource not found", then an unhandled
NullReferenceException in TileEntityPowerSource.CurrentFuel while the
power source window was binding. The tile entity was not corrupt; the
guard deleted it.
- Root cause: both halves of the guard tested "block is BlockPowered".
BlockPowerSource does not derive from BlockPowered - it derives
straight from Block - so BlockGenerator, BlockSolarPanel and
BlockBatteryBank all failed the test and were treated as orphans.
SCore's own BlockPoweredWorkstationSDX (a BlockWorkstation hosting a
TileEntityPoweredWorkstationSdx) had the same shape and was equally
exposed.
- The Chunk.save sweep removes from the chunk's live tile entity list,
not from a serialization copy, so this was not merely "skip writing
it" - the tile entity was destroyed in the running game. That is why
the UI threw immediately afterwards: the window had bound to a tile
entity that had just been removed underneath it.
- Both guards now share OrphanedPoweredTileEntity.IsOrphaned, which
tests Block.HasTileEntity - the engine's own "this block owns a tile
entity" flag, set in the constructors of BlockPowered,
BlockPowerSource and BlockWorkstation alike. That matches the actual
corruption case (a block overwritten raw by a decoration pass, so the
position is air or plain terrain and owns no tile entity at all) and
needs no class whitelist kept in sync as blocks are added.
- The predicate is deliberately conservative: anything that claims a
tile entity is left alone even if the pairing looks odd. Leaving a
questionable pair in place is no worse than vanilla, while dropping
it destroys player equipment.
- Corrected an inaccurate comment on the load-side guard: it claimed
CreatePowerItem casts the block to BlockPowered and throws. It does
not - it dispatches on PowerItemType, and there is no
"castclass BlockPowered" anywhere in Assembly-CSharp. The real path
is InitializePowerData leaving PowerItem null and CheckForNewWires
then dereferencing it.
- DATA LOSS WARNING: any generator, solar panel or battery bank already
swept in an earlier session is gone from the save. The block remains,
but its fuel and wiring state are not recoverable by this fix.
[ NPCs - Pathing Cube Configuration Not Being Read ]
- Follow-up to the composite tile entity migration below. That work was
a prerequisite, not a regression: while the cubes extended BlockSign
they had no tile entity at all, so every
"GetTileEntity(pos) as TileEntityComposite" in the scan returned null
and SetupAutoPathingBlocks bailed before reading anything. The read
path could not have worked in 3.0 under the old base class. These are
the bugs that were sitting behind it.
- PathingCode latched permanently on an empty cube. The method set
PathingCode to -1 before checking whether the sign had any text, and
the guard at the top treats any non-zero code as "already
configured". A cube whose text had not synced yet (client) or had
been lost with the old tile entity therefore locked the NPC out of
every later re-scan - permanently, since the cvar is saved with the
entity. Now bails before writing anything when the sign is empty.
Fixed in EntityAliveSDX, EntityAliveSDXV4 and EntityEnemySDX;
EntityNPCBandit already had the guard and needed no change.
- The PathingBlocks entity class property was silently ignored.
EntityUtilities.ConfigureEntityClass resolved the entity through
World.GetEntity(entityId), but SetupAutoPathingBlocks runs from
PostInit and from the spawn cubes, both before the entity is in the
world. The lookup returned null, the list came back empty, and the
hardcoded { PathingCube, PathingCube2 } default was used every time.
Added an EntityAlive overload and pointed all four callers at it; the
int overload now delegates to it and is still used by the callers
that genuinely run after the entity is in the world.
[ NPCs - Runtime Pathing Code Waypoint Scan ]
- ModGeneralUtilities.ScanForTileEntityInChunksListHelper is the other
half of the feature: SetupAutoPathingBlocks stores the code, this is
what EAIWanderSDX and the Utility AI patrol tasks call through
EntityUtilities.GetNewPositon to pick the next waypoint. It had never
been updated for the "task=Wander;pc=3" sign format and was only
reachable now that pathing cubes have composite tile entities.
- A non-zero pathing code could never match. The matcher ran
code.ToString() against text.Split(','), which expects the sign to be
a bare comma-separated code list ("3,4"). "task=Wander;pc=3" is a
single element and never equals "3", so an NPC with a code assigned
found no waypoints at all. Now parsed with PathingCubeParser, with a
fallback to the bare list so cubes authored in the older format keep
working.
- The block name filter was commented out, so lstBlocks was accepted,
defaulted and then never used. While cubes had no tile entity this
was masked; now that they are composites, every vanilla sign in the
surrounding nine chunks also carries a TEFeatureSignable and would
have been treated as a waypoint. Restored as a live check, with an
ischild skip.
- Null-guarded signText, matching the setup half.
- Unchanged and worth knowing: maxDistance still defaults to -1 and
GetNewPositon does not pass one, so the scan covers all nine chunks
(roughly 144x144 blocks). Far less harmful now that only pathing
cubes qualify, but an NPC can still choose a distant waypoint.
3.1.6.1010
Version: 3.1.6.1010
Game Version: v3.1.0 (b11)
[ EntityAliveSDX ]
- Fixed an issue where sphereii was an idiot and set_Title to not implemented.
3.1.3.1715
Version: 3.1.3.1715
- Fix run-time linking against 3.1.x
[ ErrorHandling - Legacy Prefab Sign Migration Null Chunk Guard ]
- Loading a prefab containing a legacy-format sign threw a
NullReferenceException and the game skipped ALL of that prefab's
active block data ("Skipping loading of active block data for
<prefab>"), so its signs, containers and other tile entities
never loaded.
- Root cause is a vanilla bug in the sign-to-composite migration:
Prefab.readTileEntities -> TileEntityLegacyUtils
.ReadLegacySignIntoComposite builds a chunkless TileEntityComposite
and immediately calls SetOwner -> SetModified -> setModified, which
(on a host) builds a NetPackageTileEntity. That reads
TileEntity.blockValue - a computed property, this.chunk.GetBlock(..)
- and the chunk is null during a prefab read. Vanilla forgets to
set bDisableModifiedCheck on this path the way its other chunkless
construction paths do.
- New prefix on TileEntity.setModified returns early when the tile
entity has no chunk. A chunkless TE only exists mid-read and has no
chunk-scoped context to broadcast (SetChunkModified already
null-guards chunk), so skipping it is safe and lets legacy prefabs
load their tile entities instead of dropping them. Fixes every
legacy prefab, not just ones re-saved in the current editor.
- New ErrorHandling property in blocks.xml:
FixLegacyTileEntityNullChunk (default true).
[ ErrorHandling - Orphaned Powered TE: Save-Side Sweep + Spawn Cube Hardening ]
- Follow-up to the load-side guard below. Root-caused the orphan's
origin: powered tile entities are NOT stored in prefabs
(IsTileEntitySavedInPrefab is false for BlockPowered), so vanilla
regenerates them from the block via OnBlockAdded during
Prefab.CopyIntoLocal - on the generation thread. The mismatch is
created when the spawn cube's block later leaves without its TE
(a decoration overwrite during generation, or the cube's own
self-destruct), and the corrupt {non-powered block + trigger TE}
is written straight to the region file at generation time, before
the chunk is ever loaded. That is why it reproduces on brand-new
worlds.
- Save-side sweep: a prefix on Chunk.save drops orphaned powered TEs
from the tile entity list before serialization, so a chunk
corrupted during generation is never written to disk or sent to
clients. Combined with the load-side guard (which heals chunks
already corrupt on disk), new corruption stops being produced and
old corruption is repaired on contact. Removal is a plain
collection drop with no world side effects, safe on the save
thread. Reuses the FixOrphanedPoweredTileEntities flag.
- BlockSpawnCube2SDX self-destruct hardening: DestroySelf now removes
its TileEntityPoweredTrigger explicitly (on the main thread, via a
deferred task if called off-thread) BEFORE damaging the block, so
the trigger TE and the block can never desync even if the block
change downgrades to a non-powered block or races a background
save. The "keep" path returns early and leaves the cube (and its
valid TE) in place.
[ ErrorHandling - Orphaned Powered Tile Entity Chunk Protection ]
- Field reports (bdubyah, Thee_Legion): "EXCEPTION: In load chunk"
with "Specified cast is not valid" out of
TileEntityPoweredTrigger.CreatePowerItem when loading chunks
containing NPC spawn cube POIs, on NEW worlds with all prior
fixes installed.
- Root cause: CreatePowerItem casts the block at the tile entity's
position to BlockPowered. A powered trigger TE can outlive its
block - POI decoration overwrites blocks raw without firing
OnBlockRemoved, so a TE created when a prefab placed a spawn cube
(BlockMotionSensor.OnBlockAdded) survives at a position whose
block is now air or unpowered. The cast throws during Chunk.read
and ChunkSnapshotUtil.LoadChunk responds by DELETING the entire
chunk - POI, player builds and NPCs included. The 3.0.31 spawn
cube fix closed the SetBlock-routed paths but could not cover raw
decoration overwrites, which never notify the block.
- New prefix on TileEntityPowered.InitializePowerData: if the block
at the TE's position is not a BlockPowered, power init is skipped
(a null PowerItem is a state vanilla already tolerates - client
side powered TEs never have one), a warning names the position and
block, and the orphaned TE is removed on the main thread so it is
not saved again. The chunk loads normally. Covers every powered TE
type and any mod's stale powered tile entities, not just SCore's
spawn cubes.
- New ErrorHandling property in blocks.xml:
FixOrphanedPoweredTileEntities (default true).
3.0.40.1018
Version: 3.0.40.1018
Game Version: v3.0.1 (b4)
Re-build 3.1.6.1010 against 3.0.1 stable.
3.0.36.1849
Version: 3.0.36.1849
[ EntityAliveSDX / EntityAliveSDXV4 Save Record Hardening ]
- NPC records are now written as a versioned, component-framed section
(identity, quest journal, patrol, buffs/progression, weapon, loot)
instead of one fragile sequential stream. Each component serializes
into its own length-prefixed frame, so a component that fails to
write is dropped whole (with an error log) instead of half-written,
and a component that fails to read is skipped without misaligning
the ones after it. Previously a mid-write failure in Buffs (where
hired/leader cvars live) silently corrupted everything after it -
on reload the NPC lost its owner and wandered off or despawned,
reported by users as NPCs disappearing across save/load.
- Guards against a vanilla chunk killer: EntityCreationData stores
each entity's data behind a ushort length prefix but writes the full
byte array. Any entity record over 65,535 bytes saves a wrapped
around length, and the next Chunk.load misparses the chunk's entity
list - ChunkSnapshotUtil.LoadChunk then DELETES the whole chunk
(NPC, player builds and all). Long-lived NPCs with big quest
journals, cvar-heavy buffs and full backpacks can plausibly cross
that limit. The new writer measures the total record and sheds
components to stay under it - quest journal first, then
buffs/progression, loot last - logging each drop, so the chunk
always stays loadable.
- Old saves load transparently via a legacy fallback reader (records
upgrade to the new format on their next save). Note: not forward
compatible - a downgraded SCore cannot read records saved by this
version, and server/client must run matching SCore versions for
NPC spawn sync.
- Also fixed a latent asymmetry: progression was written only when
present but read unconditionally; the new format stores an explicit
flag.
- New ErrorHandling property in blocks.xml: LogOversizedEntityData
(default true) logs an error naming ANY entity (vanilla or modded)
whose record exceeds the 65,535 byte limit, so oversized records
can be traced before their chunk dies.
[ ErrorHandling - Companion HUD List Drift Fix ]
- Vanilla XUiC_CompanionEntryList.RefreshPartyList repositions its
own grid by (party members - 1) rows PLUS (companions - 1) rows.
The party term is correct (the companion grid shares its XML
position with the party grid and must sit below the party rows),
but the companion term shifts the whole companion block down one
extra row for every companion beyond the first - on the
top-anchored, downward-stacking HUD grid the list visibly marches
down the screen as companions are added. XUiC_PartyEntryList never
repositions itself, which is why party entries don't drift.
- New postfix recomputes the offset with only the party-row term,
keeping companions stacked directly under the party rows. Reads
the row height from the grid's cell_height instead of vanilla's
hardcoded 40 pixels, so custom HUD layouts with different row
heights lay out correctly too.
- Not fixable from XML alone: the 40px step and the companion count
offset are hardcoded in the vanilla controller.
- New ErrorHandling property in blocks.xml:
FixCompanionEntryListDrift (default true).
[ ErrorHandling - Chunk Load Path Race Protection ]
- Second thread-safety hole in the same family as the OptimizeLayout
race: RegionFileManager's chunk-load path shares ONE read buffer,
cached decompressor and load stream across every caller with no
locking. Chunk loads normally run on the generation thread only,
but chunk resets (regionreset / worldchunkreset console commands,
ActionResetRegions game events - commonly scheduled on live
servers) load chunks from the MAIN thread through the same shared
reader. Overlapping loads tear the shared buffers mid
decompression ("EXC invalid distance too far back", "Wrong chunk
header!") and the failed load then deletes the chunk - and every
NPC standing in it - from the region file.
- New Harmony prefix/finalizer holds a per-instance lock across all
of ChunkSnapshotUtil.LoadChunk (the shared load stream is still
being parsed by chunk.load after the raw read returns, so the
lock must span the whole method).
- New ErrorHandling properties in blocks.xml:
- RegionFileLoadChunkLock (default true) enables the guard.
- LogRegionFileLoadChunkLock logs each time a load had to wait
on a concurrent load (each hit is a prevented corruption);
can be noisy during mass chunk resets.
[ ErrorHandling - rendermap Live Server Warning ]
- The web panel / rendermap full-map render opens a SECOND
RegionFileManager over the live save's region files. Region file
locking is per-instance and files open shared, so its reads tear
against the live manager's writes, its failed loads delete chunks
from the live save, and its cleanup compacts region files the
live manager has cached sector tables for.
- SCore now logs a clear warning when a full map render starts
while a game is loaded: run rendermap with no players connected,
or take a backup first. Warning only; the render is not blocked.
- The patch resolves its target lazily and disables itself on
clients (MapRenderer needs Webserver.dll, which only ships with
dedicated servers), so client installs are unaffected.
- New ErrorHandling property in blocks.xml: WarnRenderMapLiveServer
(default true).
[ ErrorHandling - RegionFileV2.OptimizeLayout Race Protection ]
- Field reports of chunk corruption ("EXCEPTION: In load chunk",
"Wrong chunk header!", negative read counts, "Memory stream is not
expandable") traced to a base-game thread-safety hole: vanilla
RegionFileV2.OptimizeLayout compacts a region file in place
(truncate + rewrite) without taking the instance lock that
ReadData/WriteData hold, and the chunk load path (GenerateChunksThread
-> RegionFileManager.GetChunkSync) takes no saveLock. A chunk load
overlapping a compaction on the save thread reads garbage from the
half-rewritten file, and the failed load then DELETES that chunk from
the region file and regenerates it - losing player builds.
- New Harmony prefix/finalizer pair holds the region file's instance
lock for the whole compaction, making it mutually exclusive with the
already-locked reads and writes. Does not repair regions corrupted
before the patch, and cannot protect against the process being killed
mid-compaction.
- New ErrorHandling properties in blocks.xml:
- RegionFileOptimizeLayoutLock (default true) enables the guard.
- LogRegionFileOptimizeLayoutLock logs each time a compaction had
to wait on a concurrent read/write - every hit is a corruption
event that was prevented, useful for confirming the race in the
field.
[ ErrorHandling - Mute Sign Data Manager Renderer Warnings ]
- Pathing cubes and other "programming" sign blocks have no visual
renderer on purpose, so vanilla SignDataManager.TryApplyRenderingData
spammed "Unexpected case in Sign Data Manager: signRenderer is null"
for every update. Vanilla already skips null renderers safely; the
warning was the only effect.
- A feature-gated prefix reruns the identical vanilla logic without
logging for null signRenderer / signRenderer.Renderer entries.
- New ErrorHandling property in blocks.xml: MuteSignRendererWarnings
(default true). Set to false to restore the vanilla warnings.
3.0.31.1019
Version: 3.0.31.1019
[ SphereII Disable Special Attack ]
- A small modlet to disable the ranged special attacks on Chuck and the Rancher, so they only melee.
- Removes Chuck's ranged boulder-throw attack (AITask + Action1 on its hand item)
- Removes the Rancher's ranged insect-swarm attack (AITask + Action1 on its hand item)
[ NPC Utility AI - CrouchOverride Cvar to Pin Commanded Stance ]
- NPC crouching was mirrored from the leader's stance every UAI tick
(SetCrouching in Scripts/UtilityAI/UAISCoreUtils.cs and Features/NPCv4/
UtilityAI/Utils/CombatUtils.cs), so any stance set externally (e.g. a
voice-command mod telling a hired NPC to take cover) was overwritten on
the next tick regardless of which UAI task called SetCrouching.
- Both SetCrouching chokepoints now check a "CrouchOverride" entity cvar
before applying the mirrored value: 0/absent = unchanged (mirror leader,
default behavior), 1 = force crouch, 2 = force stand. Since every call
site funnels through these two methods, this covers Follow, Idle, Loot,
ApproachSpot, AttackTargetEntity, BackupFromTarget, and MoveToExplore
without any changes to the individual UAI tasks.
- Lets other mods pin a hired/commanded NPC's stance independently of the
player's own stance, persisting across save/load since it's a normal
entity cvar. Requested by the NPCVoiceControl mod team for squad-tactics
stance commands.
[ BlockSpawnCube2SDX - Chunk Corruption From Stale TileEntityPoweredTrigger ]
- Revisiting a POI whose spawn cube had already fired could corrupt the
chunk on save/reload, later throwing "Specified cast is not valid" out
of TileEntityPoweredTrigger.CreatePowerItem while loading the chunk
(Chunk.load -> TileEntityPowered.OnReadComplete ->
InitializePowerData -> CreatePowerItemForTileEntity), and could snowball
into further corrupted regions afterward.
- BlockSpawnCube2SDX extends BlockMotionSensor, which creates a
TileEntityPoweredTrigger in OnBlockAdded, but the class never overrode
OnBlockRemoved to clean it up - unlike every other custom TE-owning
block in this codebase (BlockPoweredWorkstation, BlockDecoAoE,
BlockMortSpawner, the water blocks, the portal blocks), which all
explicitly remove their tile entity on removal. The leftover TE could
survive block destruction and get serialized into the chunk save,
mismatched against whatever block now occupies that position.
- UpdateTick also unconditionally called DestroySelf() after every tick
regardless of whether MaxSpawned had been reached, immediately firing a
SetBlockRPC(meta++) followed back-to-back by DestroySelf's own
DamageBlock/SetBlockRPC call on the same block position in the same
tick - two rapid consecutive writes to one position while its TE still
existed, the likely source of the save race. This also silently broke
MaxSpawned > 1 configs, which died after their first spawn instead of
continuing to spawn.
- Fixed by adding an explicit OnBlockRemoved override that calls
_chunk.RemoveTileEntityAt<TileEntityPoweredTrigger>(...), and by only
calling DestroySelf() once meta >= MaxSpawned, rescheduling via
GetTickRate() otherwise so multi-spawn configs actually spawn more than
once before the block destroys itself.
3.0.28.1654
Version: 3.0.28.1654
[ NPC Dialogue - Lockup After Declining Hire Offer or Early ESC/Tab Close ]
- Declining the "Hire for N Dukes" prompt (or closing the dialogue early with ESC/Tab
instead of finishing it normally) left the NPC's second conversation completely
dead: no dialogue menu appeared at all, and the player stayed movement-locked until
forcing the dialogue closed again with ESC/Tab.
- Root cause traced through the decompiled engine: EntityAliveSDX.OnEntityActivated
only sends a new talk lock request when
`!windowManager.IsModalWindowOpen() || GetModalWindow().Id == "radial"` - if the
window manager still thinks a modal window is open from the previous conversation,
every later interaction attempt silently no-ops (no lock request, no dialog, no
unlock, so the player also stays movement-locked - matching both symptoms).
[ Crafting - CraftingManager.GetRecipe(hash) No Longer Valid ]
- The engine's CraftingManager no longer exposes a way to fetch a specific Recipe by
its hash code alone, breaking RecipeHasIngredients (Scripts/Buffs/RecipeHasIngredients.cs),
which relied on `CraftingManager.GetRecipe(recipeHash)` to look back up the exact
recipe that was just crafted.
- RecipeStackOutputStackOnRecipeCrafted (Features/PassiveEffectHooks/Harmony/Triggers/
OnRecipeCrafted.cs) now also stamps a "RecipeName" string metadata entry
(__instance.recipe.GetName()) alongside the existing "Recipe" hash-code metadata.
- RecipeHasIngredients.IsValid reads both back: it calls
CraftingManager.GetAllRecipes(recipeName) to get every recipe registered under that
name, then walks the results comparing GetHashCode() against the stored hash to find
the exact recipe that was crafted (multiple recipes can share the same name, so the
name alone isn't a unique enough lookup key).
[ NPC Corpse Bags - NullReferenceException Opening bagStorage Window ]
- Looting an NPC's dropped bag (BackpackNPC/EntityBackpackNPC) threw a
NullReferenceException in XUiC_BagContainer's GetLockedSlotsFromStorage delegate
(get_LockedSlots -> OnOpen) the moment the loot window opened. Root cause traced
through the decompiled engine: Entity.OnLockedLocal resolves
`LootContainer.GetLootContainer(GetLootList())` and passes it into
XUiC_BagStorageWindowGroup.Open, which calls XUiC_BagContainer.SetBag(bag,
lootContainer, ...) - vanilla SetBag silently no-ops (never assigns its Bag
property) if either the bag or the resolved LootContainer is null. The window still
opens regardless, so the later OnOpen chain always ran against a null Bag.
- EntityBackpackNPC.GetLootList() returns a separate `lootListOnDeath` field instead
of the entity class's LootList property, and CopyPropertiesFromEntityClass() only
populated that field from an entity-class property named "LootListOnDeath" - which
the BackpackNPC entity_class (0-XNPCCore/Config/entityclasses.xml) never defines,
only "LootList" (="playerBackpack"). So GetLootList() always returned an empty
string for NPC bags, LootContainer.GetLootContainer("") resolved to null, and
SetBag's guard silently swallowed the bag assignment - all while the player's own
death backpack (plain vanilla EntityItem, no GetLootList override) worked fine.
- CopyPropertiesFromEntityClass() now falls back to the entity class's "LootList"
property when "LootListOnDeath" isn't explicitly set, so NPC bags resolve the same
loot container definition already configured for their contents/sizing and open
without crashing.
[ Lock Picking - Cop Car Downgrading to Alarm Variant ]
- XUiC_PickLocking.OnClose: picking a lock successfully (on the ILockable path,
LockedFeature == null) chose the downgrade block with
`if (!LockpickDowngradeBlock.isair) blockValue = LockpickDowngradeBlock; else if
(!DowngradeBlock.isair) blockValue = DowngradeBlock;` - prefer the pick-specific
downgrade, fall back to the generic one only if it isn't set. For the cop car
(cntPoliceCar01), LockpickDowngradeBlock is its safe "picked, bonus loot" variant
and DowngradeBlock is its alarm-triggering variant, so this was already the correct
preference order.
- Restored that preference order (a prior edit had turned the else-if into two
sequential ifs, which reassigned blockValue to DowngradeBlock immediately after
setting it to LockpickDowngradeBlock - LockpickDowngradeBlock could never actually
win). With the fix, a successfully picked cop car now downgrades to its bonus-loot
variant instead of the alarm variant that triggers a call.
[ Multiplayer - HarvestManager Loot Window Auto-Closing on Dedicated Server ]
- NetPackageHarvestInventoryData.ProcessPackage (dedicated-server client: opens a
trader NPC's harvest inventory delivered from the server) built its own
SCoreLootContainer with only a chunk reference and never set localChunkPos, so it
defaulted to the chunk's bedrock corner - tens of meters from the player/NPC. The
loot window's distance-based auto-close read that bogus position and closed the
window (at a constant ~63m) the instant it opened, regardless of the player's real
distance to the NPC. HarvestManager.GetOrCreate already carried the equivalent fix
for the listen-server/local path (PositionAtEntity, stamping chunk/localChunkPos
onto the NPC's live position) but it was never applied to this client-rebuilt
container.
- HarvestManager.PositionAtEntity is now public, and
NetPackageHarvestInventoryData.ProcessPackage calls it on the container right after
construction, so the dedicated-server client path gets the same live-position fix.
[ Multiplayer - HarvestManager OpenContainer NullReferenceException on Dedicated Server ]
- EntityUtilities.ExecuteCMD's "OpenInventory" case branched only on
ConnectionManager.IsServer to decide whether to open a trader NPC's harvest
container directly (OpenContainer(player as EntityPlayerLocal, ...)) or request it
from the server. IsServer is true for dedicated servers too, and
DialogActionExecuteCommandSDX declares ActionType => AddBuff, which rides the
vanilla dialog engine's buff-action replication - so this case also runs
authoritatively on a headless dedicated server, where there is no EntityPlayerLocal
at all. `player as EntityPlayerLocal` was always null there, and
OpenContainer(null, ...) threw a NullReferenceException inside
LocalPlayerUI.GetUIForPlayer(null).
- Added a `player is EntityPlayerLocal playerLocal` guard inside the IsServer branch:
if the replay's player isn't local (dedicated server, or a listen-server host
replaying a remote guest's action), it's now a safe no-op instead of a crash - the
owning client's own ExecuteCMD call (running in its own process, where IsServer is
false) still sends the request packet independently. Listen-server hosts and real
remote clients are unaffected.
[ Multiplayer - Weapon Swap Sync Fix ]
- NetPackageWeaponSwap.ProcessPackage: the server branch previously returned immediately
without applying the swap or relaying it, so a client-initiated weapon swap only ever
updated the sending client and was silently dropped everywhere else. The server now
applies the swap to its own EntityAliveSDX like any other recipient, then relays the
package to all other clients (excluding the original sender) so weapon swaps stay in
sync across the whole server.
3.0.25.912
Version: 3.0.25.912
[ NPC Interaction - Remove Trader Hours Restriction ]
- EntityAliveSDX.OnEntityActivated / EntityAliveSDXV4.OnEntityActivated: no longer call
base.OnEntityActivated (EntityTrader), which gated the whole interaction behind
TraderData.TraderInfo.IsTraderActivitiesOpen and showed a "come back later" tooltip
instead of opening dialog/trade outside of trader hours. NPCs built on these classes
aren't schedule-restricted vendors, so both methods now reproduce only the rest of the
base behavior (the modal-window check and LockManager.Instance.LockRequestLocal call),
letting NPCs talk and trade at any time of day.
Version: 3.0.24.1327
[ Utility AI - Wander/Follow Recovery ]
- UAITaskWanderSDX: task instances are singletons (one per <task> XML element, shared by
every entity using the package), so per-entity state (target, stuck count, redirect
cooldown) is now tracked in entityId-keyed dictionaries instead of plain fields, which
were being silently clobbered/shared across every wandering NPC. On blocked movement
(wall/ledge/corner), NPCs now escalate through widening search attempts and redirect to
a new target instead of getting stuck forever.
- UAITaskFollowSDX: while blocked, hired NPCs now retry pathing to their leader every 1s
instead of only waiting out the full 5s teleport timer with no recalculation attempt.
- UAISCoreUtils.CheckJump: no longer runs its edge/ledge jump-safety check while the entity
is on a Follow order, so it can no longer clear a following NPC's path out from under it.
- EntityAliveSDX/EntityAliveSDXV4: GetSurfaceY (terrain-plus-placed-blocks height lookup)
is now public instead of private, so it can be reused by external callers.
[ Lock Picking - TEFeatureLockPickable Support ]
- Fixed a NullReferenceException when picking a block secured only by TEFeatureLockPickable
(chests, cars, etc. without a full TEFeatureLockable) - the code resolved the feature but
then called SetLocked() on a still-null TEFeatureLockable reference.
- TEFeatureLockPickable-backed blocks now route through SCore's own pick-lock minigame
(XUiC_PickLocking) instead of vanilla's timer/break-chance UI, using the block's own
configured lock-pick item. On success it replicates vanilla's own completion (marks
fully unlocked, swaps to the configured downgrade block, fires the configured success
event) instead of the ILockable-only SetLocked(false) path.
[ Quality - Debug Logging ]
- Re-enabled QualityTroublshooting.cs, a set of Harmony log taps across the crafting-quality
pipeline (recipe queue input, tile entity sync, HandleRecipeQueue, AddCraftComplete) used
to trace custom quality levels through crafting. Gated behind
AdvancedItemFeatures.CustomQualityLevels, same as the rest of the custom quality system.
3.0.16.732
Version: 3.0.16.732
[ Prefabs ]
- Updated Cave Prefabs from Stallionsden.
[ DropBox ]
- Removed excessive debug logs
[ Lock Picking - Door Activation Command Migration ]
- Fixed lock picking no longer triggering on doors. The engine now prefixes
BlockCompositeTileEntity.OnBlockActivated command names with the owning feature
(e.g. "TEFeatureDoor:open" instead of the bare "open"), so
BlockDoorSecurePatch.BlockDoorSecureOnBlockActivated's exact string match against
"open" always failed and the door fell through to vanilla behavior. Updated the
check to match "TEFeatureDoor:open".