sharplib/data/JsonData.cs
2026-02-14 02:06:00 -08:00

200 lines
4.9 KiB
C#

using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Reflection;
using System.Text.Encodings.Web;
static public class json
{
static public T load<T>( string filename ) where T : lib.JsonData
{
return (T)lib.JsonData.load<T>( filename );
}
static public lib.JsonData load( string filename )
{
return lib.JsonData.load( filename );
}
}
namespace lib
{
// Merged the Interface and Class logic to remove ambiguity
public interface JsonData
{
#region Configuration
// Centralized JSON Options for consistency across Load/Save
private static readonly JsonSerializerOptions _options = new()
{
WriteIndented = true, // Pretty print
PropertyNameCaseInsensitive = true, // Forgiving load
IncludeFields = true, // Serialize public fields (game dev standard)
ReferenceHandler = ReferenceHandler.Preserve, // Handles circular refs and polymorphism $id metadata
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // Cleaner files
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
#endregion
#region SaveLoad
// Entry point for Generic Load
static public T res_load<T>( string filename ) where T : JsonData
{
return (T)actual_load( filename, typeof( T ) );
}
// Entry point for untyped Load
static public JsonData load( string filename )
{
return actual_load( filename, null );
}
// Wrapper for generic load
static public T load<T>( string filename ) where T : JsonData
{
return (T)actual_load( filename, typeof( T ) );
}
static public JsonData actual_load( string filename, Type t )
{
JsonData json = null;
try
{
// 1. Path Sanitization
if( !File.Exists( filename ) && filename.StartsWith( "res://" ) )
{
filename = filename.Replace( "res://", "" );
}
// 2. Load or Create
if( File.Exists( filename ) )
{
string jsonContent = File.ReadAllText( filename );
// If T is null, we can't deserialize effectively without Type info.
// Defaulting to JsonData or the passed type.
Type targetType = t ?? typeof( JsonData );
json = (JsonData)JsonSerializer.Deserialize( jsonContent, targetType, _options );
if( json != null )
{
json.Filename = filename;
}
}
else
{
json = CreateTemplate( filename, t );
}
}
catch( Exception ex ) when( ex is IOException || ex is JsonException )
{
log.error( $"Failed to load {filename}: {ex.Message}" );
json = CreateTemplate( filename, t );
}
return json;
}
private static JsonData CreateTemplate( string filename, Type t )
{
log.debug( $"JsonData file {filename} not found, creating template." );
JsonData json = null;
// Handle potential null type if called generically without constraints
if( t == null )
{
log.error( "Cannot create template for unknown type." );
return null;
}
try
{
json = refl.CreateObject( t ) as JsonData;
}
catch( Exception e )
{
log.error( $"Exception while creating config {t}, Msg {e.Message}" );
}
// Write out the template if configured to do so
if( json != null && ConfigCfg.s_cfg.writeOutTemplateFiles )
{
var templateFile = $"templates/{filename}";
var dirName = Path.GetDirectoryName( templateFile );
Util.checkAndAddDirectory( dirName );
log.info( $"Writing out template config of type {t?.Name} in {templateFile}" );
// Force set filename for the template save, then restore?
// Or just pass path to saveDebug.
json.Filename = templateFile;
json.save();
}
return json;
}
static public void actual_save( JsonData json, string filename )
{
try
{
// Ensure directory exists
string dir = Path.GetDirectoryName( filename );
if( !string.IsNullOrEmpty( dir ) && !Directory.Exists( dir ) )
{
Directory.CreateDirectory( dir );
}
// Serialize to string first (safer than open stream + fail)
string jsonString = JsonSerializer.Serialize( json, json.GetType(), _options );
File.WriteAllText( filename, jsonString );
}
catch( Exception ex )
{
log.exception( ex, $"Type { json.GetType().Name } {filename}: {ex.Message}" );
}
}
#endregion
#region Instance Logic
//private string _filename = "{unknown}";
public string Filename { get; set; }
static public void startup()
{
// Assuming res.Mgr handles the delegates correctly
//res.Mgr.Register( JsonData.load );
res.Mgr.RegisterSub( typeof( ConfigBase ) );
}
#endregion
}
public class JsonDataRes : JsonData, res.Resource
{
public string Filename { get; set; }
}
static public class JsonDataEx
{
static public void save( this JsonData json )
{
// Allow overriding path for templates, otherwise use object's filename
JsonData.actual_save( json, json.Filename );
}
}
}