TASEmulators / BizHawk

BizHawk is a multi-system emulator written in C#. BizHawk provides nice features for casual gamers such as full screen, and joypad support in addition to full rerecording and debugging tools for all system cores.
http://tasvideos.org/BizHawk.html
Other
2.2k stars 384 forks source link

API Hawk: Cannot load configuration file when attempting to load persisted configurations for external tools. #3337

Open tommai78101 opened 2 years ago

tommai78101 commented 2 years ago

Summary

If the external tool is opened at least once, and the external tool uses the following C# codes below, in theory, the configuration file config.ini will include information of the external tool.

[ConfigPersist]
public GeneticAlgorithmBotSettings Settings { get; set; }
public class GeneticAlgorithmBotSettings {
    public RecentFiles RecentBotFiles { get; set; } = new RecentFiles();
    public bool TurboWhenBotting { get; set; } = true;
    public bool InvisibleEmulation { get; set; }
}

Below is the snippet of the config.ini:

  ...

  "CommonToolSettings": {
    "BizHawk.Client.EmuHawk.TAStudio": {
      "_wndx": 543,
      "_wndy": 55,
      "Width": 525,
      "Height": 615,
      "SaveWindowPosition": true,
      "TopMost": false,
      "FloatingWindow": true,
      "AutoLoad": false
    },
    "GeneticAlgorithmBot.GeneticAlgorithmBot": {
      "_wndx": 1091,
      "_wndy": 149,
      "Width": 723,
      "Height": 626,
      "SaveWindowPosition": true,
      "TopMost": false,
      "FloatingWindow": true,
      "AutoLoad": false
    }
  },
  "CustomToolSettings": {
    "BizHawk.Client.EmuHawk.TAStudio": {
      "Settings": {
        "$type": "BizHawk.Client.EmuHawk.TAStudio+TAStudioSettings, EmuHawk",
        "RecentTas": {
          "recentlist": [],
          "MAX_RECENT_FILES": 8,
          "AutoLoad": false,
          "Frozen": false
        },
        "AutoPause": true,
        "AutoRestoreLastPosition": false,
        "FollowCursor": true,
        "EmptyMarkers": false,
        "ScrollSpeed": 6,
        "FollowCursorAlwaysScroll": false,
        "FollowCursorScrollMethod": "near",
        "BranchCellHoverInterval": 1,
        "SeekingCutoffInterval": 2,
        "AutosaveInterval": 120000,
        "AutosaveAsBk2": false,
        "AutosaveAsBackupFile": false,
        "BackupPerFileSave": false,
        "SingleClickAxisEdit": false,
        "OldControlSchemeForBranches": false,
        "LoadBranchOnDoubleClick": true,
        "DenoteStatesWithIcons": false,
        "DenoteStatesWithBGColor": true,
        "DenoteMarkersWithIcons": false,
        "DenoteMarkersWithBGColor": true,
        "MainVerticalSplitDistance": 0,
        "BranchMarkerSplitDistance": 183,
        "BindMarkersToInput": false,
        "CopyIncludesFrameNo": false,
        "Palette": {
          "CurrentFrame_InputLog": "181, 231, 247",
          "GreenZone_FrameCol": "221, 255, 221",
          "GreenZone_InputLog": "210, 249, 211",
          "GreenZone_InputLog_Stated": "196, 247, 200",
          "GreenZone_InputLog_Invalidated": "224, 251, 224",
          "LagZone_FrameCol": "255, 220, 221",
          "LagZone_InputLog": "244, 218, 218",
          "LagZone_InputLog_Stated": "240, 208, 210",
          "LagZone_InputLog_Invalidated": "247, 229, 229",
          "Marker_FrameCol": "247, 255, 201",
          "AnalogEdit_Col": "144, 144, 112"
        }
      },
      "TasViewFont": "Arial, 8.25pt, style=Bold"
    },
    "GeneticAlgorithmBot.GeneticAlgorithmBot": {
      "Settings": {
        "$type": "GeneticAlgorithmBot.GeneticAlgorithmBotSettings, GeneticAlgorithmBot",
        "RecentBotFiles": {
          "recentlist": [],
          "MAX_RECENT_FILES": 8,
          "AutoLoad": false,
          "Frozen": false
        },
        "TurboWhenBotting": true,
        "InvisibleEmulation": false
      }
    }
  },

  ...

I suspect that the formatting of the JSON object for CustomToolSettings and CommonToolSettings both didn't expect there to be an external tool reference if the external tool is using [ConfigPersist] attribute in the code.

