Wufflez / Loki

A character editor for Valheim
GNU General Public License v3.0
58 stars 23 forks source link

Using Loki (Master, not 0.11-beta) on Mistlands release version, not PTB, causes fch file to have an error, yet backup, PTB upgraded saves work. #30

Open rjbprime opened 1 year ago

rjbprime commented 1 year ago

Hello @Wufflez, I ran the Public Test Build when it initially came out for a bit, after following IronGate dev's backup, created a new character, played for about 10 minutes, then reverted back to the main, not PTB build, whilst reverting saves.

Today, after updating to the not-PTB build of Mistlands, I created a new character, grabbed the code for Loki from your Gihub, then compiled it. Upon loading the new character from today (not the one from two weeks ago), it give an error as shown in the attached image , yet it seems to load my two other saves, one being the PTB Mistlands character, the other being the one I've been playing up until about August this year, with no PTB or anything.

Not being a dot net 3 core coder, I am unsure on what to do.

attached image

Wufflez commented 1 year ago

Are you using the latest code, another user has updated the program recently for the mistland stuff but I haven't released a compiled version for a while.

I haven't played the game in a long while (although the new mistland update is tempting me back! haha)

If you are still having issues, you could upload your save file and I could try to debug it here locally. I need to update my game though ideally and also decompile Valheim to have a look at what they've changed. I'm a bit out of the loop to be honest with you.

rjbprime commented 1 year ago

All good. Before I git cloned this repo, i tried the other fork that had mistlands compatibility, it gave the same error. I then cleared the directory, cloned this one's master, worked out how to build, and ran the newly built exe to try the save, same error.

I'll attach the save to this as zip file, just rename it from "filename".zip to "filename".fch.

ragnar_backup_20221208-165954.zip

flying4fun commented 1 year ago

the other fork that had mistlands compatibility

Which fork was the one you tried? (actually just looking for a mistlands fork of this repo)

rjbprime commented 1 year ago

@flying4fun: Was referring to @bdomars patched version. I've tried that, it doesn't appear to work with save either.

Wufflez commented 1 year ago

@rjbprime I've had a quick look at your save and I can see why it's not loading, it's due to hitting a Carapace Spear in your inventory. It seems the mistlands patch is not yet complete. I haven't yet played the mistalands update yet as I've been away from my PC for a while, but once I finish work for the holiday period I will have some time at my computer to patch in the missing item and any others that might be missing too. I'll release a new version then.

There are a few other things I need to address like the previous update too which added cloud saves and some built in backup mechanism, I should just swap Loki to use the same backup naming scheme and show legacy / cloud saves in the selection the same way as the game does.

I appologise for the delay, it's been a crazy month for me and difficult to find time for hobbies.

Thanks

flying4fun commented 1 year ago

Gotcha, thanks. I tried @bdomars patched version and I too can verify it has issues if you have any of the new mistland items in your inventory. Otherwise, it works fine.

Brandon-T commented 1 year ago

Some new items that need to be added:

["Softtissue"] = new SharedItemData
            {
                ItemName = "Softtissue", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 40, DisplayName = "Soft tissue", MaxQuality = 1, ItemType = (ItemType)1,
            },
            ["Fish2"] = new SharedItemData
            {
                ItemName = "Fish2", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 10, DisplayName = "Pike", MaxQuality = 4, ItemType = (ItemType)1,
            },
            ["Fish1"] = new SharedItemData
            {
                ItemName = "Fish1", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 10, DisplayName = "Perch", MaxQuality = 2, ItemType = (ItemType)1,
            },
            ["FishingBaitForest"] = new SharedItemData
            {
                ItemName = "FishingBaitForest", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 100, DisplayName = "Mossy fishing bait",
                MaxQuality = 1, ItemType = (ItemType)9,
            },
            ["BowSpineSnap"] = new SharedItemData
            {
                ItemName = "BowSpineSnap", IsTeleportable = true, UsesDurability = true, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 1, DisplayName = "Spine snap",
                MaxQuality = 2, ItemType = (ItemType)4,
            },
            ["ArrowCarapace"] = new SharedItemData
            {
                 ItemName = "ArrowCarapace", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                 DurabilityPerLevel = 50, MaxStack = 100, DisplayName = "Carapace arrow",
                 MaxQuality = 1, ItemType = (ItemType)9
            },
            ["MushroomMagecap"] = new SharedItemData
            {
                ItemName = "MushroomMagecap", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 50, DisplayName = "Magecap",
                MaxQuality = 1, ItemType = (ItemType)2,
            },
            ["MushroomJotunpuffs"] = new SharedItemData
            {
                ItemName = "MushroomJotunpuffs", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 50, DisplayName = "Jotun puffs",
                MaxQuality = 1, ItemType = (ItemType)2,
            },
            ["ScaleHide"] = new SharedItemData
            {
                ItemName = "ScaleHide", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 50, DisplayName = "Scale hide",
                MaxQuality = 1, ItemType = (ItemType)1,
            }

I don't actually have the Carapace stuff so not 100% sure the max stack size, but the rest of info is correct.

New SkillType: Fishing = 104, SkillType.Fishing => Properties.Resources.Fishing,

Just breakpoint:

public static SharedItemData TryFindSharedData(string itemName) => 
            ItemData.TryGetValue(itemName, out SharedItemData sharedData) ? sharedData : null;

and see which items it returns null for. Those would be new items.

With the above changes, it works fine on Mistlands. I've only tested it on a local server but should be fine on any server actually.

rjbprime commented 1 year ago

@Brandon-T : I thought there were several new fish, the crossbow skill, the eitr mage gear, and quite a few other things.

The Detailed Patch Notes for 0.212.7 over on steam says the folowing:

Valheim Patch Notes.txt

Brandon-T commented 1 year ago

@Brandon-T : I thought there were several new fish, the crossbow skill, the eitr mage gear, and quite a few other things.

The Detailed Patch Notes for 0.212.7 over on steam says the folowing:

Valheim Patch Notes.txt

I guess so, but it doesn't show up for me yet. I think they're unlocked so I probably have to do something in game to unlock it. However when I decompiled the valheim assembly, I did not see a crossbow skill in it, or a mage skill (eitr).

rjbprime commented 1 year ago

The crossbow skill is separate from the bow skill, and is used for the new arbalest crossbow, and the eitr skills are Blood magic & Elemental magic. Blood Magic is increased with the use of the staff of protection or the Dead Riser, whereas the Elemental magic is raised by the Staves of frost or ember.

rjbprime commented 1 year ago

All unlocked with mistlands resources and crafting tables.

Brandon-T commented 1 year ago

You're right. The new skills are (ElementalMagic [Previously known as FireMagic], BloodMagic [Previously known as Frost Magic], Crossbows, Fishing):

public enum SkillType
    {
        None = 0,
        Swords = 1,
        Knives = 2,
        Clubs = 3,
        Polearms = 4,
        Spears = 5,
        Blocking = 6,
        Axes = 7,
        Bows = 8,
        ElementalMagic = 9,
        BloodMagic = 10,
        Unarmed = 11,
        Pickaxes = 12,
        WoodCutting = 13,
        Crossbows = 14,
        Jump = 100,
        Sneak = 101,
        Run = 102,
        Swim = 103,
        Fishing = 104,
        Ride = 110,
        All = 999
    }
bdomars commented 1 year ago

Oh, my PR was merged 😄 I've been away for a while so didn't notice before.

