diff --git a/ARKBreedingStats/ARKBreedingStats.csproj b/ARKBreedingStats/ARKBreedingStats.csproj index 202df7ec..4f31d3ec 100644 --- a/ARKBreedingStats/ARKBreedingStats.csproj +++ b/ARKBreedingStats/ARKBreedingStats.csproj @@ -40,6 +40,7 @@ + diff --git a/ARKBreedingStats/Form1.importSave.cs b/ARKBreedingStats/Form1.importSave.cs index d3a1a9d4..2eb77234 100644 --- a/ARKBreedingStats/Form1.importSave.cs +++ b/ARKBreedingStats/Form1.importSave.cs @@ -155,8 +155,7 @@ private async Task RunSavegameImport(string fileLocation, string conveni } catch (Exception ex) { - var noAsaSupportInfo = ex.Message.StartsWith("Found unknown Version 20819") ? "Importing save games from ARK: Survival Ascended (ASA) is not yet supported, currently only ARK: Survival Evolved (ASE) is supported for save file import.\n\n" : null; - MessageBoxes.ExceptionMessageBox(ex, $"{noAsaSupportInfo}An error occurred while importing the file {fileLocation}.", "Save file import error"); + MessageBoxes.ExceptionMessageBox(ex, $"An error occurred while importing the file {fileLocation}.", "Save file import error"); return string.Empty; } finally diff --git a/ARKBreedingStats/ImportSavegame.cs b/ARKBreedingStats/ImportSavegame.cs index 9f486c0c..55b890a7 100644 --- a/ARKBreedingStats/ImportSavegame.cs +++ b/ARKBreedingStats/ImportSavegame.cs @@ -1,6 +1,7 @@ using ARKBreedingStats.Library; using ARKBreedingStats.species; using ARKBreedingStats.values; +using Python.Runtime; using SavegameToolkit; using SavegameToolkit.Arrays; using SavegameToolkit.Structs; @@ -26,6 +27,81 @@ private ImportSavegame(float gameTime) } public static async Task ImportCollectionFromSavegame(CreatureCollection creatureCollection, string filename, string serverName) + { + byte[] first16Bytes = new byte[16]; + using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read)) + { + fs.ReadExactly(first16Bytes, 0, 16); + } + string checkString = System.Text.ASCIIEncoding.ASCII.GetString(first16Bytes); + bool isAsaSavegame = checkString.Contains("SQLite format"); + var creatures = await (isAsaSavegame ? ImportCollectionFromAsaSavegame(creatureCollection, filename, serverName) : ImportCollectionFromAseSavegame(creatureCollection, filename, serverName)); + + ArkName.ClearCache(); + + // if there are creatures with unknown species, check if the according mod-file is available + var unknownSpeciesCreatures = creatures.Where(c => c.Species == null).ToArray(); + + if (!unknownSpeciesCreatures.Any() + || Properties.Settings.Default.IgnoreUnknownBlueprintsOnSaveImport + || MessageBox.Show("The species of " + unknownSpeciesCreatures.Length + " creature" + (unknownSpeciesCreatures.Length != 1 ? "s" : "") + " is not recognized, probably because they are from a mod that is not loaded.\n" + + "The unrecognized species-classes are as follows, all the according creatures cannot be imported:\n\n" + string.Join("\n", unknownSpeciesCreatures.Select(c => c.name).Distinct().ToArray()) + + "\n\nTo import the unrecognized creatures, you first need mod values-files, see Settings - Mod value manager… if the mod value is available\n\n" + + "Do you want to import the recognized creatures? If you click no, nothing is imported.", + "Unrecognized species while importing savegame", MessageBoxButtons.YesNo, MessageBoxIcon.Question + ) == DialogResult.Yes + ) + { + ImportCollection(creatureCollection, creatures.Where(c => c.Species != null).ToList(), serverName); + } + } + + private static async Task ImportCollectionFromAsaSavegame(CreatureCollection creatureCollection, string filename, string serverName) + { + var importUnclaimedBabies = Properties.Settings.Default.SaveFileImportUnclaimedBabies; + var saveImportCryo = Properties.Settings.Default.SaveImportCryo; + + // TODO: properly set up python engine & make sure arkparse is installed + if (!PythonEngine.IsInitialized) PythonEngine.Initialize(); + Creature[] creatures; + using (Py.GIL()) + { + dynamic pathlib = Py.Import("pathlib"); + dynamic asa_save = Py.Import("arkparse.saves.asa_save"); + dynamic path = pathlib.Path(filename); + dynamic save = asa_save.AsaSave(path); + float gameTime = save.save_context.game_time; + dynamic dino_api = Py.Import("arkparse.api.dino_api"); + dynamic DinoApi = dino_api.DinoApi(save); + PyDict creatureObjects = DinoApi.get_all_tamed(); + + IEnumerable tamedCreatures = creatureObjects.Values().Where(o => (importUnclaimedBabies || !IsUnclaimedBaby(o)) && (saveImportCryo || !IsInCryo(o))); + + if (!string.IsNullOrWhiteSpace(Properties.Settings.Default.ImportTribeNameFilter)) + { + string[] filters = Properties.Settings.Default.ImportTribeNameFilter.Split(',') + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToArray(); + + if (filters.Any()) + { + tamedCreatures = tamedCreatures.Where(c => + { + return filters.Any(filter => c.owner.tribe.Contains(filter)); + }); + } + } + + ImportSavegame importSavegame = new ImportSavegame(gameTime); + int? wildLevelStep = creatureCollection.getWildLevelStep(); + creatures = tamedCreatures.Select(o => (Creature)importSavegame.ConvertTamedDino(o, wildLevelStep)).Where(c => c != null).ToArray(); + } + + return creatures; + } + + private static async Task ImportCollectionFromAseSavegame(CreatureCollection creatureCollection, string filename, string serverName) { (GameObjectContainer gameObjectContainer, float gameTime) = await Task.Run(() => ReadSavegameFile(filename)); var ignoreClasses = Values.V.IgnoreSpeciesClassesOnImport; @@ -56,25 +132,7 @@ public static async Task ImportCollectionFromSavegame(CreatureCollection creatur ImportSavegame importSavegame = new ImportSavegame(gameTime); int? wildLevelStep = creatureCollection.getWildLevelStep(); - var creatures = tamedCreatureObjects.Select(o => importSavegame.ConvertGameObject(o, wildLevelStep)).Where(c => c != null).ToArray(); - - ArkName.ClearCache(); - - // if there are creatures with unknown species, check if the according mod-file is available - var unknownSpeciesCreatures = creatures.Where(c => c.Species == null).ToArray(); - - if (!unknownSpeciesCreatures.Any() - || Properties.Settings.Default.IgnoreUnknownBlueprintsOnSaveImport - || MessageBox.Show("The species of " + unknownSpeciesCreatures.Length + " creature" + (unknownSpeciesCreatures.Length != 1 ? "s" : "") + " is not recognized, probably because they are from a mod that is not loaded.\n" - + "The unrecognized species-classes are as follows, all the according creatures cannot be imported:\n\n" + string.Join("\n", unknownSpeciesCreatures.Select(c => c.name).Distinct().ToArray()) - + "\n\nTo import the unrecognized creatures, you first need mod values-files, see Settings - Mod value manager… if the mod value is available\n\n" - + "Do you want to import the recognized creatures? If you click no, nothing is imported.", - "Unrecognized species while importing savegame", MessageBoxButtons.YesNo, MessageBoxIcon.Question - ) == DialogResult.Yes - ) - { - ImportCollection(creatureCollection, creatures.Where(c => c.Species != null).ToList(), serverName); - } + return tamedCreatureObjects.Select(o => importSavegame.ConvertGameObject(o, wildLevelStep)).Where(c => c != null).ToArray(); } private static (GameObjectContainer, float) ReadSavegameFile(string fileName) @@ -254,5 +312,149 @@ private Creature ConvertGameObject(GameObject creatureObject, int? levelStep) return creature; } + + private Creature ConvertTamedDino(dynamic dino, int? levelStep) + { + if (!Values.V.TryGetSpeciesByBlueprint(dino.@object.blueprint.As(), out Species species)) + { + // species is unknown, creature cannot be imported. + // use name-field to temporarily save the unknown blueprint name to display in a messageBox + return new Creature { name = dino.@object.blueprint.As().Split('.').Last() }; + } + + string imprinterName = dino.owner.imprinter; + string owner = string.IsNullOrWhiteSpace(imprinterName) ? dino.owner.tamer_string : imprinterName; + + int[] wildLevels = ExtractStatLevels(dino.stats.base_stat_points); + wildLevels[Stats.Torpidity] = dino.stats.base_level.As() - 1; + int[] tamedLevels = ExtractStatLevels(dino.stats.added_stat_points); + int[] mutatedLevels = ExtractStatLevels(dino.stats.mutated_stat_points); + + float ti = dino.@object.get_property_value("TamedIneffectivenessModifier", 0).As(); + double te = 1f / (1 + (ti == 0 ? dino.@object.get_property_value("TameIneffectivenessModifier", 0).As() : ti)); + + var arkId = Utils.ConvertArkIdsToLongArkId(dino.id_.id1.As(), dino.id_.id2.As()); + + string tamedName = dino.tamed_name; + string tribeName = dino.owner.tribe; + bool isFemale = dino.is_female.As(); + float imprintingQuality = dino.stats.@object.get_property_value("DinoImprintingQuality", 0).As(); + int mutationsFemale = dino.@object.get_property_value("RandomMutationsFemale", 0).As(); + int mutationsMale = dino.@object.get_property_value("RandomMutationsMale", 0).As(); + bool neutered = dino.@object.get_property_value("bNeutered", false).As(); + bool mutagenApplied = dino.@object.get_property_value("MutagenApplied", false).As(); + + Creature creature = new Creature(species, + tamedName, owner, tribeName, + isFemale ? Sex.Female : Sex.Male, + wildLevels, tamedLevels, mutatedLevels, te, + !string.IsNullOrWhiteSpace(imprinterName), + imprintingQuality, + levelStep + ) + { + imprinterName = imprinterName, + guid = Utils.ConvertArkIdToGuid(arkId), + ArkId = arkId, + ArkIdImported = true, + ArkIdInGame = Utils.ConvertImportedArkIdToIngameVisualization(arkId), + domesticatedAt = DateTime.Now, // TODO: possible to convert ingame-time to realtime? + addedToLibrary = DateTime.Now, + mutationsMaternal = mutationsFemale, + mutationsPaternal = mutationsMale, + flags = (neutered ? CreatureFlags.Neutered : CreatureFlags.None) + | (mutagenApplied ? CreatureFlags.MutagenApplied : CreatureFlags.None) + }; + + float babyAge = dino.@object.get_property_value("BabyAge", 1).As(); + if (babyAge < 1) + { + double maturationDuration = species.breeding?.maturationTimeAdjusted ?? 0; + float bornSecondsAgo = (float)maturationDuration * babyAge; + if (bornSecondsAgo < maturationDuration - 120) // there seems to be a slight offset of one of these saved values, so don't display a creature as being in cooldown if it is about to leave it in the next 2 minutes + creature.growingUntil = DateTime.Now.Add(TimeSpan.FromSeconds(maturationDuration - bornSecondsAgo)); + } + else + { + double nextMatingPossible = dino.@object.get_property_value("NextAllowedMatingTime", 0).As(); + if (_gameTime < nextMatingPossible) + { + creature.cooldownUntil = DateTime.Now.Add(TimeSpan.FromSeconds(nextMatingPossible - _gameTime)); + } + } + + dynamic ancestorsFemale = dino.@object.get_property_value("DinoAncestors"); + if (ancestorsFemale != null) + { + dynamic femaleParent = ancestorsFemale[-1].female; + creature.motherGuid = Utils.ConvertArkIdToGuid(Utils.ConvertArkIdsToLongArkId( + femaleParent.id_.id1.As(), + femaleParent.id_.id2.As() + )); + creature.motherName = femaleParent.name; + creature.isBred = true; + } + dynamic ancestorsMale = dino.@object.get_property_value("DinoAncestorsMale"); + if (ancestorsMale != null) + { + dynamic maleParent = ancestorsMale[-1].male; + creature.fatherGuid = Utils.ConvertArkIdToGuid(Utils.ConvertArkIdsToLongArkId( + maleParent.id_.id1.As(), + maleParent.id_.id2.As() + )); + creature.fatherName = maleParent.name; + creature.isBred = true; + } + + creature.colors = new byte[Ark.ColorRegionCount]; + dynamic colorSetIndices = dino.get_color_set_indices(); + for (int i = 0; i < Ark.ColorRegionCount; i++) + { + creature.colors[i] = colorSetIndices[i].As(); + } + + bool isDead = dino.is_dead.As(); + if (isDead) + { + creature.Status = CreatureStatus.Dead; + } + bool isInCryo = dino.is_cryopodded.As(); + if (isInCryo) + { + creature.Status = CreatureStatus.Cryopod; + } + + creature.RecalculateCreatureValues(levelStep); + + return creature; + } + + private static bool IsUnclaimedBaby(dynamic dino) + { + TeamType teamType = TeamTypes.ForTeam(dino.@object.get_property_value("TargetingTeam", 0).As()); + return teamType == TeamType.Breeding; + } + private static bool IsInCryo(dynamic dino) + { + return dino.is_cryopodded.As(); + } + + private static int[] ExtractStatLevels(dynamic statPoints) + { + int[] stats = new int[Stats.StatsCount]; + stats[Stats.Health] = statPoints.health.As(); + stats[Stats.Stamina] = statPoints.stamina.As(); + stats[Stats.Torpidity] = statPoints.torpidity.As(); + stats[Stats.Oxygen] = statPoints.oxygen.As(); + stats[Stats.Food] = statPoints.food.As(); + stats[Stats.Water] = statPoints.water.As(); + stats[Stats.Temperature] = statPoints.temperature.As(); + stats[Stats.Weight] = statPoints.weight.As(); + stats[Stats.MeleeDamageMultiplier] = statPoints.melee_damage.As(); + stats[Stats.SpeedMultiplier] = statPoints.movement_speed.As(); + stats[Stats.TemperatureFortitude] = statPoints.fortitude.As(); + stats[Stats.CraftingSpeedMultiplier] = statPoints.crafting_speed.As(); + return stats; + } } } diff --git a/ARKBreedingStats/values/Values.cs b/ARKBreedingStats/values/Values.cs index 412a0f0e..3f4cfe8e 100644 --- a/ARKBreedingStats/values/Values.cs +++ b/ARKBreedingStats/values/Values.cs @@ -845,6 +845,12 @@ public Species SpeciesByBlueprint(string blueprintPath, bool removeTrailingC) => ? blueprintPath.Substring(0, blueprintPath.Length - 2) : blueprintPath); + public bool TryGetSpeciesByBlueprint(string blueprintPath, out Species species, bool removeTrailingC = true) + { + species = SpeciesByBlueprint(blueprintPath, removeTrailingC); + return species != null; + } + /// /// Sets the ModsManifest. If the value is null, a new default object will be created. ///