BizHawk
BizHawk copied to clipboard
API Hawk: Cannot load configuration file when attempting to load persisted configurations for external tools.
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
- Use any blank C# project that is set up to create an external tool for BizHawk.
- Declare a class type with the
[ConfigPersist]
attribute and create a class type with any properties (int
,float
, or evenRecentFiles
class type). - Build the external tool.
- Load the external tool in EmuHawk just 1 time.
- Close the external tool window (Important step!)
- Close EmuHawk.
- Open EmuHawk again.
- 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.
- BizHawk dev build at 070e7035b3e3ad2b28c1a25b0506e4ba8975cf9c; Win10 Pro 21H1; Intel/NVIDIA
Can this be solved the same way core settings/syncsettings was @nattthebear?
On quick glance, it seems like it could be, yes.
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.