IEMod / IEMod.pw

A mod for Pillars of Eternity
31 stars 19 forks source link

Longer Term: Asset Editor #8

Open tjayharvey opened 9 years ago

tjayharvey commented 9 years ago

The original IEMod page (http://rien-ici.com/iemod/asset_editor) had a reference to an Asset Editor for adjusting the values of things that are covered in assetbundles). Are there other ways to handle editing of those objects? I'm less interested in the visual items (which would really need a graphical editor) and more interested in the capability to tweak the game data that's been embedded there. If they can be manipulated another way, that'd at least be additional functionality. If not, getting this up to date might be something to look at in the long term.

GregRos commented 9 years ago

Yes, there are other ways. I've played around with how to do it once. Pretty much every interesting resource like an item, spell, etc. can be accessed through an instance of a C# class. The relevant classes are GenericSpell, something like ItemList, etc. You can modify or create them after the game loads, so you could change the damage dealt by Wall of Fire or something, as well as create a new spell. You can also do more complicated things. You can't really see every member of a spell like Wall of Fire through its .NET class definition. You have to explore the Unity GameObject-Component model which exists outside of .NET, and can only be accessed through external calls.

However, I'm not sure how to design it in a way that can be easily used by others. I also haven't used it enough to say how reliable it is, at which point these objects should be modified, how long the changes will stick, etc. Basically, there are lots of questions to ask about this, and lots of experimentation to do to get it work, or even to know if it can be done reliably.

This way may actually better than editting the unity3d files, if it is actually practical, because you don't have to keep track of a million patched assets. You also don't have to maintain that extension.

I don't have any knowledge of the unity editor though, so I don't know if rewriting the extension would be hard or not. One thing that bothers me is that using it requires piracy, or paying absurd amounts of money.

tjayharvey commented 9 years ago

Yeah, I'm not up for supporting piracy.

We might be able to hook something into the game startup that runs a collection of object modifiers on the things after they've been loaded. That would certainly affect fewer files. Worth trying, at least. I think editing abilities, talents, and spells is probably something people would find useful. Probably will require a lot of playing with Unity to figure out.

tjayharvey commented 9 years ago

I've done some playing with this and it looks like it will be fairly complicated. Finding the spot where the abilities are actually loaded took some doing, and even then I'm not positive it uses the same ability object for multiple characters (i.e. multiple ciphers might have independent copies of Soul Shock). Would definitely require overriding some of the Attack/Ability classes as the members that we'd need to modify don't seem to be publicly writable. It is probably doable, but it's almost certainly not going to be something that's very accessible to casual modders. For instance, if I wanted to restore the original AE size on Soul Shock, I have to change the first get the ability (itself, tricky), then take the AttackBase the ability uses, then take the AttackBase linked to that one as an ExtraAttack (since the initial "attack" of Soul Shock is an invisible particle sent at your party member target), then adjust the (not-publicly-accessable) Blast radius.

So as I see it, we have two approaches:

  1. Figure out how to deserialize Asset bundles, edit them, and then have the patcher replace object bundles in the target.
  2. Write some kind of generic ability replacement routine that can take a set of ability changes and apply them (at character load and level up, probably). Probably takes some kind of config language telling it what to do (from routines that it knows about).

I'm not actually sure which of these is more fragile. Since the Asset Bundle just seems to be a serialized object graph, any change to those classes is going to break custom edited Asset Bundles. Similarly, the ability value replacer could fail spectacularly and require a lot of effort to fix if there's a significant refactor in combat code. But at least we'd get some help from the compiler.

I will try to play with this at some point more, but these are my thoughts from the first 6 hours I spent trying to make this work.

GregRos commented 9 years ago

That is pretty disheartening. I was really hoping it would be easier than that :(

Can you tell me where the abilities are loaded?

Also, did you take a look at Talents?

Do you think there is any hope of adding new talents or abilities that you can see on level up, for example?

As for accessibility, I can write a patchwork attribute that makes all the members of a class public. It breaks if you do this sort of thing everywhere (I have no idea why, could never trace the problem), but if you just use it with a handful of classes it would probably work.

tjayharvey commented 9 years ago

I'll have to hunt it down again (I threw away the code I was using, but I think I still have the relevant classes open in the decompiler). The object bundles themselves are deserialized in PersistenceManager, but as far as I can tell they only get loaded when either a) a character having one of the abilities gets loaded or b) The AbilityProgressionTable for that class gets loaded. I'm not 100% sure how the latter works, but that's almost certainly where new spells and talents would need to go. The problem is that none of this stuff seems to have constructors/initializers or even consistent mutators. I'm going to create a branch so I can play with it without mucking with my ability to apply bugfixes to other stuff.

I think if you could get the talents/abilities to load (with all of their relevant detail), they could be added to the correct AbilityProgressionTable and that would make them show up on level up.

In some of these cases, I just can't figure out how some of the private members get set. There are only read-access use of them in the body of the class, and it's private! At this point, I'm assuming the private values are just filled in by deserializing from the object bundle, but maybe I'm just missing something?

GregRos commented 9 years ago

I don't think so. Private members can get set by reflection, serialization, in the body of the class, or in a nested class.

I think it makes sense that there are lots of these private members, because they probably make the abilities in the unity editor, and then serialize it from there. They wouldn't need to set anything in the code. But it makes modding extremely confusing.

What about GameResources.LoadPrefab? It looks like things such as characters are loaded through PersistenceManager, but items and abilities that have been prepared in advance in the editor are loaded through GameResources.LoadPrefab. Or maybe they are connected and I haven't noticed. I assume after the prefab is loaded, it is cloned for each owner. There is code in LoadPrefab that involved specifically loading files ending in unity3d, but I can't see code like that in PersistenceManager.

GregRos commented 9 years ago

:DDDDDDDD

LOOK:

    [ModifiesType(nameof(GameResources))]
    public class mod_GameResources : GameResources {
        [NewMember]
        [DuplicatesBody(nameof(LoadPrefab))]
        public static T orig_LoadPrefab<T>(string filename, string assetName, bool instantiate)
        where T : Object{

            return default(T);
        }

        [ModifiesMember(nameof(LoadPrefab))]
        public static T mod_LoadPrefab<T>(string filename, string assetName, bool instantiate)
            where T : Object {
            var prefab = orig_LoadPrefab<T>(filename, assetName, instantiate);
            var asGo = prefab as GameObject;
            var ab = asGo?.GetComponent<AttackBase>();
            if (ab != null) {
                IEDebug.Log($"Loaded prefab: {asGo.name}");
                if (ab.name.Contains("Fireball")) {
                    ab.AccuracyBonus = 100;
                }
            }
            return prefab;
        }
    }

Result

Obviously the interface needs to be well-designed, but it looks like this method has potential. If we just squat on the LoadPrefab method and create a dictionary of object ⇒ modification, we can apply modifications to any game object we can identify by name or partial name (as requirements demand, though this may require a different data structure). I just hope that objects actually have to pass through LoadPrefab to be created, and that this method isn't fragile in some way.

We can also return our own custom objects that we added to AbilityProgressionTable if the game tries to load their prefabs, so it doesn't try to find prefabs that don't exist.

Unfortunately, this method is not without its shortcomings. For example, it doesn't work with items that create Fireballs, probably because the item contains an embedded copy of a fireball that isn't actually related to the fireball spell prefab, it just shares the same image and stats.

See image

Oh, if you plan to run the code, pull IEMod and Patchwork because it revealed a bug in Patchwork that I fixed.

tjayharvey commented 9 years ago

I think they are connected. That's where I started but moved into PersistanceManager because I was never able to get that to work (all I did was put log messages in each one), but I may have gotten the wrong signature (there's a bunch in that file IIRC).

That's great news though.

tjayharvey commented 9 years ago

Ah, yes. I remember running into this error before and assuming that Patchwork just didn't work with generics and that I couldn't override this method. That must have been why I went looking for where the characters are loaded.

tjayharvey commented 9 years ago

Yep, I succesfully modified Soul Shock's "Extra AOE". Though I may have gone a BIT overboard on my first attempt (AOE size was practically the size of Cad Nua). I think I also missed the fact that you could use GetComponent to get the AttackBase out. That explains why there's no public access within the class, something that's not at all obvious from reading the class code. Good find. Thanks for the help. At least I learned a lot about the different components of attacks, which should make modifying them easier!

GregRos commented 9 years ago

Sure :)

It's all part of the confusing and mysterious world of Unity GameObject-Components. I read a little bit of how it works in the Unity scripting tutorial. By the way, unless you're sure you want to get nulls as the return, you should use my extension method Component because it throws informative exceptions if it can't find something. I think I may have said this before, but NullReferenceExceptions gave me so many headaches I just can't stress this enough.