From 02d0cf1cdad67939dac3e3845778130069fbf42c Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Wed, 4 Feb 2026 14:33:54 -0500 Subject: [PATCH 1/3] Fix an issue where lots not played in vanilla TS1 wouldn't have controllers and exp pack data loaded (#288) * Fixing issues with loading fresh lots * Removing unneeded fallback code * Removing unneeded check per PR feedback --- .../tso.simantics/Utils/VMTS1ActivatorNew.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/TSOClient/tso.simantics/Utils/VMTS1ActivatorNew.cs b/TSOClient/tso.simantics/Utils/VMTS1ActivatorNew.cs index 8d3332cd8..5a246d0e6 100644 --- a/TSOClient/tso.simantics/Utils/VMTS1ActivatorNew.cs +++ b/TSOClient/tso.simantics/Utils/VMTS1ActivatorNew.cs @@ -668,6 +668,23 @@ public Blueprint LoadFromIff(IffFile iff) VM.Load(fsov); VM.UpdateFreeObjectID(); + // Spawn controller objects that are missing from the saved lot. + // In vanilla TS1, these are spawned automatically and saved into OBJM. + // If the lot was never opened in vanilla, they won't be in the save, + // so we need to spawn them here. + var controllerObjects = content.WorldObjects.ControllerObjects.Select(x => (uint)x.ID).ToList(); + + foreach (var controller in controllerObjects) + { + // Check if controller already exists in the loaded lot + var exists = VM.Entities.Any(e => e.Object.OBJ.GUID == controller); + if (!exists) + { + // Spawn missing controller at OUT_OF_WORLD + VM.Context.CreateObjectInstance(controller, LotTilePos.OUT_OF_WORLD, Direction.NORTH); + } + } + // Attempt to recover queue names. foreach (var ava in VM.Context.ObjectQueries.Avatars) { From 3072dfcdebd07c7f2630a3148657ff15bc25d851 Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Sun, 8 Feb 2026 15:56:55 -0500 Subject: [PATCH 2/3] Adding support for the free will toggle for Simitone (#290) --- TSOClient/FSO.UI/GlobalSettings.cs | 3 +++ .../tso.simantics/Primitives/VMFindBestAction.cs | 14 ++++++++++++-- TSOClient/tso.simantics/VM.cs | 6 ++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/TSOClient/FSO.UI/GlobalSettings.cs b/TSOClient/FSO.UI/GlobalSettings.cs index 5e7a56108..41ff3d48a 100644 --- a/TSOClient/FSO.UI/GlobalSettings.cs +++ b/TSOClient/FSO.UI/GlobalSettings.cs @@ -120,6 +120,7 @@ public GlobalSettings(string path) : base(path) { } { "ArchiveServerGUID", "" }, { "ArchiveClientGUID", "" }, + { "TS1FreeWill", "true" }, }; public override Dictionary DefaultValues @@ -196,6 +197,8 @@ public override Dictionary DefaultValues public string ArchiveServerGUID { get; set; } public string ArchiveClientGUID { get; set; } + public bool TS1FreeWill { get; set; } + public static int TARGET_COMPAT_STATE = 2; } } diff --git a/TSOClient/tso.simantics/Primitives/VMFindBestAction.cs b/TSOClient/tso.simantics/Primitives/VMFindBestAction.cs index b3682e883..de416f508 100644 --- a/TSOClient/tso.simantics/Primitives/VMFindBestAction.cs +++ b/TSOClient/tso.simantics/Primitives/VMFindBestAction.cs @@ -98,12 +98,22 @@ public override VMPrimitiveExitCode Execute(VMStackFrame context, VMPrimitiveOpe return VMPrimitiveExitCode.GOTO_TRUE; } + var caller = (VMAvatar)context.Caller; + + // Check if free will is disabled for player family Sims + // Visitors (PersonType == 1) and pets should still have autonomy + var visitor = (caller.GetPersonData(VMPersonDataVariable.PersonType) == 1); + if (!VM.FreeWillEnabled && !visitor && !caller.IsPet) + { + // Free will is disabled and this is a player family Sim (not visitor, not pet) + // Return false to indicate no autonomous action was chosen + return VMPrimitiveExitCode.GOTO_FALSE; + } + var ents = new List(context.VM.Context.ObjectQueries.WithAutonomy); var processed = new HashSet(); - var caller = (VMAvatar)context.Caller; var pos1 = caller.Position; - var visitor = (caller.GetPersonData(VMPersonDataVariable.PersonType) == 1); var child = (caller.IsChild && context.VM.TS1); var attenTable = visitor ? TTAB.VisitorAttenuationValues : TTAB.AttenuationValues; var global = Content.Content.Get().WorldObjectGlobals; diff --git a/TSOClient/tso.simantics/VM.cs b/TSOClient/tso.simantics/VM.cs index 9a7589527..d66baf8e0 100644 --- a/TSOClient/tso.simantics/VM.cs +++ b/TSOClient/tso.simantics/VM.cs @@ -66,6 +66,12 @@ public bool BlueprintRestore //we can assume one application won't be running TS1 and TSO at the same time. public bool Aborting = false; + /// + /// Global toggle for free will (autonomy). When disabled, player family Sims will not + /// autonomously choose actions. Visitors and pets still have free will. + /// + public static bool FreeWillEnabled = true; + private const long TickInterval = 33 * TimeSpan.TicksPerMillisecond; public byte[][] HollowAdj; From fc2781dac55b2214d77da1b7f376e0e8b0f10869 Mon Sep 17 00:00:00 2001 From: Alex Yong Date: Thu, 12 Feb 2026 22:38:08 +0000 Subject: [PATCH 3/3] Updating code to handle animation loading errors from custom content more gracefully --- TSOClient/tso.content/Content.cs | 14 +++ TSOClient/tso.content/TS1/TS1BCFProvider.cs | 90 +++++++++++++++++-- .../tso.content/TS1/TS1ObjectProvider.cs | 82 ++++++++++++----- .../tso.simantics/Model/VMAnimationState.cs | 7 +- TSOClient/tso.vitaboy.model/Animation.cs | 16 ++++ TSOClient/tso.vitaboy.model/BCF.cs | 27 +++++- 6 files changed, 200 insertions(+), 36 deletions(-) diff --git a/TSOClient/tso.content/Content.cs b/TSOClient/tso.content/Content.cs index 4910abc08..7f4e7d262 100644 --- a/TSOClient/tso.content/Content.cs +++ b/TSOClient/tso.content/Content.cs @@ -34,6 +34,8 @@ public static void Init(string basepath, GraphicsDevice device){ if (!INSTANCE.Inited) INSTANCE.Init(); return; } + // Clear any previously recorded failed files + FailedContentFiles.Clear(); INSTANCE = new Content(basepath, ContentMode.CLIENT, device, true); } @@ -88,6 +90,12 @@ public static bool TS1Hybrid public bool Inited = false; public ChangeManager Changes; + + /// + /// List of files that failed to load during content initialization. + /// Used to display warnings to users about problematic custom content. + /// + public static List FailedContentFiles { get; private set; } = new List(); /// /// Creates a new instance of Content. @@ -269,6 +277,12 @@ private void Init() TS1Global?.Init(); LoadProgress = ContentLoadingProgress.InitBCF; BCFGlobal?.Init(); + + // Collect any failed files from BCF loading + if (BCFGlobal?.FailedFiles?.Count > 0) + { + FailedContentFiles.AddRange(BCFGlobal.FailedFiles); + } if (!TS1) PIFFRegistry.Init(Path.Combine(FSOEnvironment.ContentDir, "Patch/")); else PIFFRegistry.Init(Path.Combine(FSOEnvironment.ContentDir, "TS1Patch/")); diff --git a/TSOClient/tso.content/TS1/TS1BCFProvider.cs b/TSOClient/tso.content/TS1/TS1BCFProvider.cs index cda560928..35219e14a 100644 --- a/TSOClient/tso.content/TS1/TS1BCFProvider.cs +++ b/TSOClient/tso.content/TS1/TS1BCFProvider.cs @@ -23,6 +23,21 @@ public class TS1BCFProvider public Dictionary SkelHostBCF = new Dictionary(); public Content ContentManager; public Dictionary CollectionsByName = new Dictionary(); + + /// + /// List of files that failed to load with their error messages. + /// + public List FailedFiles { get; private set; } = new List(); + + /// + /// Information about a file that failed to load. + /// + public class FailedFileInfo + { + public string Filename { get; set; } + public string ErrorMessage { get; set; } + public string ErrorType { get; set; } + } public TS1BCFProvider(Content contentManager, TS1Provider provider) { @@ -42,19 +57,78 @@ public void Init() var allBCFs = BCFProvider.ListGeneric(); foreach (var bcf in allBCFs) { - var file = (BCF)bcf.GetThrowawayGeneric(); - foreach (var anim in file.Animations) + try { - AnimHostBCF[anim.Name.ToLowerInvariant()] = Path.GetFileName(bcf.ToString().ToLowerInvariant().Replace('\\', '/')); - AnimRealCase[anim.Name.ToLowerInvariant()] = anim.Name; + var file = (BCF)bcf.GetThrowawayGeneric(); + if (file == null) + { + // Failed to decode the file (returned null) + var filename = Path.GetFileName(bcf.ToString()); + FailedFiles.Add(new FailedFileInfo + { + Filename = filename, + ErrorMessage = "File could not be decoded (unsupported or corrupted format)", + ErrorType = "DecodeError" + }); + continue; + } + + foreach (var anim in file.Animations) + { + AnimHostBCF[anim.Name.ToLowerInvariant()] = Path.GetFileName(bcf.ToString().ToLowerInvariant().Replace('\\', '/')); + AnimRealCase[anim.Name.ToLowerInvariant()] = anim.Name; + } + foreach (var skin in file.Appearances) + { + SkinHostBCF[skin.Name.ToLowerInvariant()] = Path.GetFileName(bcf.ToString().ToLowerInvariant().Replace('\\', '/')); + } + foreach (var skel in file.Skeletons) + { + SkelHostBCF.Add(skel.Name.ToLowerInvariant(), Path.GetFileName(bcf.ToString().ToLowerInvariant().Replace('\\', '/'))); + } } - foreach (var skin in file.Appearances) + catch (System.IO.EndOfStreamException) { - SkinHostBCF[skin.Name.ToLowerInvariant()] = Path.GetFileName(bcf.ToString().ToLowerInvariant().Replace('\\', '/')); + // Common error for truncated/corrupted animation files + var filename = Path.GetFileName(bcf.ToString()); + FailedFiles.Add(new FailedFileInfo + { + Filename = filename, + ErrorMessage = "File appears to be truncated or corrupted (unexpected end of file). Ensure the file is complete and not damaged.", + ErrorType = "EndOfStream" + }); + } + catch (System.IO.InvalidDataException ex) + { + // Validation error - file has invalid counts or data + var filename = Path.GetFileName(bcf.ToString()); + FailedFiles.Add(new FailedFileInfo + { + Filename = filename, + ErrorMessage = $"Invalid file format: {ex.Message}", + ErrorType = "InvalidData" + }); } - foreach (var skel in file.Skeletons) + catch (System.IO.IOException ex) { - SkelHostBCF.Add(skel.Name.ToLowerInvariant(), Path.GetFileName(bcf.ToString().ToLowerInvariant().Replace('\\', '/'))); + var filename = Path.GetFileName(bcf.ToString()); + FailedFiles.Add(new FailedFileInfo + { + Filename = filename, + ErrorMessage = $"I/O error reading file: {ex.Message}", + ErrorType = "IOException" + }); + } + catch (Exception ex) + { + // Catch any other exceptions to prevent crashing + var filename = Path.GetFileName(bcf.ToString()); + FailedFiles.Add(new FailedFileInfo + { + Filename = filename, + ErrorMessage = $"Error loading file: {ex.Message}", + ErrorType = ex.GetType().Name + }); } } diff --git a/TSOClient/tso.content/TS1/TS1ObjectProvider.cs b/TSOClient/tso.content/TS1/TS1ObjectProvider.cs index ea3fad453..d82dc14e6 100644 --- a/TSOClient/tso.content/TS1/TS1ObjectProvider.cs +++ b/TSOClient/tso.content/TS1/TS1ObjectProvider.cs @@ -36,7 +36,26 @@ public void Init() var allIffs = GameObjects.ListGeneric(); foreach (var iff in allIffs) { - var file = (IffFile)iff.GetThrowawayGeneric(); + IffFile file = null; + try + { + file = (IffFile)iff.GetThrowawayGeneric(); + } + catch (Exception ex) + { + // Log failed object file loads + string failedFilename = Path.GetFileName(iff.ToString().Replace('\\', '/')); + Content.FailedContentFiles.Add(new TS1BCFProvider.FailedFileInfo + { + Filename = failedFilename, + ErrorMessage = $"Failed to load object file: {ex.Message}", + ErrorType = ex.GetType().Name + }); + continue; + } + + if (file == null) continue; + var source = GameObjectSource.Far; string filename = Path.GetFileName(iff.ToString().Replace('\\', '/')); if (iff is FileContentReference) @@ -70,31 +89,46 @@ public void Init() if (obj.ObjectType == OBJDType.Person) PersonGUIDs.Add(obj.GUID); //does this object appear in the catalog? - if ((obj.FunctionFlags > 0 || obj.BuildModeType > 0) && obj.Disabled == 0 && - (obj.IsMultiTile || obj.NumGraphics > 0) && (obj.MasterID == 0 || obj.SubIndex == -1)) + bool passesCatalogCheck = (obj.FunctionFlags > 0 || obj.BuildModeType > 0) && obj.Disabled == 0 && + (obj.IsMultiTile || obj.NumGraphics > 0) && (obj.MasterID == 0 || obj.SubIndex == -1); + + if (passesCatalogCheck) { - //todo: more than one of these set? no normal game objects do this - //todo: room sort - var cat = (sbyte)Math.Log(obj.FunctionFlags, 2); - if (obj.FunctionFlags == 0) cat = (sbyte)(obj.BuildModeType+7); - var item = new ObjectCatalogItem() + try + { + //todo: more than one of these set? no normal game objects do this + //todo: room sort + var cat = (sbyte)Math.Log(obj.FunctionFlags, 2); + if (obj.FunctionFlags == 0) cat = (sbyte)(obj.BuildModeType+7); + var item = new ObjectCatalogItem() + { + Category = (sbyte)(cat), //0-7 buy categories. 8-15 build mode categories + RoomSort = (byte)obj.RoomFlags, + GUID = obj.GUID, + DisableLevel = 0, + Price = obj.Price, + Name = obj.ChunkLabel, + + Subsort = (byte)obj.FunctionSubsort, + CommunitySort = (byte)obj.CommunitySubsort, + DowntownSort = (byte)obj.DTSubsort, + MagictownSort = (byte)obj.MTSubsort, + StudiotownSort = (byte)obj.STSubsort, + VacationSort = (byte)obj.VacationSubsort + }; + ItemsByCategory[item.Category].Add(item); + ItemsByGUID[item.GUID] = item; + } + catch (Exception ex) { - Category = (sbyte)(cat), //0-7 buy categories. 8-15 build mode categories - RoomSort = (byte)obj.RoomFlags, - GUID = obj.GUID, - DisableLevel = 0, - Price = obj.Price, - Name = obj.ChunkLabel, - - Subsort = (byte)obj.FunctionSubsort, - CommunitySort = (byte)obj.CommunitySubsort, - DowntownSort = (byte)obj.DTSubsort, - MagictownSort = (byte)obj.MTSubsort, - StudiotownSort = (byte)obj.STSubsort, - VacationSort = (byte)obj.VacationSubsort - }; - ItemsByCategory[item.Category].Add(item); - ItemsByGUID[item.GUID] = item; + // Log catalog item creation failures + Content.FailedContentFiles.Add(new TS1BCFProvider.FailedFileInfo + { + Filename = filename, + ErrorMessage = $"Failed to add object '{obj.ChunkLabel}' to catalog: {ex.Message}", + ErrorType = "CatalogError" + }); + } } } } diff --git a/TSOClient/tso.simantics/Model/VMAnimationState.cs b/TSOClient/tso.simantics/Model/VMAnimationState.cs index 03d42f725..336a31ca0 100644 --- a/TSOClient/tso.simantics/Model/VMAnimationState.cs +++ b/TSOClient/tso.simantics/Model/VMAnimationState.cs @@ -33,6 +33,7 @@ public VMAnimationState(Animation animation, bool backwards) private void GetTimeProps() { var animation = Anim; + if (animation == null) return; // Animation failed to load, skip time properties foreach (var motion in animation.Motions) { if (motion.TimeProperties == null) { continue; } @@ -79,7 +80,11 @@ public virtual void Load(VMAnimationStateMarshal input) Speed = input.Speed; Weight = input.Weight; Loop = input.Loop; - GetTimeProps(); + // Only process time properties if animation was successfully loaded + if (Anim != null) + { + GetTimeProps(); + } var currentFrame = CurrentFrame; var currentTime = (currentFrame * 1000) / 30; diff --git a/TSOClient/tso.vitaboy.model/Animation.cs b/TSOClient/tso.vitaboy.model/Animation.cs index 52d302b77..4b11554b6 100644 --- a/TSOClient/tso.vitaboy.model/Animation.cs +++ b/TSOClient/tso.vitaboy.model/Animation.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using Microsoft.Xna.Framework; using FSO.Files.Utils; @@ -41,6 +42,11 @@ public void Read(BCFReadProxy io, bool bcf) if (bcf) { Name = io.ReadPascalString(); + // Validate: if name is empty or null, this might be a corrupted file + if (string.IsNullOrEmpty(Name)) + { + throw new InvalidDataException("Animation name is empty - file may be corrupted or in wrong format"); + } XSkillName = io.ReadPascalString(); } else @@ -53,6 +59,11 @@ public void Read(BCFReadProxy io, bool bcf) IsMoving = (bcf) ? ((byte)io.ReadInt32()) : io.ReadByte(); TranslationCount = io.ReadUInt32(); + // Sanity check: unreasonably high translation count suggests corrupted file + if (TranslationCount > 100000) + { + throw new InvalidDataException($"Translation count {TranslationCount} is unreasonably high - file may be corrupted"); + } if (!bcf) { Translations = new Vector3[TranslationCount]; @@ -68,6 +79,11 @@ public void Read(BCFReadProxy io, bool bcf) } RotationCount = io.ReadUInt32(); + // Sanity check: unreasonably high rotation count suggests corrupted file + if (RotationCount > 100000) + { + throw new InvalidDataException($"Rotation count {RotationCount} is unreasonably high - file may be corrupted"); + } if (!bcf) { Rotations = new Quaternion[RotationCount]; diff --git a/TSOClient/tso.vitaboy.model/BCF.cs b/TSOClient/tso.vitaboy.model/BCF.cs index f2541426c..988e39590 100644 --- a/TSOClient/tso.vitaboy.model/BCF.cs +++ b/TSOClient/tso.vitaboy.model/BCF.cs @@ -20,21 +20,42 @@ public BCF(Stream stream, bool cmx) { using (var io = (cmx) ? new BCFReadString(stream, true) : (BCFReadProxy)IoBuffer.FromStream(stream, ByteOrder.LITTLE_ENDIAN)) { - Skeletons = new Skeleton[io.ReadInt32()]; + int skelCount = io.ReadInt32(); + // Sanity checks for corrupted files + if (skelCount < 0 || skelCount > 1000) + { + throw new InvalidDataException($"Invalid skeleton count: {skelCount}. File may be corrupted or in wrong format."); + } + + Skeletons = new Skeleton[skelCount]; for (int i = 0; i < Skeletons.Length; i++) { Skeletons[i] = new Skeleton(); Skeletons[i].Read(io, true); Skeletons[i].ParentBCF = this; } - Appearances = new Appearance[io.ReadInt32()]; + + int appCount = io.ReadInt32(); + if (appCount < 0 || appCount > 10000) + { + throw new InvalidDataException($"Invalid appearance count: {appCount}. File may be corrupted or in wrong format."); + } + + Appearances = new Appearance[appCount]; for (int i = 0; i < Appearances.Length; i++) { Appearances[i] = new Appearance(); Appearances[i].ReadBCF(io); Appearances[i].ParentBCF = this; } - Animations = new Animation[io.ReadInt32()]; + + int animCount = io.ReadInt32(); + if (animCount < 0 || animCount > 10000) + { + throw new InvalidDataException($"Invalid animation count: {animCount}. File may be corrupted or in wrong format."); + } + + Animations = new Animation[animCount]; for (int i = 0; i < Animations.Length; i++) { Animations[i] = new Animation();