diff --git a/HKVizMod/CharmsExport.cs b/HKVizMod/CharmsExport.cs new file mode 100644 index 0000000..90c9b4b --- /dev/null +++ b/HKVizMod/CharmsExport.cs @@ -0,0 +1,47 @@ +using Modding; +using Satchel; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace HKViz { + internal class CharmsExport : Loggable { + class CharmInfo { + public string charmId; + public string spriteName; + } + + + private static CharmsExport? _instance; + public static CharmsExport Instance { + get { + if (_instance != null) return _instance; + _instance = new CharmsExport(); + return _instance; + } + } + + + public void Export() { + var go = GameObject.FindObjectOfType().transform.Find("Collected Charms"); + var sprites = go.GetComponentsInChildren(); + Log("sprites length" + sprites.Length); + + var infos = sprites.Where(it => it.gameObject.name == "Sprite").Select(it => new CharmInfo { + charmId = it.transform.parent.gameObject.name, + spriteName = it.sprite.name, + }).ToList(); + + Debug.Log("infos" + infos.Count); + + var json = Json.Stringify(infos); + using (var writer = new StreamWriter(StoragePaths.GetUserFilePath("charms-inventory-export.txt"))) { + writer.Write(json); + } + } + } +} diff --git a/HKVizMod/Constants.cs b/HKVizMod/Constants.cs index 17757d7..e6d34bb 100644 --- a/HKVizMod/Constants.cs +++ b/HKVizMod/Constants.cs @@ -3,11 +3,17 @@ internal static class Constants { public static string WEBSITE_DISPLAY_LINK = "hkviz.org"; public static string WEBSITE_LINK = "https://www.hkviz.org"; + // ----- PRODUCTION ----- public static string API_URL = "https://www.hkviz.org/api/rest/"; public static string LOGIN_URL = "https://www.hkviz.org/ingameauth/"; + public static string API_URL_SUFFIX = ""; + // ----- LOCAL ----- //public static string API_URL = "http://localhost:3000/api/rest/"; //public static string LOGIN_URL = "http://localhost:3000/ingameauth/"; + //public static string API_URL_SUFFIX = ""; + + public static string RECORDER_FILE_VERSION = "1.6.0"; public static string GetVersion() => typeof(Constants).Assembly.GetName().Version.ToString(); } diff --git a/HKVizMod/EnemiesExport.cs b/HKVizMod/EnemiesExport.cs new file mode 100644 index 0000000..d628f63 --- /dev/null +++ b/HKVizMod/EnemiesExport.cs @@ -0,0 +1,58 @@ +using Modding; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace HKViz { + internal class EnemiesExport : Loggable { + class EnemyJournalInfo { + public string portraitName; + public string convoName; + public string descConvo; + public string nameConvo; + public string notesConvo; + public string playerDataBoolName; + public string playerDataKillsName; + public string playerDataName; + public string playerDataNewDataName; + } + + + private static EnemiesExport? _instance; + public static EnemiesExport Instance { + get { + if (_instance != null) return _instance; + _instance = new EnemiesExport(); + return _instance; + } + } + + + public void Export() { + var journalList = GameObject.FindObjectOfType(); + var listItems = journalList.transform.GetComponentsInChildren(true); + + var enemies = listItems.Select(e => new EnemyJournalInfo { + portraitName = e.transform.GetChild(0).GetComponent().sprite.name, + convoName = e.convoName, + descConvo = e.descConvo, + nameConvo = e.nameConvo, + notesConvo = e.notesConvo, + playerDataBoolName = e.playerDataBoolName, + playerDataKillsName = e.playerDataKillsName, + playerDataName = e.playerDataName, + playerDataNewDataName = e.playerDataNewDataName, + }).ToList(); + + + var json = Json.Stringify(enemies); + using (var writer = new StreamWriter(StoragePaths.GetUserFilePath("enemies-journal-export.txt"))) { + writer.Write(json); + } + } + } +} diff --git a/HKVizMod/HKVizAuthManager.cs b/HKVizMod/HKVizAuthManager.cs index 1694734..33d84fc 100644 --- a/HKVizMod/HKVizAuthManager.cs +++ b/HKVizMod/HKVizAuthManager.cs @@ -96,7 +96,7 @@ public void GlobalSettingsLoaded() { } private void Application_focusChanged(bool focused) { - Log("Focus changed"); + // Log("Focus changed"); if (focused && State == LoginState.WAITING_FOR_USER_LOGIN_IN_BROWSER) { CheckSessionState(fromSettings: false); } diff --git a/HKVizMod/HKVizMod.cs b/HKVizMod/HKVizMod.cs index 4386855..86390ef 100644 --- a/HKVizMod/HKVizMod.cs +++ b/HKVizMod/HKVizMod.cs @@ -162,6 +162,9 @@ private void InitFsmIfNotAlreadyHappened() { recording.WriteEntry(RecordingPrefixes.SPELL_UP); }); + // ----- HEALING / FOCUSING ----- + PlayerHealthWriter.Instance.InitFsm(); + // ----- NAIL ARTS ----- // cyclone Hooks.HookStateEntered(new FSMData( @@ -259,6 +262,7 @@ private void InitFsmIfNotAlreadyHappened() { private void HeroUpdateHook() { var unixMillis = recording.GetUnixMillis(); + KnightManager.Instance.UpdateKnight(); ModWriter.Instance.OnKnightUpdate(); GameManagerWriter.Instance.WriteChangedFields(unixMillis); PlayerPositionWriter.Instance.WritePositionsIfNeeded(unixMillis); @@ -318,7 +322,7 @@ public MenuScreen GetMenuScreen(MenuScreen modListMenu, ModToggleDelegates? togg => HKVizModUI.Instance.GetMenuScreen(modListMenu, toggleDelegates); public void OnLoadGlobal(GlobalSettings s) { - Log("Steam-user" + GameLauncherUser.Instance.GetUserId()); + //Log("Steam-user" + GameLauncherUser.Instance.GetUserId()); GlobalSettingsManager.Instance.InitializeFromSavedSettings(s); HKVizAuthManager.Instance.GlobalSettingsLoaded(); UploadManager.Instance.GlobalSettingsLoaded(); diff --git a/HKVizMod/HKVizMod.csproj b/HKVizMod/HKVizMod.csproj index bdae54c..989d778 100644 --- a/HKVizMod/HKVizMod.csproj +++ b/HKVizMod/HKVizMod.csproj @@ -10,7 +10,7 @@ Copyright © Oliver Grack 2024 Oliver Grack 7035 - 1.5.1 + 1.6.0 false bin\$(Configuration)\ latest diff --git a/HKVizMod/KnightManager.cs b/HKVizMod/KnightManager.cs new file mode 100644 index 0000000..b18e75f --- /dev/null +++ b/HKVizMod/KnightManager.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace HKViz { + internal class KnightManager { + private static KnightManager instance; + public static KnightManager Instance { + get { + if (instance == null) { + instance = new KnightManager(); + } + return instance; + } + } + + + private Transform? knight; + + public Transform? Knight => knight; + + public void UpdateKnight() { + if (knight == null) { + // if destroyed needs to find new player + knight = GameObject.Find("Knight").transform; + if (knight != null) { + //OnNewKnightFound?.Invoke(knight); + PlayerHealthWriter.Instance.InitNewKnight(knight); + } + } + } + + //public event Action? OnNewKnightFound; + } +} diff --git a/HKVizMod/MapExport.cs b/HKVizMod/MapExport.cs index 79aeecb..be67d2f 100644 --- a/HKVizMod/MapExport.cs +++ b/HKVizMod/MapExport.cs @@ -2,6 +2,7 @@ using Modding; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using UnityEngine; @@ -18,7 +19,18 @@ public static MapExport Instance { } } - + private string getObjectPath(GameObject go, GameObject relativeToParent) { + var builder = new StringBuilder(); + var current = go; + while (current != null && current != relativeToParent) { + if (go != current) { + builder.Insert(0, "/"); + } + builder.Insert(0, current.name); + current = current.transform.parent.gameObject; + } + return builder.ToString(); + } public MapData Export() { Log("Started map export. This should run at the beginning of the game, after buying the first map, but before buying the quill"); @@ -29,9 +41,40 @@ public MapData Export() { var gameMapGO = GameObject.Find("Game_Map(Clone)"); var gameMapPos = gameMapGO.transform.position; + TextData textDataFromObjects(SetTextMeshProGameText it) { + var current = it.gameObject; + while (current != null) { + current.SetActive(true); + current = current.transform.parent?.gameObject; + } + var tmp = it.GetComponent(); + tmp.ForceMeshUpdate(true); + return new TextData( + objectPath: getObjectPath(it.gameObject, gameMapGO), + convoName: it.convName, + sheetName: it.sheetName, + position: it.transform.position - gameMapPos, + fontSize: tmp.fontSize, + fontWeight: tmp.fontWeight, + bounds: new ExportBounds( + min: it.transform.TransformPoint(tmp.textBounds.min) - gameMapPos, + max: it.transform.TransformPoint(tmp.textBounds.max) - gameMapPos + ), + origColor: tmp.color + ); + } + + var areaNames = gameMapGO.GetComponentsInChildren(true) + .Select(areaName => textDataFromObjects(areaName.GetComponent())) + .ToList(); + foreach (var m in gameMapGO.GetComponentsInChildren(true)) { - var rsd = m.Rsd; + var roomSpriteDefinition = m.Rsd; + var textDatas = m.gameObject.GetComponentsInChildren(true) + //.Where(it => it.gameObject.name.Contains("Area Name")) + .Select(it => textDataFromObjects(it)) + .ToList(); //Log(m.Rsd?.MapZone); @@ -43,8 +86,8 @@ public MapData Export() { //Log(spriteRenderer.bounds); //Log(spriteRenderer.sprite.texture.GetPixels32()); - var mapZone = rsd.MapZone.ToString(); - var sceneName = rsd.SceneName; + var mapZone = roomSpriteDefinition.MapZone.ToString(); + var sceneName = roomSpriteDefinition.SceneName; var gameObjectName = m.name; var roughMapRoom = m.GetComponent(); @@ -97,12 +140,14 @@ public MapData Export() { // spriteRough = roughSpriteInfo != null ? $"IMPORT_STRING_START:{roughSpriteInfo.name?.ToLower()}:IMPORT_STRING_END" : null, sprite = $"{spriteInfo.name}", spriteRough = roughSpriteInfo != null ? $"{roughSpriteInfo.name}" : null, + texts = textDatas }); } - var mapData = new MapData() { - rooms = rooms, - }; + var mapData = new MapData( + rooms: rooms, + areaNames: areaNames + ); var json = Json.Stringify(mapData); //var js = json // .Replace("\"IMPORT_STRING_START:", "") diff --git a/HKVizMod/MapExportTypes.cs b/HKVizMod/MapExportTypes.cs index 127340e..6e83fb1 100644 --- a/HKVizMod/MapExportTypes.cs +++ b/HKVizMod/MapExportTypes.cs @@ -38,23 +38,37 @@ public class MapRoomData { public string sprite; public string? spriteRough; public bool hasRoughVersion; + public List texts; } [System.Serializable] - public class ExportBounds { - public Vector3 min; - public Vector3 max; + public record TextData( + string objectPath, + string convoName, + string sheetName, + Vector3 position, + float fontSize, + float fontWeight, + ExportBounds bounds, + Vector4 origColor + ); + [System.Serializable] + public record ExportBounds( + Vector3 min, + Vector3 max + ) { public static ExportBounds fromBounds(UnityEngine.Bounds bounds, UnityEngine.Vector3 substract) { - return new ExportBounds { - min = bounds.min - substract, - max = bounds.max - substract, - }; + return new ExportBounds( + min: bounds.min - substract, + max: bounds.max - substract + ); } } [System.Serializable] - public class MapData { - public List rooms; - } + public record MapData( + List rooms, + List areaNames + ); } diff --git a/HKVizMod/PlayerHealthWriter.cs b/HKVizMod/PlayerHealthWriter.cs new file mode 100644 index 0000000..623e2b8 --- /dev/null +++ b/HKVizMod/PlayerHealthWriter.cs @@ -0,0 +1,114 @@ +using Core.FsmUtil; +using HutongGames.PlayMaker.Actions; +using Modding; +using Satchel; +using System; +using System.Linq; +using UnityEngine; + +namespace HKViz { + internal class PlayerHealthWriter: Loggable { + private RecordingFileManager recording = RecordingFileManager.Instance; + private RecordingSerializer serializer = RecordingSerializer.Instance; + + private static PlayerHealthWriter? instance; + public static PlayerHealthWriter Instance { + get { + if (instance == null) { + instance = new PlayerHealthWriter(); + } + return instance; + } + } + + int healthPreHeal = 0; + + + public void InitFsm() { + + // ----- FOCUSING / HEALING ---- + Hooks.HookStateExited(new FSMData( + FsmName: "Spell Control", + StateName: "Focus Start" + ), a => { + recording.WriteEntry(RecordingPrefixes.FOCUS_START); + }); + + Hooks.HookStateExitedViaTransition(new FSMData( + FsmName: "Spell Control", + StateName: "Focus" + ), (fms, transition) => { + // transition tells us if the focus was successful, canceled releasing the button, taking damage or sth else. + // if it is completed, the write happens in another hook, since here variables for health increase are not set yet. + if (transition == "FOCUS COMPLETED") { + return; // this case is handled in the next hook + } + var transitionShort = transition switch { + // "FOCUS COMPLETED" => , // success // already returned + "HERO DAMAGED" => "d", // canceled by taking damage + "BUTTON UP" => "c", // canceled by player releasing key + _ => transition, + }; + recording.WriteEntry(RecordingPrefixes.FOCUS_CANCELED, transitionShort); + }); + + Hooks.HookStateEntered(new FSMData( + FsmName: "Spell Control", + StateName: "Focus Heal" + ), (fsm) => { + healthPreHeal = PlayerData.instance.health; + }); + } + + internal void InitNewKnight(Transform knight) { + var fsm = knight.gameObject.LocateMyFSM("Spell Control"); + var foundState = fsm.TryGetState("Focus Heal", out var healState); + if (!foundState) { + LogError("HKViz unable to locate Focus Heal state in Spell Control"); + return; + } + + // Wait Action should always be the last one in an unmodded game + // this search hopefully makes it a bit more recilient to other mods. + var waitIndex = -1; + for(var i=0; i { + + // here healing success is handled, since vars are set: + var theoreticallyHealedMasks = fsm.GetIntVariable("Health Increase")?.Value ?? 0; + + var healthPostHeal = HeroController.instance.playerData.health; + //var actualHealed = healthPostHeal - healthPreHeal; + + //Log("Success heal theoreticallyHealedMasks=" + theoreticallyHealedMasks + " healthPostHeal=" + healthPostHeal + + // " actualHealed=" + actualHealed + " healthPreHeal=" + healthPreHeal + // ); + + recording.WriteEntryPrefix(RecordingPrefixes.FOCUS_SUCCESS); + recording.Write(serializer.serialize(theoreticallyHealedMasks)); + recording.WriteSep(); + recording.Write(serializer.serialize(healthPreHeal)); + recording.WriteSep(); + recording.Write(serializer.serialize(healthPostHeal)); + recording.WriteNL(); + }; + + if (waitIndex >= 0) { + //Log("Initialized heal recording before wait. At action index" + waitIndex); + healState.InsertCustomAction(successfulHealAction, waitIndex); + } else { + Log("Could not find WAIT action in healing fsm, heal recording may be less accurate"); + healState.AddCustomAction(successfulHealAction); + } + + //Log(String.Join(", ", healState.Actions.Select(it => it.GetType().Name).ToArray())); + } + } +} diff --git a/HKVizMod/RecordingFileManager.cs b/HKVizMod/RecordingFileManager.cs index 232d621..7d97bb7 100644 --- a/HKVizMod/RecordingFileManager.cs +++ b/HKVizMod/RecordingFileManager.cs @@ -6,7 +6,6 @@ namespace HKViz { internal class RecordingFileManager : Loggable { - private static string RECORDER_FILE_VERSION = "1.5.1"; private static RecordingFileManager? _instance; public static RecordingFileManager Instance { get { @@ -162,7 +161,7 @@ public void StartPart() { // and we would just continue writing on the errored line. WriteNL(); WriteEntry(RecordingPrefixes.RECORDING_ID, localRunId); - WriteEntry(RecordingPrefixes.RECORDING_FILE_VERSION, RECORDER_FILE_VERSION); + WriteEntry(RecordingPrefixes.RECORDING_FILE_VERSION, Constants.RECORDER_FILE_VERSION); //} AfterSwitchedFile?.Invoke(); } diff --git a/HKVizMod/RecordingPrefixes.cs b/HKVizMod/RecordingPrefixes.cs index 03cf9aa..2496ae3 100644 --- a/HKVizMod/RecordingPrefixes.cs +++ b/HKVizMod/RecordingPrefixes.cs @@ -21,6 +21,10 @@ internal static class RecordingPrefixes { public static readonly string SPELL_UP = "sup"; public static readonly string SPELL_DOWN = "sdn"; + public static readonly string FOCUS_START = "f"; + public static readonly string FOCUS_CANCELED = "Fc"; + public static readonly string FOCUS_SUCCESS = "F"; + public static readonly string NAIL_ART_CYCLONE = "nc"; public static readonly string NAIL_ART_D_SLASH = "nd"; public static readonly string NAIL_ART_G_SLASH = "ng"; diff --git a/HKVizMod/ServerApi.cs b/HKVizMod/ServerApi.cs index 45bba9e..b708893 100644 --- a/HKVizMod/ServerApi.cs +++ b/HKVizMod/ServerApi.cs @@ -16,7 +16,7 @@ public static ServerApi Instance { public IEnumerator ApiPost(string path, TBody body, Action onSuccess, Action onError) { var json = Json.Stringify(body); - var url = Constants.API_URL + path; + var url = Constants.API_URL + path + Constants.API_URL_SUFFIX; var request = new UnityWebRequest(url, "POST"); byte[] bodyRaw = Encoding.UTF8.GetBytes(json); @@ -27,14 +27,14 @@ public IEnumerator ApiPost(string path, TBody body, Action(string path, Action onSuccess, Action onError) { - var url = Constants.API_URL + path; + var url = Constants.API_URL + path + Constants.API_URL_SUFFIX; Log(url); var request = UnityWebRequest.Get(url); return WrapWWW(request, onSuccess, onError); } public IEnumerator ApiDelete(string path, Action onSuccess, Action onError) { - var url = Constants.API_URL + path; + var url = Constants.API_URL + path + Constants.API_URL_SUFFIX; Log(url); var request = UnityWebRequest.Delete(url); return WrapWWW(request, onSuccess, onError); diff --git a/HKVizMod/UploadManager.cs b/HKVizMod/UploadManager.cs index 4197bed..144712e 100644 --- a/HKVizMod/UploadManager.cs +++ b/HKVizMod/UploadManager.cs @@ -40,6 +40,8 @@ public class UploadQueueEntry { internal class UploadManager : Loggable { [Serializable] private class CreateUploadPartUrlRequest { + public string modVersion; + public string ingameAuthId; public string localRunId; public int partNumber; @@ -69,6 +71,7 @@ private class CreateUploadPartUrlResponse { public string fileId; public string runId; public string signedUploadUrl; + public bool alreadyFinished; } [Serializable] @@ -137,6 +140,8 @@ private void UploadFirstFileInQueue() { ServerApi.Instance.ApiPost( path: "ingameupload/part/init", body: new CreateUploadPartUrlRequest() { + modVersion = Constants.GetVersion(), + ingameAuthId = authId, localRunId = queueEntry.localRunId, partNumber = queueEntry.partNumber, @@ -162,31 +167,37 @@ private void UploadFirstFileInQueue() { lastUnixSeconds = queueEntry.lastUnixSeconds, }, onSuccess: initResponse => { - try { - ServerApi.Instance.R2Upload( - initResponse.signedUploadUrl, StoragePaths.GetRecordingPath( - partNumber: queueEntry.partNumber, - localRunId: queueEntry.localRunId, - profileId: queueEntry.profileId - ), - onSuccess: () => { - ServerApi.Instance.ApiPost( - path: "ingameupload/part/finish", - body: new MarkUploadPartFinishedRequest() { - ingameAuthId = authId, - localRunId = queueEntry.localRunId, - fileId = initResponse.fileId - }, - onSuccess: response => OnSuccess(), - onError: OnError - ).StartGlobal(); - }, - onError: OnError - ).StartGlobal(); - } catch (Exception ex) { - LogError("R2 upload failed"); - LogError(ex); - OnError(null); + if (initResponse.alreadyFinished) { + Log("Upload was already marked complete in Backend. Will not try to upload again"); + OnSuccess(); + } else { + + try { + ServerApi.Instance.R2Upload( + initResponse.signedUploadUrl, StoragePaths.GetRecordingPath( + partNumber: queueEntry.partNumber, + localRunId: queueEntry.localRunId, + profileId: queueEntry.profileId + ), + onSuccess: () => { + ServerApi.Instance.ApiPost( + path: "ingameupload/part/finish", + body: new MarkUploadPartFinishedRequest() { + ingameAuthId = authId, + localRunId = queueEntry.localRunId, + fileId = initResponse.fileId + }, + onSuccess: response => OnSuccess(), + onError: OnError + ).StartGlobal(); + }, + onError: OnError + ).StartGlobal(); + } catch (Exception ex) { + LogError("R2 upload failed"); + LogError(ex); + OnError(null); + } } }, onError: OnError