But yes as you seem to have figured out the updates I did were not actually enough to work with Mistlands, merely they changed the reading and writing of bytes to allow for editing but all new Mistlands content is still missing from the editor and makes it crash even though the savefile bytes are read in the correct order.

Did you @Brandon-T already make changes you'd like to contribute?

I've been looking for a way to exctract the missing data from the game itself but haven't figured that out yet, although I think @Wufflez already has a way of doing that?

Brandon-T commented 1 year ago

Oh, my PR was merged 😄 I've been away for a while so didn't notice before.

But yes as you seem to have figured out the updates I did were not actually enough to work with Mistlands, merely they changed the reading and writing of bytes to allow for editing but all new Mistlands content is still missing from the editor and makes it crash even though the savefile bytes are read in the correct order.

Did you @Brandon-T already make changes you'd like to contribute?

I've been looking for a way to exctract the missing data from the game itself but haven't figured that out yet, although I think @Wufflez already has a way of doing that?

I dumped all items from the game into a list: Valheim.txt

I don't actually know if I broke any item names or anything as I did not test it. I don't feel like updating the project to .NET 6 from .NET 3, and compiling it. But I was interested in what items the game has and how the game works, so I dumped it. Hopefully the list helps.

Wrote code to dump it:

#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>

enum class ItemType {
    None = 0,
    Material = 1,
    Consumable = 2,
    OneHandedWeapon = 3,
    Bow = 4,
    Shield = 5,
    Helmet = 6,
    Chest = 7,
    Ammo = 9,
    Customization = 10,
    Legs = 11,
    Hands = 12,
    Trophy = 13,
    TwoHandedWeapon = 14,
    Torch = 15,
    Misc = 16,
    Shoulder = 17,
    Utility = 18,
    Tool = 19,
    AttachAtgeir = 20,
    Fish = 21,
    TwoHandedWeaponLeft = 22,
    AmmoNonEquipable = 23,
};

struct ItemData {
    std::string item_name;
    bool is_teleportable;
    bool uses_durability;
    double max_durability;
    double durability_per_level;
    int max_stack;
    std::string display_name;
    int max_quality;
    ItemType item_type;
};

std::string to_string(const ItemType& type)
{
    switch (type)
    {
    case ItemType::None: return "None";
    case ItemType::Material: return "Material";
    case ItemType::Consumable: return "Consumable";
    case ItemType::OneHandedWeapon: return "OneHandedWeapon";
    case ItemType::Bow: return "Bow";
    case ItemType::Shield: return "Shield";
    case ItemType::Helmet: return "Helmet";
    case ItemType::Chest: return "Chest";
    case ItemType::Ammo: return "Ammo";
    case ItemType::Customization: return "Customization";
    case ItemType::Legs: return "Legs";
    case ItemType::Hands: return "Hands";
    case ItemType::Trophy: return "Trophy";
    case ItemType::TwoHandedWeapon: return "TwoHandedWeapon";
    case ItemType::Torch: return "Torch";
    case ItemType::Misc: return "Misc";
    case ItemType::Shoulder: return "Shoulder";
    case ItemType::Utility: return "Utility";
    case ItemType::Tool: return "Tool";
    case ItemType::AttachAtgeir: return "AttachAtgeir";
    case ItemType::Fish: return "Fish";
    case ItemType::TwoHandedWeaponLeft: return "TwoHandedWeaponLeft";
    case ItemType::AmmoNonEquipable: return "AmmoNonEquipable";
    }
}

std::string trim(const std::string& str)
{
    std::string s = str;
    std::string white_space;
    for (int i = 0; i < 128; ++i) {
        if (i > 32 && i <= 126) {
            continue;
        }

        white_space += static_cast<char>(i);
    }

    s.erase(s.find_last_not_of(white_space) + 1);
    s.erase(0, s.find_first_not_of(white_space));
    return s;
}

bool parse(const std::string& line, const std::string& id, std::string& out)
{
    try {
        std::size_t offset = line.find(id);
        if (offset != std::string::npos)
        {
            out = trim(line.substr(offset + id.length()));
            return true;
        }
    }
    catch (const std::exception& e) {
        std::cerr << "INVALID PARSE: " + id + " with: " + line << "\n";
        throw e;
    }
    return false;
}

int main()
{
    std::vector<ItemData> items;

    std::fstream localizations{ std::filesystem::path("./Assets/Resources/localization.txt"), std::ios::in };
    if (!localizations)
    {
        return -1;
    }

    localizations.seekg(0, std::ios::end);
    std::string locale;
    locale.resize(localizations.tellg());
    localizations.seekg(0, std::ios::beg);
    localizations.read(&locale[0], locale.size());

    const std::filesystem::path path = "./Assets/PrefabInstance";
    for (auto const& dir_entry : std::filesystem::directory_iterator(path)) {
        if (dir_entry.path().extension() == ".prefab") {
            std::fstream file{dir_entry.path(), std::ios::in};
            if (file) {
                file.seekg(0, std::ios::end);
                std::size_t size = file.tellg();
                file.seekg(0, std::ios::beg);

                bool found_item = false;
                std::string str;
                while (std::getline(file, str)) {
                    if (str.find("m_itemData") != std::string::npos) {
                        found_item = true;
                        break;
                    }
                }

                if (found_item) {
                    file.seekg(0, std::ios::beg);
                    str.resize(size);
                    file.read(&str[0], size);

                    std::stringstream ss(str);
                    std::size_t offset = str.find("m_itemData");
                    ss.seekg(offset, std::ios::beg);

                    std::string line;
                    ItemData item_data = {};

                    while (std::getline(ss, line)) {
                        if (line.find("---") == 0) {
                            break;
                        }

                        std::string value;
                        if (parse(line, "m_name:", value))
                        {
                            item_data.display_name = value;
                        }
                        else if (parse(line, "m_teleportable:", value))
                        {
                            item_data.is_teleportable = std::atoi(value.c_str());
                        }
                        else if (parse(line, "m_useDurability:", value))
                        {
                            item_data.uses_durability = std::atoi(value.c_str());
                        }
                        else if (parse(line, "m_maxDurability:", value))
                        {
                            item_data.max_durability = std::atof(value.c_str());
                        }
                        else if (parse(line, "m_durabilityPerLevel:", value))
                        {
                            item_data.durability_per_level = std::atof(value.c_str());
                        }
                        else if (parse(line, "m_maxStackSize:", value))
                        {
                            item_data.max_stack = std::atoi(value.c_str());
                        }
                        else if (parse(line, "m_maxQuality:", value))
                        {
                            item_data.max_quality = std::atoi(value.c_str());
                        }
                        else if (parse(line, "m_itemType:", value))
                        {
                            int type = std::atoi(value.c_str());
                            if (type >= 0 || type <= static_cast<int>(ItemType::AmmoNonEquipable))
                            {
                                item_data.item_type = static_cast<ItemType>(std::atoi(value.c_str()));
                            }
                            else
                            {
                                throw std::runtime_error("Invalid Item Type: " + std::to_string(type));
                            }
                        }
                    }

                    ss = std::stringstream(str);
                    ss.seekg(0, std::ios::beg);
                    while (std::getline(ss, line))
                    {
                        std::string value;
                        if (parse(line, "m_Name:", value))
                        {
                            item_data.item_name = value;
                            break;
                        }
                    }

                    if (!item_data.display_name.empty()) {
                        std::string item_locale_key = "\"" + item_data.display_name.substr(1) + "\",\"";
                        offset = locale.find(item_locale_key);
                        if (offset != std::string::npos)
                        {
                            offset += item_locale_key.size();

                            std::size_t offset2 = locale.find("\",", offset);
                            if (offset2 != std::string::npos)
                            {
                                item_data.display_name = locale.substr(offset, offset2 - offset);
                            }
                        }
                    }
                    else {
                        item_data.display_name = "$" + item_data.item_name;
                    }

                    items.push_back(item_data);
                }
            }
        }
    }

    std::fstream output{ "~/Desktop/Valheim.txt", std::ios::out };
    for (const auto& item_data : items) {
        /*std::cout << "Item: " << item_data.item_name << "\n";
        std::cout << "  is_teleportable: " << (item_data.is_teleportable ? "true" : "false") << "\n";
        std::cout << "  uses_durability: " << (item_data.uses_durability ? "true" : "false") << "\n";
        std::cout << "  max_durability: " << item_data.max_durability << "\n";
        std::cout << "  durability_per_level: " << item_data.durability_per_level << "\n";
        std::cout << "  max_stack: " << item_data.max_stack << "\n";
        std::cout << "  display_name: " << item_data.display_name << "\n";
        std::cout << "  max_quality: " << item_data.max_quality << "\n";
        std::cout << "  item_type: " << to_string(item_data.item_type) << "\n";*/

        std::stringstream ss;
        ss << "[\"" << item_data.item_name << "\"]";
        ss << " = new SharedItemData\n";
        ss << "{\n";
        ss << "\tItemName = " << "\"" << item_data.item_name << "\", IsTeleportable = " << (item_data.is_teleportable ? "true" : "false") << ", UsesDurability = " << (item_data.uses_durability ? "true" : "false") << ", MaxDurability = " << item_data.max_durability << ",\n";
        ss << "\tDurabilityPerLevel = " << item_data.durability_per_level << ", MaxStack = " << item_data.max_stack << ", DisplayName = \"" << item_data.display_name << "\",\n";
        ss << "\tMaxQuality = " << item_data.max_quality << ", ItemType = ItemType." << to_string(item_data.item_type) << ",\n";
        ss << "},\n";

        output << ss.str();
    }
    return 0;
}

