diff --git a/plugin_template/BepInEx/plugins/SpaceWarp/localizations/space_warp_localizations.csv b/plugin_template/BepInEx/plugins/SpaceWarp/localizations/space_warp_localizations.csv index e335e0d..101fa84 100644 --- a/plugin_template/BepInEx/plugins/SpaceWarp/localizations/space_warp_localizations.csv +++ b/plugin_template/BepInEx/plugins/SpaceWarp/localizations/space_warp_localizations.csv @@ -1,48 +1,49 @@ -Key,Type,Description,English,Portuguese BR,French -SpaceWarp/ModList/EnableAll,Text,,Enable All,Ativar todos,Activer tous -SpaceWarp/ModList/DisableAll,Text,,Disable All,Desativar todos,Désactiver tous -SpaceWarp/ModList/RevertChanges,Text,,Revert Changes,Reverter alterações,Rétablir les changements -SpaceWarp/ModList/ApplyChanges,Text,,Apply and restart,Aplicar e reiniciar,Appliquer et redémarrer -SpaceWarp/ModList/multipleChangesDetected,Text,,"{0} changes detected, please restart to apply them","{0} alterações detetadas, porfavor reinicie o jogo","{0} changements détectés, veuillez redémarrer pour les appliquer" -SpaceWarp/ModList/singleChangeDetected,Text,,"1 change detected, please restart to apply it","1 alteração detectada, reinicie para aplicá-la","1 changement détecté, veuillez redémarrer pour l'appliquer" -SpaceWarp/ModList/Header,Text,,spacewarp.modlist,spacewarp.modlist,spacewarp.modlist -SpaceWarp/ModList/CoreMods,Text,,Core Mods,Mods do jogo,Mods du jeu -SpaceWarp/ModList/EnabledMods,Text,,Enabled Mods,Mods ativos,Mods activés -SpaceWarp/ModList/ErroredMods,Text,,Errored Mods,Mods com erros,Mods avec des erreurs -SpaceWarp/ModList/DisabledMods,Text,,Disabled Mods,Mods desativados,Mods désactivés -SpaceWarp/ModList/Version,Text,,Version,Versão,Version -SpaceWarp/ModList/Author,Text,,Author,Autor,Auteur -SpaceWarp/ModList/Source,Text,,Source,Fonte,Source -SpaceWarp/ModList/Description,Text,,Description,Descrição,Description -SpaceWarp/ModList/KSP2Version,Text,,KSP2 Version,Versão do jogo,Version de KSP2 -SpaceWarp/ModList/Dependencies,Text,,Dependencies,Dependências,Dépendances -SpaceWarp/ModList/outdated,Text,,(outdated),(desatualizado),(obsolète) -SpaceWarp/ModList/unsupported,Text,,(unsupported),(não suportado),(non pris en charge) -SpaceWarp/ModList/disabled,Text,,(disabled),(desativado),(désactivé) -SpaceWarp/ModList/mismatched,Text,,(mismatched),(não corresponde com o swinfo),(non concordant) -SpaceWarp/ModList/baddirectory,Text,,(bad directory),(problemas no diretorio),(mauvais dossier) -SpaceWarp/ModList/missingswinfo,Text,,(missing SW Info),(não possui um swinfo),(SW Info manquant) -SpaceWarp/ModList/OpenModSettings,Text,,Mod Settings,Configurações dos mods,Paramètres des mods -SpaceWarp/ModList/OpenModsFolder,Text,,Open Mods Folder,Abrir pasta de mods,Ouvrir le dossier des mods -SpaceWarp/ModList/MissingDependency,Text,,Dependency is missing,Esta dependência não está presente,Cette dépendance est manquante -SpaceWarp/ModList/ErroredDependency,Text,,Dependency had an error while loading,Esta dependência teve um erro durante o loading,Une erreur s'est produite lors du chargement de cette dépendance -SpaceWarp/ModList/DisabledDependency,Text,,Dependency is disabled,Esta dependência esta desativada,Cette dépendance est manquante -SpaceWarp/ModList/UnspecifiedDependency,Text,,Dependency was not specified in SW info,Esta dependência não está presente no swinfo,Cette dépendance n'a pas été spécifiée dans SW Info -SpaceWarp/ModList/UnsupportedDependency,Text,,Dependency is of an unsupported version,Esta dependência é de uma versão nao suportada,Cette dépendance est d'une version non supportée -SpaceWarp/ModList/Conflicts,Text,,Conflicts,Incompatibilidades,Conflits -SpaceWarp/ModList/Details,Text,,Details,Detalhes,Détails -SpaceWarp/Mods,Text,,Mods,Mods,Mods -SpaceWarp/Mods/Outdated,Text,,Mods ⚠,Mods ⚠,Mods ⚠ -SpaceWarp/Mods/Errored,Text,,Mods ⚠,Mods ⚠,Mods ⚠ -SpaceWarp,Text,,Space Warp,Space Warp,Space Warp -SpaceWarp/VersionChecking,Text,,Allow Space Warp to check versions for mods over the network?,Deixar SpaceWarp verificar a versão dos mods na internet?,Autoriser Space Warp à vérifier les versions des mods sur le réseau ? -SpaceWarp/Yes,Text,,Yes,Sim,Oui -SpaceWarp/No,Text,,No,Não,Non -SpaceWarp/Console/Header,Text,,SPACE WARP - Console,SPACE WARP - Consola,SPACE WARP - Console -SpaceWarp/Console/Clear,Text,,Clear,Limpar,Effacer -SpaceWarp/Console/AutoScroll,Text,,Auto Scroll,Scroll automatico,Défilement automatique -SpaceWarp/Console/On,Text,,On,On,On -SpaceWarp/Console/Off,Text,,Off,Off,Off -SpaceWarp/AvcDialog/Title,Text,,spacewarp.avc,spacewarp.avc,spacewarp.avc -SpaceWarp/AvcDialog/MainText,Text,,Allow SpaceWarp to automatically check for mod updates online?,Permitir que o SpaceWarp verifique automaticamente se há atualizações de mods online?,Autoriser SpaceWarp à vérifier automatiquement les mises à jour de mods en ligne ? -SpaceWarp/AvcDialog/MinorText,Text,,*You can change this later in the settings menu.,*Pode alterá-lo mais tarde no menu de definições.,*Vous pouvez modifier plus tard dans le menu des réglages. \ No newline at end of file +Key,Type,Description,English,Portuguese BR,French,Italiano,Chinese-Simplified [zh-CN] +SpaceWarp/ModList/EnableAll,Text,,Enable All,Ativar todos,Activer tous,Abilita tutto,全部启用 +SpaceWarp/ModList/DisableAll,Text,,Disable All,Desativar todos,Désactiver tous,Disabilita tutto,全部禁用 +SpaceWarp/ModList/RevertChanges,Text,,Revert Changes,Reverter alterações,Rétablir les changements,Annulla modifiche,还原更改 +SpaceWarp/ModList/ApplyChanges,Text,,Apply and restart,Aplicar e reiniciar,Appliquer et redémarrer,Applica e riavvia,应用并重新启动 +SpaceWarp/ModList/multipleChangesDetected,Text,,"{0} changes detected, please restart to apply them","{0} alterações detetadas, porfavor reinicie o jogo","{0} changements détectés, veuillez redémarrer pour les appliquer","{0} modifiche rilevate, si prega di riavviare per applicarle","{0}检测到更改,请重新启动以应用它们" +SpaceWarp/ModList/singleChangeDetected,Text,,"1 change detected, please restart to apply it","1 alteração detectada, reinicie para aplicá-la","1 changement détecté, veuillez redémarrer pour l'appliquer","1 modifica rilevata, si prega di riavviare per applicarla","检测到 1 个更改,请重新启动以应用它" +SpaceWarp/ModList/Header,Text,,spacewarp.modlist,spacewarp.modlist,spacewarp.modlist,spacewarp.modlist,模组列表 +SpaceWarp/ModList/CoreMods,Text,,Core Mods,Mods do jogo,Mods du jeu,Mod Principali,核心模组 +SpaceWarp/ModList/EnabledMods,Text,,Enabled Mods,Mods ativos,Mods activés,Mod Abilitate,启用的模组 +SpaceWarp/ModList/ErroredMods,Text,,Errored Mods,Mods com erros,Mods avec des erreurs,Mod con Errori,错误的模组 +SpaceWarp/ModList/DisabledMods,Text,,Disabled Mods,Mods desativados,Mods désactivés,Mod Disabilitate,禁用的模组 +SpaceWarp/ModList/Version,Text,,Version,Versão,Version,Versione,版本 +SpaceWarp/ModList/Author,Text,,Author,Autor,Auteur,Autore,作者 +SpaceWarp/ModList/Source,Text,,Source,Fonte,Source,Fonte,源 +SpaceWarp/ModList/Description,Text,,Description,Descrição,Description,Descrizione,描述 +SpaceWarp/ModList/KSP2Version,Text,,KSP2 Version,Versão do jogo,Version de KSP2,Versione KSP2,KSP2版本 +SpaceWarp/ModList/Dependencies,Text,,Dependencies,Dependências,Dépendances,Dipendenze,依赖 +SpaceWarp/ModList/outdated,Text,,(outdated),(desatualizado),(obsolète),(obsoleto),(已过时) +SpaceWarp/ModList/unsupported,Text,,(unsupported),(não suportado),(non pris en charge),(non supportato),(不支持) +SpaceWarp/ModList/disabled,Text,,(disabled),(desativado),(désactivé),(disabilitata),(已禁用) +SpaceWarp/ModList/mismatched,Text,,(mismatched),(não corresponde com o swinfo),(non concordant),(non corrispondente),(不匹配) +SpaceWarp/ModList/baddirectory,Text,,(bad directory),(problemas no diretorio),(mauvais dossier),(cartella errata),(目录错误) +SpaceWarp/ModList/missingswinfo,Text,,(missing SW Info),(não possui um swinfo),(SW Info manquant),(mancanza di informazioni SW),(缺少SW信息) +SpaceWarp/ModList/OpenModSettings,Text,,Mod Settings,Configurações dos mods,Paramètres des mods,Impostazioni Mod,模组设置 +SpaceWarp/ModList/OpenModsFolder,Text,,Open Mods Folder,Abrir pasta de mods,Ouvrir le dossier des mods,Apri Cartella Mod,打开Mods文件夹 +SpaceWarp/ModList/MissingDependency,Text,,Dependency is missing,Esta dependência não está presente,Cette dépendance est manquante,Mancanza di una dipendenza,缺少依赖项 +SpaceWarp/ModList/ErroredDependency,Text,,Dependency had an error while loading,Esta dependência teve um erro durante o loading,Une erreur s'est produite lors du chargement de cette dépendance,Errore nel caricamento di una dipendenza,依赖项在加载时出错 +SpaceWarp/ModList/DisabledDependency,Text,,Dependency is disabled,Esta dependência esta desativada,Cette dépendance est manquante,Dipendenza disabilitata,依赖项已禁用 +SpaceWarp/ModList/UnspecifiedDependency,Text,,Dependency was not specified in SW info,Esta dependência não está presente no swinfo,Cette dépendance n'a pas été spécifiée dans SW Info,Dipendenza non specificata nelle info SW,未在软件信息中指定依赖项 +SpaceWarp/ModList/UnsupportedDependency,Text,,Dependency is of an unsupported version,Esta dependência é de uma versão nao suportada,Cette dépendance est d'une version non supportée,Dipendenza di una versione non supportata,依赖项的版本不受支持 +SpaceWarp/ModList/Conflicts,Text,,Conflicts,Incompatibilidades,Conflits,Conflitti,冲突 +SpaceWarp/ModList/Details,Text,,Details,Detalhes,Détails,Dettagli,详细信息 +SpaceWarp/Mods,Text,,Mods,Mods,Mods,Mod,模组 +SpaceWarp/Mods/Outdated,Text,,Mods ⚠,Mods ⚠,Mods ⚠,Mod ⚠,模组 ⚠/color> +SpaceWarp/Mods/Errored,Text,,Mods ⚠,Mods ⚠,Mods ⚠,Mod ⚠,模组 ⚠/color> +SpaceWarp,Text,,Space Warp,Space Warp,Space Warp,Space Warp,空间扭曲 +SpaceWarp/VersionChecking,Text,,Allow Space Warp to check versions for mods over the network?,Deixar Space Warp verificar a versão dos mods na internet?,Autoriser Space Warp à vérifier les versions des mods sur le réseau ?,Autorizzare Space Warp a controllare le versioni delle mod su internet?,是否允许 Space Warp 通过网络检查模组的版本? +SpaceWarp/Yes,Text,,Yes,Sim,Oui,Sì,是 +SpaceWarp/No,Text,,No,Não,Non,No,否 +SpaceWarp/Console/Header,Text,,SPACE WARP - Console,SPACE WARP - Consola,SPACE WARP - Console,SPACE WARP - Console,控制台 +SpaceWarp/Console/Clear,Text,,Clear,Limpar,Effacer,Pulisci,清理 +SpaceWarp/Console/AutoScroll,Text,,Auto Scroll,Scroll automatico,Défilement automatique,Scorrimento automatico,自动滚动 +SpaceWarp/Console/On,Text,,On,On,On,On,开 +SpaceWarp/Console/Off,Text,,Off,Off,Off,Off,关 +SpaceWarp/AvcDialog/Title,Text,,spacewarp.avc,spacewarp.avc,spacewarp.avc,spacewarp.avc,spacewarp.avc +SpaceWarp/AvcDialog/MainText,Text,,Allow Space Warp to automatically check for mod updates online?,Permitir que o Space Warp verifique automaticamente se há atualizações de mods online?,Autoriser Space Warp à vérifier automatiquement les mises à jour de mods en ligne ?,Autorizzare Space Warp a controllare automaticamente gli aggiornamenti delle mod online?,允许 Space Warp 自动在线检查模组更新? +SpaceWarp/AvcDialog/MinorText,Text,,*You can change this later in the settings menu.,*Pode alterá-lo mais tarde no menu de definições.,*Vous pouvez modifier plus tard dans le menu des réglages.,*Puoi cambiare questa impostazione più tardi nel menu delle impostazioni.,*您可以稍后在设置菜单中更改此设置。 +SpaceWarp/Menu/Apps,Text,,Apps,Aplicativos,Applications,Applicazioni,应用 diff --git a/plugin_template/BepInEx/plugins/SpaceWarp/swinfo.json b/plugin_template/BepInEx/plugins/SpaceWarp/swinfo.json index 14722a3..3d99f0f 100644 --- a/plugin_template/BepInEx/plugins/SpaceWarp/swinfo.json +++ b/plugin_template/BepInEx/plugins/SpaceWarp/swinfo.json @@ -5,7 +5,7 @@ "name": "Space Warp", "description": "Space Warp is an API for KSP 2 mod developers.", "source": "https://github.com/SpaceWarpDev/SpaceWarp", - "version": "1.8.1", + "version": "1.8.2", "version_check": "https://raw.githubusercontent.com/SpaceWarpDev/SpaceWarp/main/plugin_template/BepInEx/plugins/SpaceWarp/swinfo.json", "ksp2_version": { "min": "0.2.0", diff --git a/src/SpaceWarp.Core/API/Loading/Loading.cs b/src/SpaceWarp.Core/API/Loading/Loading.cs index d85b3b0..90d9d31 100644 --- a/src/SpaceWarp.Core/API/Loading/Loading.cs +++ b/src/SpaceWarp.Core/API/Loading/Loading.cs @@ -80,6 +80,21 @@ public static void AddAddressablesLoadingAction(string name, string label, Ac AddGeneralLoadingAction(() => new AddressableAction(name, label, action)); } + /// + /// Registers an action to be done on addressables after addressables have been loaded. Should be added either on Awake() or Start(). + /// Allows to keep asset in memory after loading them. This is useful for textures or UXML templates, for example. + /// + /// The name of the action + /// The addressables label to hook into + /// Indicates if assets should be kept in memory after loading them. + /// The action to be done on each addressables asset + /// The type of asset that this action is done upon + public static void AddAddressablesLoadingAction(string name, string label, bool keepAssets, Action action) + where T : UnityObject + { + AddGeneralLoadingAction(() => new AddressableAction(name, label, keepAssets, action)); + } + private static Action CreateAssetLoadingActionWithExtensions(string subfolder, Func> importFunction, string[] extensions) diff --git a/src/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs b/src/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs index 6f1b6cf..2fc6fe7 100644 --- a/src/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs +++ b/src/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs @@ -18,6 +18,7 @@ public class AddressableAction : FlowAction where T : UnityObject { private string _label; private Action _action; + private bool _keepAssets; /// /// Creates a new addressable loading action. @@ -30,6 +31,19 @@ public AddressableAction(string name, string label, Action action) : base(nam _label = label; _action = action; } + + /// + /// Creates a new addressable loading action, with the option to keep the asset in memory after loading. + /// This is useful for textures or UXML templates, for example. + /// + /// Name of the action. + /// Label of the asset to load. + /// Action to perform on the loaded asset. + /// Allows to keep asset in memory after loading them. + public AddressableAction(string name, string label, bool keepAssets, Action action) : this(name, label, action) + { + _keepAssets = keepAssets; + } private bool DoesLabelExist(object label) { @@ -55,7 +69,7 @@ public override void DoAction(Action resolve, Action reject) { GameManager.Instance.Assets.LoadByLabel(_label,_action,delegate(IList assetLocations) { - if (assetLocations != null) + if (assetLocations != null && !_keepAssets) { Addressables.Release(assetLocations); } diff --git a/src/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs b/src/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs index a77deae..1d3b206 100644 --- a/src/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs +++ b/src/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs @@ -1,6 +1,8 @@ using HarmonyLib; +using KSP.Game; using KSP.Game.Load; using KSP.IO; +using Newtonsoft.Json; using SpaceWarp.API.SaveGameManager; using SpaceWarp.Backend.SaveGameManager; using SpaceWarp.InternalUtilities; @@ -11,6 +13,24 @@ namespace SpaceWarp.Patching.SaveGameManager; internal class SaveLoadPatches { #region Saving + + /// + /// Common method used before serialization to save plugin data, if any. + /// + private static void SavePluginData(LoadGameData data) + { + // Take the game's LoadGameData, extend it with our own class and copy plugin save data to it + SpaceWarpSerializedSavedGame modSaveData = new(); + InternalExtensions.CopyFieldAndPropertyDataFromSourceToTargetObject(data.SavedGame, modSaveData); + modSaveData.serializedPluginSaveData = ModSaves.InternalPluginSaveData; + data.SavedGame = modSaveData; + + // Initiate save callback for plugins that specified a callback function + foreach (var plugin in ModSaves.InternalPluginSaveData) + { + plugin.SaveEventCallback(plugin.SaveData); + } + } [HarmonyPatch(typeof(SerializeGameDataFlowAction), MethodType.Constructor, [typeof(string), typeof(LoadGameData)])] [HarmonyPostfix] @@ -27,26 +47,122 @@ SerializeGameDataFlowAction __instance return; } - // Take the game's LoadGameData, extend it with our own class and copy plugin save data to it - SpaceWarpSerializedSavedGame modSaveData = new(); - InternalExtensions.CopyFieldAndPropertyDataFromSourceToTargetObject(data.SavedGame, modSaveData); - modSaveData.serializedPluginSaveData = ModSaves.InternalPluginSaveData; - data.SavedGame = modSaveData; - - // Initiate save callback for plugins that specified a callback function - foreach (var plugin in ModSaves.InternalPluginSaveData) + SavePluginData(data); + } + + /// + /// Handles save game serialization in memory, like when launching from VAB. Current + /// game is serialized to a buffer and kept in memory. + /// + [HarmonyPatch(typeof(SerializeGameToMemoryFlowAction), MethodType.Constructor, [typeof(LoadOrSaveCampaignTicket)])] + [HarmonyPostfix] + private static void InjectToMemoryPluginSaveGameData( + LoadOrSaveCampaignTicket loadOrSaveCampaignTicket, + // ReSharper disable once InconsistentNaming + SerializeGameToMemoryFlowAction __instance + ) + { + // Skip plugin data injection if there are no mods that have registered for save/load actions + if (ModSaves.InternalPluginSaveData.Count == 0) { - plugin.SaveEventCallback(plugin.SaveData); + return; } + + SavePluginData(loadOrSaveCampaignTicket.LoadGameData); } #endregion #region Loading + /// + /// Common method used after deserialization to load plugin data, if any. + /// + private static void LoadPluginSaveData(SpaceWarpSerializedSavedGame serializedSavedGame) + { + // Perform plugin load data if plugin data is found in the save file + if (serializedSavedGame.serializedPluginSaveData.Count <= 0) return; + + // Iterate through each plugin + foreach (var loadedData in serializedSavedGame.serializedPluginSaveData) + { + // Match registered plugin GUID with the GUID found in the save file + var existingData = ModSaves.InternalPluginSaveData.Find( + p => p.ModGuid == loadedData.ModGuid + ); + if (existingData == null) + { + SpaceWarpPlugin.Instance.SWLogger.LogWarning( + $"Saved data for plugin '{loadedData.ModGuid}' found during a load event, however " + + $"that plugin isn't registered for save/load events. Skipping load for this plugin." + ); + continue; + } + + // Perform a callback if plugin specified a callback function. This is done before plugin data is + // actually updated. + existingData.LoadEventCallback(loadedData.SaveData); + + // Copy loaded data to the SaveData object plugin registered + InternalExtensions.CopyFieldAndPropertyDataFromSourceToTargetObject( + loadedData.SaveData, + existingData.SaveData + ); + } + } + + /// + /// DeserializeBufferFlowAction is used when reverting to VAB / Launch from flight + /// + [HarmonyPatch(typeof(DeserializeBufferFlowAction), "DoAction")] + [HarmonyPrefix] + private static bool DeserializeBufferLoadedPluginData( + Action resolve, + Action reject, + // ReSharper disable once InconsistentNaming + DeserializeBufferFlowAction __instance + ) + { + // Skip plugin deserialization if there are no mods that have registered for save/load actions + if (ModSaves.InternalPluginSaveData.Count == 0) + { + return true; + } + + __instance._game.UI.SetLoadingBarText(__instance.Description); + try + { + if (DeserializeBufferFlowAction._ignoreNullValueSerialzationSettings == null) + { + DeserializeBufferFlowAction._ignoreNullValueSerialzationSettings = IOProvider.CloneSerializerSettings(IOProvider.GetDefaultSerializerSettings()); + DeserializeBufferFlowAction._ignoreNullValueSerialzationSettings.NullValueHandling = NullValueHandling.Ignore; + } + + // Deserialize save buffer to our own class that extends game's SerializedSavedGame + var serializedSavedGame = IOProvider.FromBuffer(__instance._savedGameBuffer, DeserializeBufferFlowAction._ignoreNullValueSerialzationSettings); + __instance._data.SavedGame = serializedSavedGame; + __instance._data.DataLength = (long) __instance._savedGameBuffer.Length; + + // Perform plugin load data if plugin data is found in the save file + LoadPluginSaveData(serializedSavedGame); + } + catch (Exception ex) + { + UnityEngine.Debug.LogException(ex); + reject(ex.Message); + } + + resolve(); + + return false; + } + + /// + /// DeserializeContentsFlowAction is used when loading a save file + /// [HarmonyPatch(typeof(DeserializeContentsFlowAction), "DoAction")] [HarmonyPrefix] - private static bool DeserializeLoadedPluginData( + private static bool DeserializeContentsLoadedPluginData( Action resolve, Action reject, // ReSharper disable once InconsistentNaming @@ -68,35 +184,7 @@ DeserializeContentsFlowAction __instance __instance._data.DataLength = IOProvider.GetFileSize(__instance._filename); // Perform plugin load data if plugin data is found in the save file - if (serializedSavedGame.serializedPluginSaveData.Count > 0) - { - // Iterate through each plugin - foreach (var loadedData in serializedSavedGame.serializedPluginSaveData) - { - // Match registered plugin GUID with the GUID found in the save file - var existingData = ModSaves.InternalPluginSaveData.Find( - p => p.ModGuid == loadedData.ModGuid - ); - if (existingData == null) - { - SpaceWarpPlugin.Instance.SWLogger.LogWarning( - $"Saved data for plugin '{loadedData.ModGuid}' found during a load event, however " + - $"that plugin isn't registered for save/load events. Skipping load for this plugin." - ); - continue; - } - - // Perform a callback if plugin specified a callback function. This is done before plugin data is - // actually updated. - existingData.LoadEventCallback(loadedData.SaveData); - - // Copy loaded data to the SaveData object plugin registered - InternalExtensions.CopyFieldAndPropertyDataFromSourceToTargetObject( - loadedData.SaveData, - existingData.SaveData - ); - } - } + LoadPluginSaveData(serializedSavedGame); } catch (Exception ex) { diff --git a/src/SpaceWarp.Game/API/Game/Waypoints/SerializedWaypoint.cs b/src/SpaceWarp.Game/API/Game/Waypoints/SerializedWaypoint.cs new file mode 100644 index 0000000..ea136fe --- /dev/null +++ b/src/SpaceWarp.Game/API/Game/Waypoints/SerializedWaypoint.cs @@ -0,0 +1,49 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace SpaceWarp.API.Game.Waypoints; + +/// +/// This contains the serialized information for a waypoint, used for saving/loading waypoints +/// +[Serializable] +[method: JsonConstructor] +[PublicAPI] +public class SerializedWaypoint(string name, string bodyName, double latitude, double longitude, double altitude, WaypointState state) +{ + /// + /// The name of the waypoint + /// + public string Name => name; + + /// + /// The body the waypoint is on + /// + public string BodyName => bodyName; + + /// + /// The latitude of the waypoint + /// + public double Latitude => latitude; + + /// + /// The longitude of the waypoint + /// + public double Longitude => longitude; + + /// + /// The altitude of the waypoint + /// + public double Altitude => altitude; + + /// + /// The current state of the waypoint + /// + public WaypointState State => state; + + /// + /// Deserializes the waypoint, creating an actual waypoint from it + /// + /// A newly created waypoint from the serialized waypoint's parameters + public virtual Waypoint Deserialize() => new(latitude, longitude, altitude, bodyName, name, state); +} \ No newline at end of file diff --git a/src/SpaceWarp.Game/API/Game/Waypoints/Waypoint.cs b/src/SpaceWarp.Game/API/Game/Waypoints/Waypoint.cs new file mode 100644 index 0000000..502143d --- /dev/null +++ b/src/SpaceWarp.Game/API/Game/Waypoints/Waypoint.cs @@ -0,0 +1,241 @@ +using JetBrains.Annotations; +using KSP.Game; +using KSP.Sim; +using KSP.Sim.Definitions; +using KSP.Sim.impl; +using Newtonsoft.Json; + +namespace SpaceWarp.API.Game.Waypoints; + +/// +/// A handle for a waypoint in the flight/map view for KSP2 +/// +[PublicAPI] +public class Waypoint +{ + + + private SimulationObjectModel _waypointObject; + + /// + /// The current name of the waypoint + /// + public string Name => State == WaypointState.Visible ? _waypointObject.Name : _hiddenRename; + + /// + /// The current body that the waypoint is placed on + /// + public string BodyName { get; private set; } + + /// + /// The current latitude of the waypoint + /// + public double Latitude { get; private set; } + + /// + /// The current longitude of the waypoint + /// + public double Longitude { get; private set; } + + /// + /// The current altitude of the waypoint + /// + public double AltitudeFromRadius { get; private set; } + + [CanBeNull] private string _hiddenRename; + + private WaypointState _state = WaypointState.Visible; + + /// + /// Set the state of the waypoint to either being hidden/shown + /// + /// Thrown when trying to set the state of a destroyed waypoint + public WaypointState State + { + get => _state; + set + { + if (_isDestroyed) + { + throw new Exception("Waypoint was already destroyed"); + } + + if (value == _state) return; + _state = value; + if (value == WaypointState.Hidden) + { + _hiddenRename = Name; + _waypointObject.Destroy(); + _waypointObject = null; + } + else + { + var spaceSimulation = GameManager.Instance.Game.SpaceSimulation; + var celestialBodies = GameManager.Instance.Game.UniverseModel.GetAllCelestialBodies(); + var body = celestialBodies.Find(c => c.Name == BodyName); + if (body == null) + throw new Exception($"Could not create waypoint as there is no body with the name of {BodyName}"); + var waypointComponentDefinition = new WaypointComponentDefinition { Name = _hiddenRename }; + _waypointObject = spaceSimulation.CreateWaypointSimObject( + waypointComponentDefinition, body, Latitude, Longitude, AltitudeFromRadius); + _hiddenRename = null; + } + } + } + + private bool _isDestroyed; + + /// + /// Whether or not this waypoint has been destroyed + /// + public bool IsDestroyed => _isDestroyed || _waypointObject is { _isDestroyed: true }; + + private static long _nextID; + + /// + /// Creates a waypoint handle from a preexisting waypoint + /// + /// The preexisting waypoint + public Waypoint(WaypointComponent preexistingWaypoint) + { + _waypointObject = preexistingWaypoint.SimulationObject; + var body = _waypointObject.transform.parent.transform.objectModel.CelestialBody; + BodyName = body.Name; + body.GetLatLonAltFromRadius(_waypointObject.transform.Position, out var latitude, out var longitude, + out var altitudeFromRadius); + Latitude = latitude; + Longitude = longitude; + AltitudeFromRadius = altitudeFromRadius; + } + + /// + /// Create a new waypoint at the specified location + /// + /// The latitude of the waypoint + /// The longitude of the waypoint + /// The altitude of the waypoint, if null it defaults to the height of the terrain at the specified latitude/longitude + /// The body that the waypoint is around, if null it defaults to the current active vehicles body + /// The name of the waypoint, if null defaults to Waypoint-{sequential_number} + /// The initial state of the waypoint, whether or not it is visible + /// Thrown if there is no body with the name bodyName + public Waypoint(double latitude, double longitude, double? altitudeFromRadius = null, + [CanBeNull] string bodyName = null, [CanBeNull] string name = null, + WaypointState waypointState = WaypointState.Visible) + { + BodyName = bodyName ??= Vehicle.ActiveSimVessel.mainBody.Name; + Latitude = latitude; + Longitude = longitude; + var spaceSimulation = GameManager.Instance.Game.SpaceSimulation; + var celestialBodies = GameManager.Instance.Game.UniverseModel.GetAllCelestialBodies(); + var body = celestialBodies.Find(c => c.Name == bodyName); + if (body == null) + throw new Exception($"Could not create waypoint as there is no body with the name of {bodyName}"); + altitudeFromRadius ??= body.SurfaceProvider.GetTerrainAltitudeFromCenter(latitude, longitude) - body.radius; + AltitudeFromRadius = altitudeFromRadius.Value; + _state = waypointState; + if (_state == WaypointState.Visible) + { + var waypointComponentDefinition = new WaypointComponentDefinition + { Name = name ?? $"Waypoint-{_nextID++}" }; + _waypointObject = spaceSimulation.CreateWaypointSimObject( + waypointComponentDefinition, body, latitude, longitude, altitudeFromRadius.Value); + } + else + { + _hiddenRename = name ?? $"Waypoint-{_nextID++}"; + } + } + + /// + /// Destroys this waypoint + /// + /// Thrown if the waypoint was already destroyed + public void Destroy() + { + if (IsDestroyed) + { + throw new Exception("Waypoint was already destroyed"); + } + + if (State == WaypointState.Visible) + { + _waypointObject.Destroy(); + _waypointObject = null; + } + + _isDestroyed = true; + } + + /// + /// Moves a waypoint to another position + /// + /// The new latitude of the waypoint + /// The new longitude of the waypoint + /// The altitude of the waypoint, if null it defaults to the height of the terrain at the specified latitude/longitude + /// The body that the waypoint is around, if null it defaults to the waypoints current body + /// Thrown if the waypoint is destroyed, or if there is no waypoint with the name bodyName + public void Move(double latitude, double longitude, double? altitudeFromRadius = null, + [CanBeNull] string bodyName = null) + { + if (IsDestroyed) + { + throw new Exception("Waypoint was already destroyed"); + } + + bodyName ??= BodyName; + var celestialBodies = GameManager.Instance.Game.UniverseModel.GetAllCelestialBodies(); + var body = celestialBodies.Find(c => c.Name == bodyName); + if (body == null) + throw new Exception($"Could not create waypoint as there is no body with the name of {bodyName}"); + altitudeFromRadius ??= body.SurfaceProvider.GetTerrainAltitudeFromCenter(latitude, longitude) - body.radius; + BodyName = bodyName; + Latitude = latitude; + Longitude = longitude; + AltitudeFromRadius = altitudeFromRadius.Value; + if (_state != WaypointState.Visible) return; + var bodyFrame = body.transform.bodyFrame; + var relSurfacePosition = body.GetRelSurfacePosition(latitude, longitude, altitudeFromRadius.Value); + _waypointObject.transform.parent = bodyFrame; + _waypointObject.transform.Position = new Position(bodyFrame, relSurfacePosition); + } + + /// + /// Renames a waypoint + /// + /// The new name for the waypoint, if null defaults to Waypoint-{sequential_number} + /// Thrown if the waypoint is destroyed + public void Rename([CanBeNull] string name = null) + { + if (IsDestroyed) + { + throw new Exception("Waypoint was already destroyed"); + } + + if (State == WaypointState.Visible) + { + _waypointObject.Name = name ?? $"Waypoint-{_nextID++}"; + } + else + { + _hiddenRename = _waypointObject.Name; + } + } + + + /// + /// Hides the waypoint + /// + public void Hide() => State = WaypointState.Hidden; + + /// + /// Shows the waypoint + /// + public void Show() => State = WaypointState.Visible; + + /// + /// Serializes the waypoint to be saved in save data + /// + /// The waypoint as a serialized record + public virtual SerializedWaypoint Serialize() => + new(Name, BodyName, Latitude, Longitude, AltitudeFromRadius, State); +} \ No newline at end of file diff --git a/src/SpaceWarp.Game/API/Game/Waypoints/WaypointState.cs b/src/SpaceWarp.Game/API/Game/Waypoints/WaypointState.cs new file mode 100644 index 0000000..56c8fc1 --- /dev/null +++ b/src/SpaceWarp.Game/API/Game/Waypoints/WaypointState.cs @@ -0,0 +1,17 @@ +namespace SpaceWarp.API.Game.Waypoints; + +/// +/// This contains the state for a waypoint +/// +public enum WaypointState +{ + /// + /// The waypoint is shown in the flight/map view + /// + Visible, + + /// + /// The waypoint is hidden in the flight/map view + /// + Hidden +} \ No newline at end of file diff --git a/src/SpaceWarp.Game/Modules/Game.cs b/src/SpaceWarp.Game/Modules/Game.cs index c9db243..b6a3944 100644 --- a/src/SpaceWarp.Game/Modules/Game.cs +++ b/src/SpaceWarp.Game/Modules/Game.cs @@ -1,7 +1,14 @@ -using JetBrains.Annotations; +using System.Reflection; +using HarmonyLib; +using JetBrains.Annotations; using KSP.Game; using KSP.Messages; +using SpaceWarp.API.Assets; using SpaceWarp.API.Game.Messages; +using SpaceWarp.API.Game.Waypoints; +using UnityEngine; +using ILogger = SpaceWarp.API.Logging.ILogger; +using Object = UnityEngine.Object; namespace SpaceWarp.Modules; @@ -11,9 +18,18 @@ namespace SpaceWarp.Modules; [UsedImplicitly] public class Game : SpaceWarpModule { + + internal static ILogger Logger; /// public override string Name => "SpaceWarp.Game"; + /// + public override void PreInitializeModule() + { + Logger = ModuleLogger; + Harmony.CreateAndPatchAll(typeof(Patches.MapPatches)); + } + /// public override void InitializeModule() { diff --git a/src/SpaceWarp.Game/Patches/MapPatches.cs b/src/SpaceWarp.Game/Patches/MapPatches.cs new file mode 100644 index 0000000..0d0c3e5 --- /dev/null +++ b/src/SpaceWarp.Game/Patches/MapPatches.cs @@ -0,0 +1,18 @@ +using HarmonyLib; +using KSP.Map; +using KSP.Sim; +using KSP.Sim.impl; +using UnityEngine; + +namespace SpaceWarp.Patches; + +[HarmonyPatch] +internal static class MapPatches +{ + [HarmonyPatch(typeof(Map3DView), nameof(Map3DView.ProcessSingleMapItem))] + [HarmonyPrefix] + // ReSharper disable once InconsistentNaming + public static bool SkipBadItems(Map3DView __instance, MapItem item) => + item.MapItemType != MapItemType.Waypoint || + ((ISimulationModelMap)__instance.Game.UniverseModel).FromGlobalId(item.SimGUID) != null; +} \ No newline at end of file diff --git a/src/SpaceWarp.Game/SpaceWarp.Game.csproj b/src/SpaceWarp.Game/SpaceWarp.Game.csproj index d90289f..0959ba0 100644 --- a/src/SpaceWarp.Game/SpaceWarp.Game.csproj +++ b/src/SpaceWarp.Game/SpaceWarp.Game.csproj @@ -1,10 +1,18 @@ + + SpaceWarp + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/SpaceWarp.UI/Backend/UI/Appbar/AppbarBackend.cs b/src/SpaceWarp.UI/Backend/UI/Appbar/AppbarBackend.cs index 1169473..ed55555 100644 --- a/src/SpaceWarp.UI/Backend/UI/Appbar/AppbarBackend.cs +++ b/src/SpaceWarp.UI/Backend/UI/Appbar/AppbarBackend.cs @@ -95,7 +95,7 @@ public static GameObject AddButton(string buttonText, Sprite buttonIcon, string var localizer = text.gameObject.GetComponent(); if (localizer) { - UnityObject.Destroy(localizer); + localizer.Term = buttonText; } // Change the icon. @@ -260,7 +260,7 @@ public static void AddOABButton(string buttonText, Sprite buttonIcon, string but var localizer = text.gameObject.GetComponent(); if (localizer) { - UnityObject.Destroy(localizer); + localizer.Term = buttonText; } // Change the icon. @@ -303,21 +303,28 @@ private static void SetOABTrayState(bool state) #region KSC App Bar private static GameObject _kscTray; - + // ReSharper disable once InconsistentNaming private static GameObject KSCTray { get { if (_kscTray == null) - { - return _kscTray = CreateKSCTray(); + { + return _kscTray = CreateKSCTray(); } return _kscTray; } } + private const string KscMenuPath = + "GameManager/Default Game Instance(Clone)/UI Manager(Clone)/Main Canvas/KSCMenu(Clone)/Panel/Window-FacilityMenu/GRP-Body/Content/Menu"; + + private const string FlyoutName = "LaunchLocationFlyoutHeaderToggle"; + + private const string TargetName = "LaunchLocationsFlyoutTarget"; + // ReSharper disable once InconsistentNaming private static GameObject CreateKSCTray() { @@ -326,8 +333,8 @@ private static GameObject CreateKSCTray() // Find the KSC launch locations menu item; it will be used for cloning the app tray // Get the Launch Pads menu item - var kscMenu = GameObject.Find("GameManager/Default Game Instance(Clone)/UI Manager(Clone)/Main Canvas/KSCMenu(Clone)/LandingPanel/InteriorWindow/MenuButtons/Content/Menu"); - var launchLocationsButton = kscMenu != null ? kscMenu.GetChild("LaunchLocationFlyoutHeaderToggle") : null; + var kscMenu = GameObject.Find(KscMenuPath); + var launchLocationsButton = kscMenu != null ? kscMenu.GetChild(FlyoutName) : null; if (kscMenu == null || launchLocationsButton == null) { @@ -340,38 +347,57 @@ private static GameObject CreateKSCTray() kscAppTrayButton.name = "KSC-AppTrayButton"; // Set the button icon (use OAB app tray icon) - var image = kscAppTrayButton.GetChild("Header").GetChild("Content").GetChild("Icon Panel").GetChild("icon").GetComponent(); + var image = kscAppTrayButton.GetChild("ICO-Launchpad").GetComponent(); var tex = AssetManager.GetAsset($"{SpaceWarpPlugin.ModGuid}/images/oabTrayButton.png"); tex.filterMode = FilterMode.Point; image.sprite = Sprite.Create(tex, new Rect(0, 0, 32, 32), new Vector2(0.5f, 0.5f)); // Change the text to APPS - var title = kscAppTrayButton.GetChild("Header").GetChild("Content").GetChild("Title"); + var title = kscAppTrayButton.GetChild("TXT-Launchpad"); { // Suppress renaming of the button to Launchpad + title.GetComponent().text = "Apps"; // Set the text in case term does not exist var localizer = title.GetComponent(); - if (localizer) - { - UnityObject.Destroy(localizer); - } - var text = title.GetComponent(); - text.text = "Apps"; + localizer.Term = "SpaceWarp/Menu/Apps"; } // Get the popup tray and rename it - var kscAppTray = kscAppTrayButton.GetChild("LaunchLocationsFlyoutTarget"); + var originalFlyout = kscMenu.GetChild(TargetName); + var originalToggle = launchLocationsButton.GetComponent(); + var kscAppTray = UnityObject.Instantiate(originalFlyout, kscMenu.transform); kscAppTray.name = "KSC-AppTray"; + // Delete existing buttons and separators in the tray for (var i = 0; i < kscAppTray.transform.childCount; i++) { var child = kscAppTray.transform.GetChild(i); - + // Destroy all objects inside the tray, but keep the arrow ("thingy") that points to the menu button - if (!child.name.ToLowerInvariant().Contains("thingy")) + if (!child.name.ToLowerInvariant().Contains("bg-panel")) UnityObject.Destroy(child.gameObject); } + var toggleFlyout = kscAppTrayButton.GetComponent(); + toggleFlyout.OnDisable(); + toggleFlyout._target = kscAppTray; + toggleFlyout._toggle = kscAppTrayButton.GetComponent(); + toggleFlyout._toggleCanvas = kscAppTray.GetComponent(); + toggleFlyout.OnEnable(); + toggleFlyout._toggleCanvas.enabled = true; + toggleFlyout._toggle.onValueChanged.AddListener(x => + { + if (!x) return; + if (!originalToggle.isOn) return; + originalToggle.Set(false); + }); + originalToggle.onValueChanged.AddListener(x => + { + if (!x) return; + if (!toggleFlyout._toggle.isOn) return; + toggleFlyout._toggle.Set(false); + }); + Logger.LogInfo("Created KSC app tray."); return kscAppTray; @@ -386,7 +412,7 @@ public static void AddKSCButton(string buttonText, Sprite buttonIcon, string but // Find the Launchpad_1 button. var kscLaunchLocationsFlyoutTarget = GameObject.Find( - "GameManager/Default Game Instance(Clone)/UI Manager(Clone)/Main Canvas/KSCMenu(Clone)/LandingPanel/InteriorWindow/MenuButtons/Content/Menu/LaunchLocationFlyoutHeaderToggle/LaunchLocationsFlyoutTarget"); + $"{KscMenuPath}/{TargetName}"); var launchPadButton = kscLaunchLocationsFlyoutTarget != null ? kscLaunchLocationsFlyoutTarget.GetChild("Launchpad_1") : null; if (launchPadButton == null) @@ -400,18 +426,19 @@ public static void AddKSCButton(string buttonText, Sprite buttonIcon, string but modButton.name = buttonId; // Change the text - var modText = modButton.GetChild("Content").GetChild("Text (TMP)").GetComponent(); + var modText = modButton.GetChild("TXT-LocationName").GetComponent(); modText.text = buttonText; // Suppress renaming of the button var localizer = modText.gameObject.GetComponent(); if (localizer) { - UnityObject.Destroy(localizer); + // UnityObject.Destroy(localizer); + localizer.Term = buttonText; } // Change the icon - var icon = modButton.GetChild("Icon"); + var icon = modButton.GetChild("ICO-Location"); var image = icon.GetComponent(); image.sprite = buttonIcon; diff --git a/src/SpaceWarp.UI/Modules/UI.cs b/src/SpaceWarp.UI/Modules/UI.cs index eed59bc..0531896 100644 --- a/src/SpaceWarp.UI/Modules/UI.cs +++ b/src/SpaceWarp.UI/Modules/UI.cs @@ -1,5 +1,6 @@ using HarmonyLib; using JetBrains.Annotations; +using KSP.UI.Flight; using SpaceWarp.API.Assets; using SpaceWarp.API.Configuration; using SpaceWarp.API.UI.Appbar; @@ -86,6 +87,7 @@ public override void PreInitializeModule() /// public override void InitializeModule() { + ModuleLogger.LogInfo("Initializing UI"); if (VersionChecking.Instance.ConfigFirstLaunch.Value) {