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( string filename ) where T : lib.JsonData { return (T)lib.JsonData.load( 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( 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( 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 ); } } }