The game's prefab format is (m_Name is the Item-ID, and m_name is the localized item name [display name], m_itemData is the item info):

GameObject:
  serializedVersion: 6
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_Component:
  - component: {fileID: 4549847827358516}
  m_Layer: 12
  m_Name: Amber
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!4 &4549847827358516
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 1984232819277963}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: 4c735e3b6058d7147ada1f43d98ece79, type: 3}
  m_Name:
  m_EditorClassIdentifier:
  m_autoPickup: 1
  m_autoDestroy: 1
  m_itemData:
    m_stack: 1
    m_durability: 100
    m_quality: 1
    m_variant: 0
    m_shared:
      m_name: $item_amber
      m_dlc:
      m_itemType: 1
      m_icons:
      - {fileID: 21300000, guid: 9c931644b863c1e4aadeec2f9f2924b5, type: 2}
      m_attachOverride: 0
      m_description: $item_amber_description
      m_maxStackSize: 20
      m_autoStack: 1
      m_maxQuality: 1
      m_scaleByQuality: 0
      m_weight: 0.1
      m_scaleWeightByQuality: 0
      m_value: 5
      m_teleportable: 1
      m_questItem: 0
      m_equipDuration: 1
      m_variants: 0
      m_trophyPos: {x: 0, y: 0}
      m_buildPieces: {fileID: 0}
      m_centerCamera: 0
      m_setName:
      m_setSize: 0
      m_setStatusEffect: {fileID: 0}
      m_equipStatusEffect: {fileID: 0}
      m_movementModifier: 0
      m_eitrRegenModifier: 0
      m_food: 0
      m_foodStamina: 0
      m_foodEitr: 0
      m_foodBurnTime: 0
      m_foodRegen: 0
      m_armorMaterial: {fileID: 0}
      m_helmetHideHair: 1
      m_helmetHideBeard: 0
      m_armor: 10
      m_armorPerLevel: 1
      m_damageModifiers: []
      m_blockPower: 10
      m_blockPowerPerLevel: 0
      m_deflectionForce: 0
      m_deflectionForcePerLevel: 0
      m_timedBlockBonus: 1.5
      m_animationState: 1
      m_skillType: 1
      m_toolTier: 0
      m_damages:
        m_damage: 0
        m_blunt: 0
        m_slash: 0
        m_pierce: 0
        m_chop: 0
        m_pickaxe: 0
        m_fire: 0
        m_frost: 0
        m_lightning: 0
        m_poison: 0
        m_spirit: 0
      m_damagesPerLevel:
        m_damage: 0
        m_blunt: 0
        m_slash: 0
        m_pierce: 0
        m_chop: 0
        m_pickaxe: 0
        m_fire: 0
        m_frost: 0
        m_lightning: 0
        m_poison: 0
        m_spirit: 0
      m_attackForce: 50
      m_backstabBonus: 4
      m_dodgeable: 0
      m_blockable: 0
      m_tamedOnly: 0
      m_alwaysRotate: 0
      m_attackStatusEffect: {fileID: 0}
      m_spawnOnHit: {fileID: 0}
      m_spawnOnHitTerrain: {fileID: 0}
      m_projectileToolTip: 1
      m_ammoType:
      m_attack:
        m_attackType: 0
        m_attackAnimation:
        m_attackRandomAnimations: 0
        m_attackChainLevels: 0
        m_loopingAttack: 0
        m_consumeItem: 0
        m_hitTerrain: 1
        m_attackStamina: 20
        m_attackEitr: 0
        m_attackHealth: 0
        m_attackHealthPercentage: 0
        m_speedFactor: 0.2
        m_speedFactorRotation: 0.2
        m_attackStartNoise: 10
        m_attackHitNoise: 30
        m_damageMultiplier: 1
        m_forceMultiplier: 1
        m_staggerMultiplier: 1
        m_recoilPushback: 0
        m_selfDamage: 0
        m_attackOriginJoint:
        m_attackRange: 2.4
        m_attackHeight: 0.8
        m_attackOffset: 0
        m_spawnOnTrigger: {fileID: 0}
        m_toggleFlying: 0
        m_attach: 0
        m_requiresReload: 0
        m_reloadAnimation:
        m_reloadTime: 2
        m_reloadStaminaDrain: 0
        m_bowDraw: 0
        m_drawDurationMin: 0
        m_drawStaminaDrain: 0
        m_drawAnimationState:
        m_attackAngle: 90
        m_attackRayWidth: 0
        m_maxYAngle: 0
        m_lowerDamagePerHit: 1
        m_hitPointtype: 0
        m_hitThroughWalls: 0
        m_multiHit: 1
        m_pickaxeSpecial: 0
        m_lastChainDamageMultiplier: 2
        m_resetChainIfHit: 0
        m_raiseSkillAmount: 1
        m_skillHitType: 4
        m_specialHitSkill: 0
        m_specialHitType: 0
        m_attackProjectile: {fileID: 0}
        m_projectileVel: 10
        m_projectileVelMin: 2
        m_projectileAccuracy: 10
        m_projectileAccuracyMin: 20
        m_skillAccuracy: 0
        m_useCharacterFacing: 0
        m_useCharacterFacingYAim: 0
        m_launchAngle: 0
        m_projectiles: 1
        m_projectileBursts: 1
        m_burstInterval: 0
        m_destroyPreviousProjectile: 0
        m_perBurstResourceUsage: 0
        m_hitEffect:
          m_effectPrefabs: []
        m_hitTerrainEffect:
          m_effectPrefabs: []
        m_startEffect:
          m_effectPrefabs: []
        m_triggerEffect:
          m_effectPrefabs: []
        m_trailStartEffect:
          m_effectPrefabs: []
        m_burstEffect:
          m_effectPrefabs: []
      m_secondaryAttack:
        m_attackType: 0
        m_attackAnimation:
        m_attackRandomAnimations: 0
        m_attackChainLevels: 0
        m_loopingAttack: 0
        m_consumeItem: 0
        m_hitTerrain: 1
        m_attackStamina: 20
        m_attackEitr: 0
        m_attackHealth: 0
        m_attackHealthPercentage: 0
        m_speedFactor: 0.2
        m_speedFactorRotation: 0.2
        m_attackStartNoise: 10
        m_attackHitNoise: 30
        m_damageMultiplier: 1
        m_forceMultiplier: 1
        m_staggerMultiplier: 1
        m_recoilPushback: 0
        m_selfDamage: 0
        m_attackOriginJoint:
        m_attackRange: 1.5
        m_attackHeight: 0.6
        m_attackOffset: 0
        m_spawnOnTrigger: {fileID: 0}
        m_toggleFlying: 0
        m_attach: 0
        m_requiresReload: 0
        m_reloadAnimation:
        m_reloadTime: 2
        m_reloadStaminaDrain: 0
        m_bowDraw: 0
        m_drawDurationMin: 0
        m_drawStaminaDrain: 0
        m_drawAnimationState:
        m_attackAngle: 90
        m_attackRayWidth: 0
        m_maxYAngle: 0
        m_lowerDamagePerHit: 1
        m_hitPointtype: 0
        m_hitThroughWalls: 0
        m_multiHit: 1
        m_pickaxeSpecial: 0
        m_lastChainDamageMultiplier: 2
        m_resetChainIfHit: 0
        m_raiseSkillAmount: 1
        m_skillHitType: 4
        m_specialHitSkill: 0
        m_specialHitType: 0
        m_attackProjectile: {fileID: 0}
        m_projectileVel: 10
        m_projectileVelMin: 2
        m_projectileAccuracy: 10
        m_projectileAccuracyMin: 20
        m_skillAccuracy: 0
        m_useCharacterFacing: 0
        m_useCharacterFacingYAim: 0
        m_launchAngle: 0
        m_projectiles: 1
        m_projectileBursts: 1
        m_burstInterval: 0
        m_destroyPreviousProjectile: 0
        m_perBurstResourceUsage: 0
        m_hitEffect:
          m_effectPrefabs: []
        m_hitTerrainEffect:
          m_effectPrefabs: []
        m_startEffect:
          m_effectPrefabs: []
        m_triggerEffect:
          m_effectPrefabs: []
        m_trailStartEffect:
          m_effectPrefabs: []
        m_burstEffect:
          m_effectPrefabs: []
      m_useDurability: 0
      m_destroyBroken: 1
      m_canBeReparied: 1
      m_maxDurability: 100
      m_durabilityPerLevel: 50
      m_useDurabilityDrain: 1
      m_durabilityDrain: 0
      m_aiAttackRange: 2
      m_aiAttackRangeMin: 0
      m_aiAttackInterval: 2
      m_aiAttackMaxAngle: 5
      m_aiWhenFlying: 1
      m_aiWhenFlyingAltitudeMin: 0
      m_aiWhenFlyingAltitudeMax: 999999
      m_aiWhenWalking: 1
      m_aiWhenSwiming: 1
      m_aiPrioritized: 0
      m_aiInDungeonOnly: 0
      m_aiInMistOnly: 0
      m_aiMaxHealthPercentage: 1
      m_aiTargetType: 0
      m_hitEffect:
        m_effectPrefabs: []
      m_hitTerrainEffect:
        m_effectPrefabs: []
      m_blockEffect:
        m_effectPrefabs: []
      m_startEffect:
        m_effectPrefabs: []
      m_holdStartEffect:
        m_effectPrefabs: []
      m_triggerEffect:
        m_effectPrefabs: []
      m_trailStartEffect:
        m_effectPrefabs: []
      m_consumeStatusEffect: {fileID: 0}

