Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions TSOClient/FSO.UI/GlobalSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public GlobalSettings(string path) : base(path) { }

{ "ArchiveServerGUID", "" },
{ "ArchiveClientGUID", "" },
{ "TS1FreeWill", "true" },
};

public override Dictionary<string, string> DefaultValues
Expand Down Expand Up @@ -196,6 +197,8 @@ public override Dictionary<string, string> DefaultValues
public string ArchiveServerGUID { get; set; }
public string ArchiveClientGUID { get; set; }

public bool TS1FreeWill { get; set; }

public static int TARGET_COMPAT_STATE = 2;
}
}
14 changes: 14 additions & 0 deletions TSOClient/tso.content/Content.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -88,6 +90,12 @@ public static bool TS1Hybrid
public bool Inited = false;

public ChangeManager Changes;

/// <summary>
/// List of files that failed to load during content initialization.
/// Used to display warnings to users about problematic custom content.
/// </summary>
public static List<TS1BCFProvider.FailedFileInfo> FailedContentFiles { get; private set; } = new List<TS1BCFProvider.FailedFileInfo>();

/// <summary>
/// Creates a new instance of Content.
Expand Down Expand Up @@ -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/"));
Expand Down
90 changes: 82 additions & 8 deletions TSOClient/tso.content/TS1/TS1BCFProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ public class TS1BCFProvider
public Dictionary<string, string> SkelHostBCF = new Dictionary<string, string>();
public Content ContentManager;
public Dictionary<string, TS1ClothingCollection> CollectionsByName = new Dictionary<string, TS1ClothingCollection>();

/// <summary>
/// List of files that failed to load with their error messages.
/// </summary>
public List<FailedFileInfo> FailedFiles { get; private set; } = new List<FailedFileInfo>();

/// <summary>
/// Information about a file that failed to load.
/// </summary>
public class FailedFileInfo
{
public string Filename { get; set; }
public string ErrorMessage { get; set; }
public string ErrorType { get; set; }
}

public TS1BCFProvider(Content contentManager, TS1Provider provider)
{
Expand All @@ -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
});
}
}

Expand Down
82 changes: 58 additions & 24 deletions TSOClient/tso.content/TS1/TS1ObjectProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>)
Expand Down Expand Up @@ -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"
});
}
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion TSOClient/tso.simantics/Model/VMAnimationState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 12 additions & 2 deletions TSOClient/tso.simantics/Primitives/VMFindBestAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<VMEntity>(context.VM.Context.ObjectQueries.WithAutonomy);
var processed = new HashSet<short>();
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;
Expand Down
17 changes: 17 additions & 0 deletions TSOClient/tso.simantics/Utils/VMTS1ActivatorNew.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
6 changes: 6 additions & 0 deletions TSOClient/tso.simantics/VM.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Global toggle for free will (autonomy). When disabled, player family Sims will not
/// autonomously choose actions. Visitors and pets still have free will.
/// </summary>
public static bool FreeWillEnabled = true;

private const long TickInterval = 33 * TimeSpan.TicksPerMillisecond;
public byte[][] HollowAdj;

Expand Down
16 changes: 16 additions & 0 deletions TSOClient/tso.vitaboy.model/Animation.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xna.Framework;
using FSO.Files.Utils;

Expand Down Expand Up @@ -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
Expand All @@ -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];
Expand All @@ -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];
Expand Down
Loading