Repro

  1. Use any blank C# project that is set up to create an external tool for BizHawk.
  2. Declare a class type with the [ConfigPersist] attribute and create a class type with any properties (int, float, or even RecentFiles class type).
  3. Build the external tool.
  4. Load the external tool in EmuHawk just 1 time.
  5. Close the external tool window (Important step!)
  6. Close EmuHawk.
  7. Open EmuHawk again.
  8. Observe crash.

Output

Full stack trace below:

It appears your config file (config.ini) is corrupted; an exception was thrown while loading it.
On closing this warning, EmuHawk will delete your config file and generate a new one. You can go make a backup now if you'd like to look into diffs.
The caught exception was:
System.InvalidOperationException: Config Error ---> Newtonsoft.Json.JsonSerializationException: Error resolving type specified in JSON 'GeneticAlgorithmBot.GeneticAlgorithmBotSettings, GeneticAlgorithmBot'. Path 'CustomToolSettings['GeneticAlgorithmBot.GeneticAlgorithmBot'].Settings.$type', line 1573, position 87. ---> Newtonsoft.Json.JsonSerializationException: Could not load assembly 'GeneticAlgorithmBot'.
   at Newtonsoft.Json.Serialization.DefaultSerializationBinder.GetTypeFromTypeNameKey(StructMultiKey`2 typeNameKey)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Newtonsoft.Json.Serialization.DefaultSerializationBinder.BindToType(String assemblyName, String typeName)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolveTypeName(JsonReader reader, Type& objectType, JsonContract& contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, String qualifiedTypeName)
   --- End of inner exception stack trace ---
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolveTypeName(JsonReader reader, Type& objectType, JsonContract& contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, String qualifiedTypeName)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadMetadataProperties(JsonReader reader, Type& objectType, JsonContract& contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue, Object& newValue, String& id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateDictionary(IDictionary dictionary, JsonReader reader, JsonDictionaryContract contract, JsonProperty containerProperty, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateDictionary(IDictionary dictionary, JsonReader reader, JsonDictionaryContract contract, JsonProperty containerProperty, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at BizHawk.Client.Common.ConfigService.Load[T](String filepath) in E:\LargeGithubProjects\BizHawk\src\BizHawk.Client.Common\config\ConfigService.cs:line 97
   --- End of inner exception stack trace ---
   at BizHawk.Client.Common.ConfigService.Load[T](String filepath) in E:\LargeGithubProjects\BizHawk\src\BizHawk.Client.Common\config\ConfigService.cs:line 102
   at BizHawk.Client.EmuHawk.Program.SubMain(String[] args)

Host env.

YoshiRulz commented 2 years ago

Can this be solved the same way core settings/syncsettings was @nattthebear?

nattthebear commented 2 years ago

On quick glance, it seems like it could be, yes.

tommai78101 commented 2 years ago

After consulting with adelikat and YoshiRulz on Discord, I followed their suggestions to use [ConfigPersist] attribute on multiple data types that are supported by C# language and BizHawk. And it worked! Thanks to both.

Leaving the ticket open because it's still a legitimate issue.


What causes this bug:

Custom class types are stored into the config.ini file as fully qualified class names. They are typically stored in the following format:

 "$type": "GeneticAlgorithmBot.GeneticAlgorithmBotSettings, GeneticAlgorithmBot",

This qualified class type name is created from the [ConfigPersist] applied to the GeneticAlgorithmBotSettings class itself:

[ConfigPersist]
public GeneticAlgorithmBotSettings Settings { get; set; }

BizHawk doesn't know what GeneticAlgorithmBot.GeneticAlgorithmBotSettings class type this is, so it encounters a crash that cleans the config.ini back to its default state.

The solution:

We need to set the [ConfigPersist] attribute only to class types that BizHawk recognizes. I split off my custom bot settings into multiple properties, each tagged with [ConfigPersist] attribute, like so:

#region Settings
[ConfigPersist]
public RecentFiles recentFiles {
    get => this.Settings.RecentBotFiles;
    set => this.Settings.RecentBotFiles = value;
}

[ConfigPersist]
public bool TurboWhenBotting {
    get => this.Settings.TurboWhenBotting;
    set => this.Settings.TurboWhenBotting = value;
}

[ConfigPersist]
public bool InvisibleEmulation {
    get => this.Settings.InvisibleEmulation;
    set => this.Settings.InvisibleEmulation = value;
}

public GeneticAlgorithmBotSettings Settings { get; set; }
#endregion

Then, Bizhawk will store the fully qualified name for the known class type RecentFiles as such in the config.ini file:

"$type": "BizHawk.Client.Common.RecentFiles, BizHawk.Client.Common",

While all other types are stored in their primitive types handled by the JSON parser/writer.

The next time BizHawk loads the config.ini file, it will then know what to do with them.