Dumped the assets as well :) Was fun.

rjbprime commented 1 year ago

On a related note, has the new hair and beard styles been enabled for character choice? I wasn't sure if those would also conflict with the save editor, I'd thought I'd mention them.

jensbrak commented 1 year ago

I dumped all items from the game into a list: Valheim.txt

Nice! I'll use that to update my PR that decouple these items from code (making it easy to update db of Loki in the future).

A thought: One could consider using your code though, and make Loki read the items from the actual game automatically. That would make it completely obsolete to have a local copy, in code or decoupled, of the item db. On the other hand, doing so will make Loki much more reliable on the prefab structure and harder to maintain. A separate db might be a better option.

On a related note, has the new hair and beard styles been enabled for character choice? I wasn't sure if those would also conflict with the save editor, I'd thought I'd mention them.

There are alot of things introduced in Mistlands update, that needs to go into Loki. Think we've got all items covered here, skills as well, but yes, hair and beard styles will have to be updated too (I haven't seen if they already have been?).

jensbrak commented 1 year ago

@Brandon-T - what toolchain do you use to compile it with? My C++ is 25 years old :/ (from gamedev work actually!)

jensbrak commented 1 year ago

For fun, I converted the list of items you extracted @Brandon-T in my version of Loki. However, there are clearly things in the list that is not player items. They can be added to inventory in Loki but the game does not work with them, inventory slots gets locked and equipping them results in message that DLC is required. In other words, in order to make use of an extracted list we need to filter out only player items from it.

Brandon-T commented 1 year ago

For fun, I converted the list of items you extracted @Brandon-T in my version of Loki. However, there are clearly things in the list that is not player items. They can be added to inventory in Loki but the game does not work with them, inventory slots gets locked and equipping them results in message that DLC is required. In other words, in order to make use of an extracted list we need to filter out only player items from it.

Yes. That is correct. If you do have DLCs (in the future I guess), then you can equip such items that require those DLCs. I can filter out those with the isDLC flag from the prefabs. For "other" items I'm not sure but should be able to filter by type I think. But yeah, the above list contains EVERYTHING, and I didn't test it. Only a raw dump.

I am using c++17 above because it uses <filesystem> header. Any tool chain should work (VS, GCC, Clang).

I used VS on windows for the above and Clang on MacOS. Both compile it :)

Yes there are NPC items in the list. I believe the original code also contains these. For example "wolf bite" is currently in the main branch. I can try to filter them I guess. Will have to see how the game does it. But on a local server if you run devcommands, it does have items that can't be spawned when you run "spawn" command.

I think the above list also contains the beards but I'd have to check tbh.

jensbrak commented 1 year ago

I use VS but failed, need to brush off my skills there I think.

As for NPC items: already now (current version of Loki) there are items in the SharedItemData DB that Loki use, that makes no sense in being able to add to inventory. There has to be some mechanism or data in the game that distinguish NPC items from others. The prefab list is a mix of various items; some used for character look, some for inventory items and some for NPC characters.

As for beards, yes, they are in that list but to actually customize a character to have a beard, the list is not enough. Some of the items in the list are added as beards (trophys actually) for Loki, but the actual beards are hard coded and not read from the SharedItemData DB. So in order to use Loki to change appearance (First tab, ie "General") some more changes has to be added to Loki. I'll see if I can look into that too.

