Skip to content

Releases: SphereII/SphereII.Mods

3.1.9.1528

Choose a tag to compare

@SphereII SphereII released this 28 Jul 18:31

`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

Choose a tag to compare

@SphereII SphereII released this 27 Jul 18:22

*** 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

3.1.6.1010 Pre-release
Pre-release

Choose a tag to compare

@SphereII SphereII released this 25 Jul 13:15

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

3.1.3.1715 Pre-release
Pre-release

Choose a tag to compare

@SphereII SphereII released this 22 Jul 20:26

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

Choose a tag to compare

@SphereII SphereII released this 25 Jul 13:25

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

Choose a tag to compare

@SphereII SphereII released this 22 Jul 15:41

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

Choose a tag to compare

@SphereII SphereII released this 16 Jul 13:23

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

Choose a tag to compare

@SphereII SphereII released this 13 Jul 20:01

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

Choose a tag to compare

@SphereII SphereII released this 10 Jul 12:19

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

Choose a tag to compare

@SphereII SphereII released this 01 Jul 10:51

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".