Update: I fixed a version of Loki that support the new beard and hair styles from Mistlands update. Currently in my own branch for it, I'll wait with the PR until we've sorted out the NPC question.

Also worth mentioning: https://github.com/Wufflez/Loki/pull/35 would prevent the crash described in this issue. Well, upcoming PR with Mistlands items I work on would also solve the crash (since it is caused by missing items). However, I believe the bugfix is still a good idea, since Valheim eventually will be updated with even more items that Loki will not support until updated. With the fix, at least it won't crash until being updated.

Brandon-T commented 1 year ago

Item List filtered by localization and m_Name. If it can't be localized and doesn't have a name, it's not included (all items in the game are localized for a ton of languages, so if it's not localized, it's an attack power of some NPC). This means, things like Abomination_attack1 would not be in the list!

However, items like Cape of Odin is there, but it has the m_dlc: beta flag so you MUST have played the beta to own such an item in the first place. Items with ItemType::Customization is not in the list as they aren't items or trophies, but they are customizations for the main-screen during character creation. We could possibly filter out more based on ItemType as well. Will need to check further though, and possibly actually compile Loki to see the issues everyone is having.

Updated Item List:

Valheim.txt

Diff (Changes from the last list I posted): https://www.diffchecker.com/KG4EbSAE

Note: I don't actually use Loki, I just like the reverse engineering that went into it :). So use the list, but beware that it MIGHT not be perfect. If you see any more issues with it, let me know. This list can probably help out the valheim wiki as well which is missing a ton of info on the new items.

I can try to update and compile Loki with .NET 6, and see how it works later on.

jensbrak commented 1 year ago

Note: I don't actually use Loki, I just like the reverse engineering that went into it

TBH, that's precisely my take on it too and how I found Loki in the first place. I was digging into reverse engineer parts of the game, just for the fun of it. Now I can't resist making some PR's to Loki just because... well... I can.

I'll update my list with filtered list. So basically, if prefab items are localized - they're player items, is that what you're saying?

Brandon-T commented 1 year ago

Note: I don't actually use Loki, I just like the reverse engineering that went into it

TBH, that's precisely my take on it too and how I found Loki in the first place. I was digging into reverse engineer parts of the game, just for the fun of it. Now I can't resist making some PR's to Loki just because... well... I can.

I'll update my list with filtered list. So basically, if prefab items are localized - they're player items, is that what you're saying?

Yeah that's correct. Below in the updated dumper code, I added continue statements so that it doesn't add it to the list of valid items.

#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>

enum class ItemType {
    None = 0,
    Material = 1,
    Consumable = 2,
    OneHandedWeapon = 3,
    Bow = 4,
    Shield = 5,
    Helmet = 6,
    Chest = 7,
    Ammo = 9,
    Customization = 10,
    Legs = 11,
    Hands = 12,
    Trophy = 13,
    TwoHandedWeapon = 14,
    Torch = 15,
    Misc = 16,
    Shoulder = 17,
    Utility = 18,
    Tool = 19,
    AttachAtgeir = 20,
    Fish = 21,
    TwoHandedWeaponLeft = 22,
    AmmoNonEquipable = 23,
};

struct ItemData {
    std::string item_name;
    bool is_teleportable;
    bool uses_durability;
    double max_durability;
    double durability_per_level;
    int max_stack;
    std::string display_name;
    int max_quality;
    ItemType item_type;
};

std::string to_string(const ItemType& type)
{
    switch (type)
    {
    case ItemType::None: return "None";
    case ItemType::Material: return "Material";
    case ItemType::Consumable: return "Consumable";
    case ItemType::OneHandedWeapon: return "OneHandedWeapon";
    case ItemType::Bow: return "Bow";
    case ItemType::Shield: return "Shield";
    case ItemType::Helmet: return "Helmet";
    case ItemType::Chest: return "Chest";
    case ItemType::Ammo: return "Ammo";
    case ItemType::Customization: return "Customization";
    case ItemType::Legs: return "Legs";
    case ItemType::Hands: return "Hands";
    case ItemType::Trophy: return "Trophy";
    case ItemType::TwoHandedWeapon: return "TwoHandedWeapon";
    case ItemType::Torch: return "Torch";
    case ItemType::Misc: return "Misc";
    case ItemType::Shoulder: return "Shoulder";
    case ItemType::Utility: return "Utility";
    case ItemType::Tool: return "Tool";
    case ItemType::AttachAtgeir: return "AttachAtgeir";
    case ItemType::Fish: return "Fish";
    case ItemType::TwoHandedWeaponLeft: return "TwoHandedWeaponLeft";
    case ItemType::AmmoNonEquipable: return "AmmoNonEquipable";
    }
}

std::string trim(const std::string& str)
{
    std::string s = str;
    std::string white_space;
    for (int i = 0; i < 128; ++i) {
        if (i > 32 && i <= 126) {
            continue;
        }

        white_space += static_cast<char>(i);
    }

    s.erase(s.find_last_not_of(white_space) + 1);
    s.erase(0, s.find_first_not_of(white_space));
    return s;
}

bool parse(const std::string& line, const std::string& id, std::string& out)
{
    try {
        std::size_t offset = line.find(id);
        if (offset != std::string::npos)
        {
            out = trim(line.substr(offset + id.length()));
            return true;
        }
    }
    catch (const std::exception& e) {
        std::cerr << "INVALID PARSE: " + id + " with: " + line << "\n";
        throw e;
    }
    return false;
}

int main()
{
    std::vector<ItemData> items;

    std::fstream localizations{ std::filesystem::path("./Assets/Resources/localization.txt"), std::ios::in };
    if (!localizations)
    {
        return -1;
    }

    localizations.seekg(0, std::ios::end);
    std::string locale;
    locale.resize(localizations.tellg());
    localizations.seekg(0, std::ios::beg);
    localizations.read(&locale[0], locale.size());

    const std::filesystem::path path = "./Assets/PrefabInstance";
    for (auto const& dir_entry : std::filesystem::directory_iterator(path)) {
        if (dir_entry.path().extension() == ".prefab") {
            std::fstream file{dir_entry.path(), std::ios::in};
            if (file) {
                file.seekg(0, std::ios::end);
                std::size_t size = file.tellg();
                file.seekg(0, std::ios::beg);

                bool found_item = false;
                std::string str;
                while (std::getline(file, str)) {
                    if (str.find("m_itemData") != std::string::npos) {
                        found_item = true;
                        break;
                    }
                }

                if (found_item) {
                    file.seekg(0, std::ios::beg);
                    str.resize(size);
                    file.read(&str[0], size);

                    std::stringstream ss(str);
                    std::size_t offset = str.find("m_itemData");
                    ss.seekg(offset, std::ios::beg);

                    std::string line;
                    ItemData item_data = {};

                    while (std::getline(ss, line)) {
                        if (line.find("---") == 0) {
                            break;
                        }

                        std::string value;
                        if (parse(line, "m_name:", value))
                        {
                            item_data.display_name = value;
                        }
                        else if (parse(line, "m_teleportable:", value))
                        {
                            item_data.is_teleportable = std::atoi(value.c_str());
                        }
                        else if (parse(line, "m_useDurability:", value))
                        {
                            item_data.uses_durability = std::atoi(value.c_str());
                        }
                        else if (parse(line, "m_maxDurability:", value))
                        {
                            item_data.max_durability = std::atof(value.c_str());
                        }
                        else if (parse(line, "m_durabilityPerLevel:", value))
                        {
                            item_data.durability_per_level = std::atof(value.c_str());
                        }
                        else if (parse(line, "m_maxStackSize:", value))
                        {
                            item_data.max_stack = std::atoi(value.c_str());
                        }
                        else if (parse(line, "m_maxQuality:", value))
                        {
                            item_data.max_quality = std::atoi(value.c_str());
                        }
                        else if (parse(line, "m_itemType:", value))
                        {
                            int type = std::atoi(value.c_str());
                            if (type >= 0 || type <= static_cast<int>(ItemType::AmmoNonEquipable))
                            {
                                item_data.item_type = static_cast<ItemType>(std::atoi(value.c_str()));
                            }
                            else
                            {
                                throw std::runtime_error("Invalid Item Type: " + std::to_string(type));
                            }
                        }
                    }

                    if (item_data.item_type == ItemType::Customization) {
                        continue;  // Customizations like Beard, Hair, etc aren't items.
                    }

                    ss = std::stringstream(str);
                    ss.seekg(0, std::ios::beg);
                    while (std::getline(ss, line))
                    {
                        std::string value;
                        if (parse(line, "m_Name:", value))
                        {
                            item_data.item_name = value;
                            break;
                        }
                    }

                    if (!item_data.display_name.empty()) {
                        std::string item_locale_key = "\"" + item_data.display_name.substr(1) + "\",\"";
                        offset = locale.find(item_locale_key);
                        if (offset != std::string::npos)
                        {
                            offset += item_locale_key.size();

                            std::size_t offset2 = locale.find("\",", offset);
                            if (offset2 != std::string::npos)
                            {
                                item_data.display_name = locale.substr(offset, offset2 - offset);
                            }
                        }
                        else
                        {
                            continue; // Cannot be localized, so not an item
                        }
                    }
                    else {
                        item_data.display_name = "$" + item_data.item_name;
                        continue; // No display name, so not an item
                    }

                    items.push_back(item_data);
                }
            }
        }
    }

    std::fstream output{ "~/Desktop/Valheim.txt", std::ios::out };
    for (const auto& item_data : items) {
        /*std::cout << "Item: " << item_data.item_name << "\n";
        std::cout << "  is_teleportable: " << (item_data.is_teleportable ? "true" : "false") << "\n";
        std::cout << "  uses_durability: " << (item_data.uses_durability ? "true" : "false") << "\n";
        std::cout << "  max_durability: " << item_data.max_durability << "\n";
        std::cout << "  durability_per_level: " << item_data.durability_per_level << "\n";
        std::cout << "  max_stack: " << item_data.max_stack << "\n";
        std::cout << "  display_name: " << item_data.display_name << "\n";
        std::cout << "  max_quality: " << item_data.max_quality << "\n";
        std::cout << "  item_type: " << to_string(item_data.item_type) << "\n";*/

        std::stringstream ss;
        ss << "[\"" << item_data.item_name << "\"]";
        ss << " = new SharedItemData\n";
        ss << "{\n";
        ss << "\tItemName = " << "\"" << item_data.item_name << "\", IsTeleportable = " << (item_data.is_teleportable ? "true" : "false") << ", UsesDurability = " << (item_data.uses_durability ? "true" : "false") << ", MaxDurability = " << item_data.max_durability << ",\n";
        ss << "\tDurabilityPerLevel = " << item_data.durability_per_level << ", MaxStack = " << item_data.max_stack << ", DisplayName = \"" << item_data.display_name << "\",\n";
        ss << "\tMaxQuality = " << item_data.max_quality << ", ItemType = ItemType." << to_string(item_data.item_type) << ",\n";
        ss << "},\n";

        output << ss.str();
    }
    return 0;
}

If we copy the items that were removed from the list, we can get the beards and hairs:

private static readonly Beard[] SensibleBeards = 
{
    new Beard(Loki.Properties.Resources.B_No_beard, "BeardNone"),
    new Beard(Loki.Properties.Resources.B_Braided_2, "Beard5"),
    new Beard(Loki.Properties.Resources.B_Braided_2, "Beard6"),
    new Beard(Loki.Properties.Resources.B_Braided_3, "Beard9"),
    new Beard(Loki.Properties.Resources.B_Braided_4, "Beard10"),
    new Beard(Loki.Properties.Resources.B_Thick_2, "Beard11"),
    new Beard(Loki.Properties.Resources.B_Royal_1, "Beard12"),
    new Beard(Loki.Properties.Resources.B_Royal_2, "Beard13"),
    new Beard(Loki.Properties.Resources.B_Braided_5, "Beard14"),
    new Beard(Loki.Properties.Resources.B_Short_4, "Beard15"),
    new Beard(Loki.Properties.Resources.B_Stonedweller, "Beard16"),
    new Beard(Loki.Properties.Resources.B_Long_1, "Beard1"),
    new Beard(Loki.Properties.Resources.B_Long_2, "Beard2"),
    new Beard(Loki.Properties.Resources.B_Short_1, "Beard3"),
    new Beard(Loki.Properties.Resources.B_Short_2, "Beard4"),
    new Beard(Loki.Properties.Resources.B_Short_3, "Beard7"),
    new Beard(Loki.Properties.Resources.B_Thick_1, "Beard8"),
};

private static readonly Hair[] SensibleHairs =
{
    new Hair(Loki.Properties.Resources.No_hair, "HairNone"),
    new Hair(Loki.Properties.Resources.Braided_1, "Hair3"),
    new Hair(Loki.Properties.Resources.Braided_2, "Hair11"),
    new Hair(Loki.Properties.Resources.Braided_3, "Hair12"),
    new Hair(Loki.Properties.Resources.Long_1, "Hair6"),
    new Hair(Loki.Properties.Resources.Ponytail_1, "Hair1"),
    new Hair(Loki.Properties.Resources.Ponytail_2, "Hair2"),
    new Hair(Loki.Properties.Resources.Ponytail_3, "Hair4"),
    new Hair(Loki.Properties.Resources.Ponytail_4, "Hair7"),
    new Hair(Loki.Properties.Resources.Short_1, "Hair5"),
    new Hair(Loki.Properties.Resources.Short_2, "Hair8"),
    new Hair(Loki.Properties.Resources.Braided_4, "Hair13"),
    new Hair(Loki.Properties.Resources.Side_Swept_1, "Hair9"),
    new Hair(Loki.Properties.Resources.Side_Swept_2, "Hair10"),
    new Hair(Loki.Properties.Resources.Side_Swept_3, "Hair14"),
    new Hair(Loki.Properties.Resources.Pulled_back_curls, "Hair15"),
    new Hair(Loki.Properties.Resources.Gathered_braids, "Hair16"),
    new Hair(Loki.Properties.Resources.Neat_braids, "Hair17"),
    new Hair(Loki.Properties.Resources.Royal_braids, "Hair18"),
    new Hair(Loki.Properties.Resources.Curls_1, "Hair19"),
    new Hair(Loki.Properties.Resources.Curls_2, "Hair20"),
    new Hair(Loki.Properties.Resources.Twin_buns, "Hair21"),
    new Hair(Loki.Properties.Resources.Single_bun, "Hair22"),
    new Hair(Loki.Properties.Resources.Short_curls, "Hair23"),

    new Hair(Loki.Properties.Resources.Blob_hair, "TrophyBlob"),  // ???? Originally in Loki
};

Note: A lot of hairs in Loki's source code was incorrect. Example is a hair being listed as Long 1 when it is actually Ponytail 1. Not sure if that's because the game changed it or not. But the above list is the correct list for both hairs and beards.

For items, the new categories are (ItemType used in InventoryListItem.cs):

Fish = 21,
TwoHandedWeaponLeft = 22,
AmmoNonEquipable = 23,

Example: Dead Raiser -> TwoHandedWeaponLeft Black Metal Misile -> AmmoNonEquipable Wooden Metal Misile -> AmmoNonEquipable Fish -> Perch, Pike, Tuna + Others

With those changes, I'm fairly certain Loki should be flawless and it should technically work fine and support all of the Mistlands updates.

EDIT: The list is still not flawless. There are items like: DvergerArbalest_shoot which doesn't make sense and I haven't figured out how to filter it yet. There doesn't seem to be any parameter differences: https://www.diffchecker.com/IAuh1hkt between two items of the same type (one being an NPC item and one being a Player item).

EDIT: Verified there is no other way to determine if an item is legit.

jensbrak commented 1 year ago

Note: A lot of hairs in Loki's source code was incorrect.

Yes. They were hard coded to be localized and must have been updated since. I changed that and read the beards and hair from the prefab file instead. Which means we'd like to keep those and not filtering them out I suppose. I considered to do like you did above, but opted to skip localization in favor for future updates to be easy:

        private static readonly IEnumerable<Hair> SensibleHairs =
            ItemDb.AllItems
                .Where(i =>
                    i.ItemType == ItemType.Customization
                    && i.ItemName.ToLower().Contains("hair"))
                .Select(i =>
                    new Hair(i.DisplayName, i.ItemName))

and

        private static readonly IEnumerable<Beard> SensibleBeards = 
            ItemDb.AllItems
                .Where(i => 
                    i.ItemType == ItemType.Customization 
                    && i.ItemName.ToLower().Contains("beard"))
                .Select(i => 
                    new Beard(i.DisplayName, i.ItemName));

Also needed is to map new categories (Fish etc) to UI strings. Done that too in my update. Maybe one should deal with customisations like you showed instead of reading them from prefab, to keep possibility to localise. Dunno.

BTW: I changed your program (but haven't succeded in compiling it yet) to produce CSV format as I use that in my PR (see https://github.com/Wufflez/Loki/pull/34 ). My updates for Mistlands can be seen here: https://github.com/jensbrak/Loki/tree/Features/MistlandUpdate (based on PR 34). This is how I intended to use it:

        std::stringstream ss;
        ss << item_data.item_name << ",";
        ss << (item_data.is_teleportable ? "true" : "false") << ",";
        ss << (item_data.uses_durability ? "true" : "false") << ",";
        ss << item_data.max_durability << ",";
        ss << item_data.durability_per_level << "," 
        ss << item_data.max_stack << "," 
        ss << item_data.display_name << ",";
        ss << item_data.max_quality << "," 
        ss << to_string(item_data.item_type) << "\n";       

In essence, instead of storing prefab items hard coded like this:

    public static class ItemDb
    {
        private static readonly Dictionary<string, SharedItemData> ItemData = new Dictionary<string, SharedItemData>
        {
            ["Abomination_attack1"] = new SharedItemData
            {
                ItemName = "Abomination_attack1", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 1, DisplayName = "Swing attack",
                MaxQuality = 1, ItemType = (ItemType)3,
            },
            ["Abomination_attack2"] = new SharedItemData
            {
                ItemName = "Abomination_attack2", IsTeleportable = true, UsesDurability = false, MaxDurability = 100,
                DurabilityPerLevel = 50, MaxStack = 1, DisplayName = "Slam attack",
                MaxQuality = 1, ItemType = (ItemType)3,
            }, // ....

I opt to read it from external file (CSV) like this:

public static class ItemDb
    {
        private static readonly Dictionary<string, SharedItemData> ItemData = ReadItemDataFromCsvFile("SharedItemData.csv");

...and...

        private static Dictionary<string, SharedItemData> ReadItemDataFromCsvFile(string fileName)
        {
            try
            {
                using var reader = new StreamReader(fileName);
                using var csv = new CsvHelper.CsvReader(reader, CultureInfo.InvariantCulture);
                var items = csv.GetRecords<SharedItemData>().ToDictionary(item => item.ItemName);
                Debug.WriteLine($"Loaded {items.Count} items to shared item data");
                return items;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Failed to load shared item data. Details: {ex.Message}");
                return new Dictionary<string, SharedItemData>();
            }
        }
pythaeusone commented 1 year ago

@jensbrak can you make a compiled release on your Fork ? Thank you

jensbrak commented 1 year ago

can you make a compiled release on your Fork ?

Which branch? The one fixing the crash, the one separating item DB from code as discussed above - or the one adding actual Mistlands support (WIP)? I have more branches (.NET7 for instance) but these are not directly related to this thread and/or Mistlands.

(Either of the branches would be easy to release. A mix of them possible but then I need some time to choose what to merge and how. For instance, it would be logical to make a version that fix the bug (in case items are missing in the future) plus the separation of items data plus mistlands update plus .NET 7. That would, all in all, move Loki to a (imho) much better shape for future updates of Valheim, as well as make it work with Mistlands. )

pythaeusone commented 1 year ago

Had understood that you have already taken the Mistlands items, my mistake :-)

jensbrak commented 1 year ago

@pythaeusone I have, I was just not sure if that's what you wanted. WiP but it works fine so far with Mistlands. I fix a release for you asap!

jensbrak commented 1 year ago

an you make a compiled release on your Fork ?

Done. Please read disclaimer carefully. I have no intention to run a separate fork in parallell with this official repo. Just a courtesy release for anyone wanting to try out the Mistlands update I made as PR here - before it is merged (IF it is merged!).

pythaeusone commented 1 year ago

Thank you, ill try :-)

jensbrak commented 1 year ago

@Brandon-T I couldn't help myself. Inspired by your program I dug into the workings of the assets files myself. I played around a little and made the following observations - not fully confirmed but from the looks of it:

Nuff talking. Me playing around resulted in a working program, similar to yours. However, instead of C++ I used my go to language for precisely this kind of tinkering - Python.

from csv import writer
from glob import glob
from os import path
from sys import argv

# Load localization file into a dictionary with id and English translation as KVP for each line
def load_localization(pathroot):
    localization = {}
    with open(f'{pathroot}/Assets/Resources/localization.txt', 'r', encoding='UTF-8') as f:
        for translations in f.read().split('"\n"'):
            parts = translations.split('","')
            localization[parts[0]] = parts[1] 
    print(f'Loaded {len(localization):>4} translations from localization file')
    return localization

# Convert block of text to dictionary where each line is a KVP iff it contains at least one ':'
text2dict = lambda t : dict([[x.strip() for x in r.split(':', 1)] for r in list(filter(lambda l: ':' in l, t.split('\n')))])

# For each prefab file, return list of item data for all items that has shared item data and is localized
def load_item_data(pathroot, localization):
    item_data = []
    for i, filename in enumerate(glob(f'{pathroot}/Assets/PrefabInstance/*.prefab', recursive=False), 1):        
        with open(filename, 'r') as f:            
            sections = f.read().split('m_itemData:\n')
            if len(sections) < 2:       # Skip to next prefab file if there's no section with m_itemData
                continue                             
            data = text2dict(sections[1])
            id = data['m_name'][1:]     # Remove leading $ character
            if id not in localization:  # Skip to next prefab file if it's not localized                 
                continue
            name = path.basename(filename).split('.')[0] # Name of prefab file without extension == GameObject m_Name!
            if data['m_name'] == '':
                print(f'Meh: {name}')
            item_data += (
                name,
                bool(data['m_teleportable']),
                bool(data['m_useDurability']),
                int(data['m_maxDurability']),
                int(data['m_durabilityPerLevel']),
                int(data['m_maxStackSize']),
                localization[id],
                int(data['m_maxQuality']),
                int(data['m_itemType'])
            ),
    print(f'Loaded {len(item_data):>4} items from {i} prefab files')
    return item_data

# Save item data to CSV file
def save_item_data(item_data, pathfile):
    with open(pathfile, 'w', encoding='UTF-8', newline='') as f:        
        writer(f).writerows(item_data)
    print(f'Wrote  {len(item_data):>4} items to {path.abspath(pathfile)}')

# Process cmdline args and run script. Defaults used if args are missing. 
# Arg1: path to root of assets directory, Arg 2: path and filename to output file 
if __name__ == "__main__": 
    if len(argv) > 1 and argv[1].lower() in ['?', '/?', '/h', '--help', 'help']:
        print(f'Usage: python {argv[0]} [AssetsRoot] [CsvFile]')
        exit(1)
    pathroot = argv[1] if len(argv) > 1 else 'c:/_temp/ValheimData' 
    pathfile = argv[2] if len(argv) > 2 else './SharedItemData.csv'
    print(f'Processing assets in {path.abspath(pathroot)}...')
    localization = load_localization(pathroot)
    item_data = load_item_data(pathroot, localization)
    save_item_data(item_data, pathfile)
Brandon-T commented 1 year ago

@Brandon-T I couldn't help myself. Inspired by your program I dug into the workings of the assets files myself. I played around a little and made the following observations - not fully confirmed but from the looks of...

Yup! That's correct. Same exact observations. I did choose to read the asset name from the file itself just in case something changes in the future, as that's what the Unity engine reads.

All your observations are correct. I love python, but I use C++ daily at work, so it was very quick to write this :)

Yeah all of this could technically be added to Loki. Since we have localizations file too, we could export all the names automatically as well. I chose to leave Loki's code exactly how it is, and just use the same format that it uses already, but with updated items and customizations.

I did spend some time to update Loki to .NET 6 (whichever is the latest at this time). I'll share it all in a bit.

jensbrak commented 1 year ago

I did spend some time to update Loki to .NET 6 (whichever is the latest at this time).

Loki has already been updated to .NET 7 (latest), it's om main branch. :)

I did choose to read the asset name from the file itself just in case something changes in the future, as that's what the Unity engine reads.

Probably wise, but I suspect it's no coincidence that they match.

I love python, but I use C++ daily at work, so it was very quick to write this

Cool! I did C++ at work for many years ago (game dev actually) but have been using .NET (mainly) since like 2007. However, I find it interesting to dig into other languages and Python was one of them. Now I try to get myself comfortable with Rust, but it's quite a learning curve tbh, even though I really like the thoughts behind it. There's a tool for every job, but the best tools are also the ones you master. :) Found your C++ snipped refreshingly pedagogical to brush up my C++ a little :P

chose to leave Loki's code exactly how it is, and just use the same format that it uses alread

Since I have a working version using external CSV file for item data I use that one. :D

jensbrak commented 1 year ago

Oh well, the python script had some hidden bugs that probably could be fixed easily. Instead and for the fun of it, I converted the program to C#...

using CsvHelper;

// Print help if asked for
if (args.Length > 0 && new string[] { "/?", "--help" }.Any(s => args[0].ToLower().Equals(s)))
{
    Console.WriteLine($"Usage: ValheimItemDataExtract [ASSETROOT] [CSVFILE]");
    Console.WriteLine($"Extract Valheim item data from unpacked Unity assets within ASSETROOT directory and write them to CSVFILE.");
    Environment.Exit(0);
}

// Prepare what we need in order to run, including using defaults if cmdline args are missing
var PathRoot = args.Length > 0 ? args[0] : @".\ValheimExportedData\";
var PathFile = args.Length > 1 ? args[1] : @".\SharedItemData.csv";
var Localization = new Dictionary<string, string>();
var ItemData = new List<Object>();

// Process assets and save result
Console.WriteLine($"ASSETROOT is {Path.GetFullPath(PathRoot)}.");
LoadLocalization(PathRoot);
ExtractItemData(PathRoot);
SaveItemData(PathFile);

// Function to load localization file into a dictionary with name as key and English translation as value
void LoadLocalization(string pathRoot)
{
    var subPath = @"Assets\Resources\localization.txt";
    Console.WriteLine($"Processing file {subPath}...");

    foreach (var line in File.ReadAllText($@"{pathRoot}\{subPath}").Split("\"\n\""))
    {
        var parts = line.Split(new string[] { "\",\"", ",\"" }, StringSplitOptions.RemoveEmptyEntries);
        if (parts.Length > 1 && !Localization.ContainsKey(parts[0]))
        {
            Localization[parts[0]] = parts[1];
        }
    }
    Console.WriteLine($"Loaded {Localization.Count} translations.");    
}

// Function to extract item data from prefab files that have it and store it in a list
void ExtractItemData(string pathRoot)
{
    string[] ids = { "m_name", "m_teleportable", "m_useDurability", "m_maxDurability", "m_durabilityPerLevel", "m_maxStackSize", "m_maxQuality", "m_itemType" };
    var subPath = @"Assets\PrefabInstance";
    var prefabs = Directory.GetFiles($@"{pathRoot}/{subPath}", "*.prefab", SearchOption.TopDirectoryOnly);
    Console.WriteLine($"Processing {prefabs.Length} files in directory {subPath}...");

    foreach (var filename in prefabs)
    {
        var sections = File.ReadAllText(filename).Split("m_itemData:\n");
        if (sections.Length < 2)
        {
            continue;
        }
        var data = new Dictionary<string, string>(ids.Length);
        foreach (var line in sections[1].Split("\n").Where(line => line.Contains(':') && ids.Any(id => line.Contains($"{id}:"))))
        {
            var parts = line.Split(':', 2); // Some variable values are strings with : in them, we don't want to split there
            var id = parts[0].Trim();
            var value = parts[1].Trim(new char[] { ' ', '\t', '$' }); // Note the $ since that's preceeding localized string m_name
            data[id] = value;
        }
        if (Localization.TryGetValue(data[ids[0]], out string? englishTranslation))
        {
            // Anonymous type objects will do as records
            ItemData.Add(new 
            {
                ItemName = Path.GetFileNameWithoutExtension(filename),
                IsTeleportable = Convert.ToInt32(data[ids[1]]) != 0,
                UsesDurability = Convert.ToInt32(data[ids[2]]) != 0,
                MaxDurability = Convert.ToInt32(data[ids[3]]),
                DurabilityPerLevel = Convert.ToInt32(data[ids[4]]),
                MaxStack = Convert.ToInt32(data[ids[5]]),
                DisplayName = englishTranslation,
                MaxQuality = Convert.ToInt32(data[ids[6]]),
                ItemType = Convert.ToInt32(data[ids[7]]),
            });
        }
    }
    Console.WriteLine($"Extracted {ItemData.Count} items.");    
}

// Function to save the extracted item data to CSV file, provided out of the box by CSV helper package
void SaveItemData(string pathFile)
{
    using (var writer = new StreamWriter(pathFile))
    using (var csv = new CsvWriter(writer, System.Globalization.CultureInfo.InvariantCulture))
    {
        csv.WriteRecords(ItemData);
    }
    Console.WriteLine($"Saved {ItemData.Count} items to {Path.GetFullPath(pathFile)}");
}