Suggestion General Various Fixes, Thoughts

Users who are viewing this thread

xenoargh

Grandmaster Knight
1. For Hand Armors, there should be a "no_slim" XML tag that can be combined with "hides_hands" or not. This tag should prevent _slim variations of meshes from being used while gloves are being worn. This would allow gloves that aren't giant forearm-covering objects while not breaking the current behavior of the engine, and it could be a one-time check when loading up an Agent's clothing.

2. Vertex colors for meshes using the various core human body Materials aren't being utilized when said Material is used by anything but the core body meshes. It looks like the current logic is something like:

if(is_human && mesh.isBaseMeshNoClothing) --> set the vertex color value from the character generator / facegen code in the shader.

What it should be doing instead is checking a (new) Material Shader Flag "use Character Skin Color" (a boolean). If boolean == TRUE, then pass the color value into the shader and apply it.

This would be, by far, the best solution. It's one binary check per submesh per frame, and it's consistent. If, say, a modder creates brand-new Materials for the skins of their characters and wants them to have color variations that players can pick out, that's supported, without any complex changes on the engine side.

Here's why it's a problem already. Let's say we try and make an armor that uses a submesh with one of the human body Materials. It looks like this in-game:

mesh_after_testing_ingame_no_vert_color.jpg


Add in problems with various Races not having correct colors supported unless they're naked, etc.

3. There aren't good measurement tools in the Meta Mesh Editor for measuring commonly-needed things, such as weapon lengths. Suggestions:

A. There is already a bounding box being built automatically. By simply subtracting the Y-distance from the floor to the bottom of the mesh, you'd have the weapon's length value (in this case, 96 cm).

mesh_measurement_tool.jpg


B. Ideally, the end-user would get that value, the total length, width and depth.

C. Even more ideally, you'd replicate OpenBRF's point-to-point measuring tool, for times when modders need to measure something odd in a mesh, like the height of a doorway in a building mesh that they're not sure they exported at quite the right scale.

4. The Meta Mesh Editor could use a few basic features it currently lacks:

A. Scaling, Translation, Rotation and Mirroring of a mesh.

B. A standard human hand mesh, positioned at the grip point (i.e., the center in this case) that can be turned on / off in the display. Or just a nice colored line that shows the user really clearly where the handle of a given weapon is going to be.

5. There needs to be a simpler path to start building code for mods. Now that I've seen it, the C# route could use a major improvement. The example projects here on this Forum come with instructions that are either vague, missing important relevant information that newbie coders might not figure out, or are otherwise feeling incomplete. Current mods are highly dependent on reflection libraries, mainly Harmony, which probably doesn't help much with stability and definitely makes debugging mods harder.

Suggestions:

A. Consider strongly building in a C# runtime compiler, that will take loose code in a mod's folders and compile it, reporting errors usefully along the way. This means a new modder's first project could be as simple as a text file in an appropriate folder with some very simple code to show "hello world" in the game, rather than a lengthy process of setting up an IDE, setting up dependencies, etc. etc., which must be costing you hundreds if not thousands of potential modders over time.

Moreover, by encouraging runtime code as the standard, you'll be encouraging coders to stay safe. Code shouldn't be allowed to access IO willy-nilly; there should be clearly-defined methods to access files within the application's directories (reading an XML or JSON file, for example) but things like write access could be largely curtailed, making things safer. Right now you're at the legal mercy of whoever's maintaining the Harmony libraries being used, which may do any number of things, and the modders using them are totally free to write dangerous code using your engine's popularity as a good way to introduce trojans, etc.

This isn't merely scaremongering. A recent attack was made using Java in Minecraft mods, for example. Your company's reputation, if not strict liability, is at risk.

B. Bug reporting from the engine needs to be a lot more robust. I've noticed that the engine crashes silently and without useful feedback for simple mistakes in XML files, for example; at the very least, it should be outputting "Engine Crashed! Reading XML file <filename> at line <line>". This alone would save modders a ton of time.

C. Bug reporting from code, should proposal A be built into the engine, should become much easier. Instead of running a complex debugger, the engine can now report, "Engine Crash! Running Mod Code From <mod name>, Function <function name>, Line <line number>"

D. Instead of modders building code an using reflection to override the engine's code (with some danger involved- code run by reflection can do, well, anything it wants to) code could be running from internal callbacks. For example, your typical Mission code's game tick could have a callback method called Update() and modders could be writing their methods to hook into that, to do whatever they want to do during a Mission's game tick. Same with the Strategic Overworld, etc.

This largely depends on the existing code being able to be read, however! It's all well and good to want to, say, write new code to change the economy, but one needs to know where the relevant Objects are, the getters and setters, etc. That leads into the next point...

E. To better-support modders, strongly consider releasing the code of all gamecode DLLs in a ZIP file for every build, just as in the old days, when you distributed a new, uncompiled Module System with each new engine build. Right now, experienced coders using Visual Studio can look at gamecode classes and methods, to a certain extent, but are often in the dark about the inner functions or expected parameters of inputs and outputs.

6. Somebody should write a short, simple, "my first sword" document for how to build, texture and import a piece of modded content, and there needs to be a place here (not on Discord, where it's impermanent) to ask questions. I feel like the current engine kind of presumes that the only people building stuff for it will largely be 3D modeling whizzes.

Warband should have taught you better than that.

Back then, and still today, the vast majority of people wanting to try their hand at building 3D content will be relative newbies, and they need their hands held the entire way.

They don't know what Blender is. They don't know how to use it. They certainly don't know about making multiple meshes with fancy names like "my_cool_sword.1.lod2" and what all that means. Half the stuff I built was 3D meshes people built, but didn't understand enough about uvmapping and painting a skin to get done, or which needed extensive remastering. I loved helping people out with that and I made a couple of friends I know even today... but bear in mind that this is your real audience. Not just the whiz-kids. The 13-year-old kid who's seen this game, thinks it's neato, and wants to put in a sword. Support that level of simplicity and think about those people's experience.

I did something like that back then, IIRC. I'm probably not going to do it for you again. Find somebody who wants to stay and has the skills, both technical and as a writer.

7. I know it's too late to do anything at all about it, but XML sucks. It's hard to read and it's unpleasantly easy to break. JSON with a modern JSON interpreter that allows in-line comments, etc. would've been a much better choice. <shrugs> maybe back then, JSON was still too crude-looking to use. But now it's a slam dunk for a text format for data that's clear, readable and flexible.


8. There are dozens of questions about how art workflow works that nobody appears to know. You might want to fix that.

Stuff like (for example):

"Can we still use vertex animations for bow / crossbow animations" (I wanted to know, because I made a Chu-No-Ku crossbow for Warband, with animation, but it cannot work correctly by merely pulling some vertexes backwards in a shader.

"How many FBX files, including all the LODs, are really required for a complete 2-sex suit of armor" (the answer, unfortunately, is "four", and I think that's sad, when we just needed one, with one vertex-animation morph, in Warband).

"Some meshes have closed fists while others have open hands, are there shape keys for hand armours? And if so, does Taleworlds have a way to automate this?" Nobody knows, but one presumes you have a hand rig somewhere that you use to do this relatively easily. Export the FBX and put it in the next release in the modder's resource folder, along with all of the human / horse / camel bodies, please. Having to use TPACTool to break into your content, merely to get a working body mesh, is ridiculous.
"What are the art-asset rules for Hairs and Beards, and what is a workflow for putting a new one into the game?"

There are dozens more questions like this here. Most of them are unanswered.




...whew

Anyhow, that's it. I've had a pretty frustrating experience with this engine overall. The main problem, from a newcomer's perspective, is the confusing / misleading documentation, lack of a "jump right in" that's actually easy, and if you want to mess with content, the frustrations of things like, "you can look at our content, but you cannot override, change or export any of it", which is kind of the antithesis of modding, really, lol. Oh well. I'm glad that the game's sold well enough that maybe all this will get fixed up and I'll come back to a Skyrim total-conversion mod at some point, lol.
 
New requests:

1. The Editor's interactions with Atmospheres (in particular, the way light addition appears to be implemented) isn't correct vs. the game. Materials using pbr_shading (see: burned_wood Material) when set up in the same way for weapons bloom into giant blobs of pure white in-game.

I found a workaround using pbr_metallic and the shader flag use_exposure_compensation, but it's not a very good solution; it can't use the secondary texture to provide the additive element, so it's working on the whole mesh, or nothing at all. It would be preferable if pbr_metallic allowed additive that, if the shader flag self_illumination is set, behaved correctly re: exposure, rather than getting washed out, via some value in a texture multiplied by the control value in Vector Argument 1 (w) to allow artists to produce gradations of colored light on objects.

2. If a modder moves a folder to a new sub-directory or changes the name, the metadata the Editor uses gets badly messed up. Suggest that there's a feature to "clean metadata" that, if the Editor can no longer find <Texture, FBX, Etc.> in the AssetSources sub-directory where it used to be, recursively searches through all of the sub-directories in AssetSources trying to find a match. If found, the asset's metadata is corrected automatically. If it can't be found at all (for example, user has deleted a file) then it should mark the object with the exclamation-point as normal.

3. If a modder deletes a TPAC Geo entry in the Resource Browser, it also deletes the FBX file in the AssetSources folder! This is bad, because usually this is being done to fix problems with my points #2 or #4 (see below). At the very least, it should warn the user that they're about to delete their source file, that they may have been working on directly for hours, days, etc.!!! Thankfully, I didn't trust the Editor and all my files are backed up as .blend in other directories.

4. If a modder ends up in a situation where the metadata's screwed up for assets, they may try re-loading them. However, if a modder accidentally loads the wrong FBX into a TPAC Geo entry, using up a namespace reserved by another TPAC Geo entry, then one of the TPAC Geo entries disappears, making it impossible to correct the mistake!

A. Before a user loads up a FBX with <internal name> when doing a manual reload, if <name> doesn't match <metadata name>, the end-user needs to be warned before proceeding.

B. I have no idea why the TPAC Geo metadata gets erased or overwritten or masked by the other data in this situation, but I'd say that's a bug, not a feature.

5. The Editor gets into situations where, for some reason, it runs my GPU at full blast, even when the application's minimized. I've found that closing all but the Scene Editor (the default window) usually fixes this. Surely the other windows aren't needing a lot of GPU, especially if they're minimized?

6. The Editor has some sort of memory leak or other serious problem leading to a sudden crash. It's not a matter of whether it's unstable and going to crash, but when. I've run it for hours, and I've also had situations where it crashed pretty quickly. It's much, much more likely to crash if you exit Edit Mode to enter the game and come back again more than once, but it has often crashed merely because I left it running overnight and un-minimized it, lol.

7. The Editor claims it can vertex-paint colors, but the feature doesn't work.

8. Articles I found online claimed that bow animations involved painting the bow's vertexes blue, and the string in aqua (RGB 0, 255, 255). This isn't correct. It appears bows use vertex animations, but there are no examples or documentation of this feature and how if any it differs from Warband.

9. The Editor appears to use a lot more GPU memory than the regular game does. I've seen corrupted textures and other things that indicate the GPU ran out of memory... and, uh, I have a 3080 TI with 12 GB of VRAM, lol. There might be something wrong there, lol.

10. The Editor has a method to make a new Shader... but it's totally black-box:


shader_mystery_box.jpg


There isn't any documentation for this at all and it appears we're supposed to be providing relative filepaths to... er... something?
 
There needs to be simple, straightforward methods to create new MobileParty objects for modders to manipulate.

First off, MobileParty is a sealed class; it also has elements that get touched by multithreaded code. Making a Party of <template> assigned to Clan <Clan> has turned out to be ridiculously difficult, lol.


For modders, there needs to be a public static class that allows modders to build a new MobileParty that Just Works. This is a theme I keep hitting; there should be a DLL that's nothing but classes and methods explicitly for modders to access things in the world and do stuff like access the WeeklyTick / etc. callout (currently, only accessible via Reflection) so that we can actually, you know... do things.


Ideally, such a class has a method like this:

public static MobileParty MakeANewParty (String PartyTemplateNameInXML, String ClanID, Vec2 LocationInWorld, Float RadiusToSearch)

Where:

PartyTemplateNameInXML is exactly that- the template name in partyTemplates.xml. So, for example, the user could use "looters_template".

ClanID would require a reference to a Clan's name in spclans.xml, using the Faction's ID. So in this case, "looters".

LocationInWorld is pretty obvious.

RadiusToSearch should trigger an attempt to find a valid location to spawn at, using code that already exists.

That's how it should work. Simple, refers to the XMLs, not to internal names of Objects, etc. Method should return a pointer to the MobileParty that was created.



Also, the method MapFaction in MobileParty needs a setter operation; all it has is a getter. Sometimes a modder needs to change a MobileParty's Clan, which is apparently controlled by the IFaction assigned there. In general, there shouldn't be public-facing code that doesn't have both a getter and setter; if there's some reason that has to be read-only, then there needs to be a public static method in MobileParty that allows this to be changed.
 
Last edited:
As an example of just why the above needs to exist... look at the rabbit hole I went down, merely trying to make some new Bandit parties.

C#:
[HarmonyPatch(typeof(CampaignEventReceiver))]
[HarmonyPatch("WeeklyTick")]
private static class newWeeklyTick
{
    private static Boolean Prefix()
    {
        //InformationManager.DisplayMessage(new InformationMessage("Running the party spawner: " + Clan.BanditFactions.GetRandomElementInefficiently().Name, TaleWorlds.Library.Color.White));
        Clan randClan = Clan.BanditFactions.GetRandomElementInefficiently();
        CreateBanditPartyAlt(Campaign.Current.MainParty.Position2D, randClan, randClan.Name.ToString());
        return true;
    }
}


//BANDIT CREATION SCRIPT
public static void CreateBanditPartyAlt(Vec2 location, Clan clan, string partyName)
{
    float shortestDistance = 999999999999f;
    Settlement finalSet = null;
    Vec2 finalDest = location;
    foreach (Settlement set in Settlement.All)
    {
        if (set.IsVillage)
        {
            float distance = MathF.Abs((location - set.GetPosition2D).Length);
            if (distance < shortestDistance)
            {
                finalSet = set;
                finalDest = set.GetPosition2D;
                shortestDistance = distance;
            }
        }
    }

    InformationManager.DisplayMessage(new InformationMessage("Creating new party: " + clan.Name + " at " + finalSet.Name, TaleWorlds.Library.Color.White));
    MobileParty theParty = BanditPartyComponent.CreateLooterParty(clan.Name.ToString(), clan, finalSet, false);

    PartyTemplateObject pt = clan.DefaultPartyTemplate;

    //theParty.SetCustomHomeSettlement(finalSet);
    theParty.ActualClan = clan;
    InitializeMobilePartyWithPartyTemplate(theParty, pt);
    theParty.Position2D = finalDest;
    theParty.Party.UpdateVisibilityAndInspected();
    theParty.Party.Visuals.IsVisibleOrFadingOut();
    theParty.Party.Visuals.SetMapIconAsDirty();
    theParty.Party.Visuals.ValidateIsDirty(theParty.Party, 0f, 0f);
}

private static void InitializeMobilePartyWithPartyTemplate(MobileParty theParty, PartyTemplateObject pt)
{
    FillPartyStacks(theParty, pt);
    theParty.Party.Visuals.OnStartup(theParty.Party);
}

private static void FillPartyStacks(MobileParty thisParty, PartyTemplateObject pt)
{
    for (int i = 0; i < pt.Stacks.Count; i++)
    {
        int numberToAdd = pt.Stacks[i].MaxValue;
        if (numberToAdd > 0 && pt.Stacks[i].MaxValue > pt.Stacks[i].MinValue)
        {
            float playerProgress = Campaign.Current.PlayerProgress;
            int RandStack = rnd.Next(pt.Stacks[i].MinValue, pt.Stacks[i].MaxValue);
            //InformationManager.DisplayMessage(new InformationMessage("Warmup: Player Progress = " + playerProgress, TaleWorlds.Library.Color.White));
            if (playerProgress < 0.1f)
            {
                //Lowers spawn sizes quite a lot at lower levels.
                numberToAdd = MathF.Round(0.25f * RandStack);
                //Makes sure that the first slot is always populated!
                numberToAdd = Math.Max(1, numberToAdd);
            }
            if (playerProgress >= 0.1f && playerProgress < 0.5f)
            {
                numberToAdd = MathF.Round(0.5f * RandStack);
                //Makes sure all slots are always populated!
                numberToAdd = Math.Max(2, numberToAdd);
            }
            if (playerProgress >= 0.5f && playerProgress < 0.75f)
            {
                numberToAdd = RandStack;
                //Makes sure all slots are always populated!
                numberToAdd = Math.Max(4, numberToAdd);
            }
            if (playerProgress >= 0.75f)
            {
                numberToAdd = MathF.Round(1.5f * RandStack);
                //Makes sure all slots are always populated!
                numberToAdd = Math.Max(8, numberToAdd);
            }
        }

        CharacterObject character = pt.Stacks[i].Character;
        thisParty.AddElementToMemberRoster(character, numberToAdd, false);
    }
}

[HarmonyPatch(typeof(MobileParty))]
[HarmonyPatch("MapFaction")]
[HarmonyPatch(MethodType.Getter)]
private static class newMapFaction
{
    private static Boolean Prefix(MobileParty __instance, ref IFaction __result)
    {
        if (__instance.LeaderHero == null && __instance.IsBandit)
        {
            IFaction neutralFaction = Clan.BanditFactions.GetRandomElementInefficiently();                   

            //Safety code
            __result = neutralFaction;
            return false;
        }
        return true;
    }
}


So... this produced new Bandit Parties, they're valid MobileParty objects, and they're populated in the gameworld.

However, the game kept crashing on MapFaction, because none of the methods invoked after building the MobileParty, including explicitly setting ActualClan, prevented MapFaction from crashing around line 796.

Hence the wonderful expedient of writing a customized Getter, lol. That works, but because the element is picked at random, it leads to weirdness with the labels on the parties, lol.
 
OK, here's a working example of a MobileParty generator that doesn't require quite as much hackery. Again... this isn't what you want, Taleworlds. Creating a MobileParty of <template type> should be a one-line code operation on the modding end, not this reflection-dependent craziness.

C#:
        [HarmonyPatch(typeof(CampaignEventReceiver))]
        [HarmonyPatch("WeeklyTick")]
        private static class newWeeklyTick
        {
            private static Boolean Prefix()
            {
                Vec2 location = Campaign.Current.MainParty.Position2D;
                List<MobileParty> stuffToKill = new List<MobileParty>();
                foreach (MobileParty mp in MobileParty.AllBanditParties)
                {
                    if (mp != null)
                    {
                        //Uses a square-search method for speed.
                        Vec2 distance = location - mp.GetPosition2D;
                        float absX = MathF.Abs(distance.X);
                        float absY = MathF.Abs(distance.Y);
                        if(absX > 100f &&  absY > 100f)
                        {
                            stuffToKill.Add(mp);
                            InformationManager.DisplayMessage(new InformationMessage("Culling party! " + mp.Name.ToString(), new Color(0f,1f,0.25f)));
                        }
                    }
                }

                foreach (MobileParty mp in stuffToKill)
                {
                    if (MobileParty.AllBanditParties.Contains(mp))
                    {
                        mp.RemoveParty();
                    }
                }

                for (int i = 0; i < 5; i++) {
                    Clan randClan = Clan.BanditFactions.GetRandomElementInefficiently();
                    CreateBanditPartyAlt(location, randClan, randClan.Name.ToString());
                }

                
                return true;
            }
        }


        //BANDIT CREATION SCRIPT
        public static void CreateBanditPartyAlt(Vec2 location, Clan clan, string partyName)
        {
            float shortestDistance = 50f;//I think this is kilometers... maybe?
            List<Settlement> finalSet = new List<Settlement>();
            Vec2 finalDest;
            foreach (Settlement set in Settlement.All)
            {
                if (set.IsVillage)
                {
                    //Uses a square-search method for speed.
                    float absX = MathF.Abs(location.x - set.GetPosition2D.x);
                    float absY = MathF.Abs(location.y - set.GetPosition2D.y);

                    //If we're in the square, do the thing.
                    if (absX <= shortestDistance && absY <= shortestDistance)
                    {
                        finalSet.Add(set);
                    }
                }
            }
            if(finalSet.Count == 0)
            {
                InformationManager.DisplayMessage(new InformationMessage("Could not create new MobileParty, no valid destination was found!!!", new Color(1f, 0f, 0f)));
                return;//Abort!
            }

            Settlement setToSpawnAt = finalSet[rnd.Next(0, finalSet.Count)];
            finalDest = setToSpawnAt.Position2D;

            //InformationManager.DisplayMessage(new InformationMessage("Creating new party: " + clan.Name + " at " + finalSet.Name, TaleWorlds.Library.Color.White));
            MobileParty theParty = BanditPartyComponent.CreateLooterParty(clan.StringId, clan, setToSpawnAt, false);
            InformationManager.DisplayMessage(new InformationMessage("Building new party via random system! Clan: " + clan.StringId + " Location: " + setToSpawnAt.GetName().ToString(), TaleWorlds.Library.Color.White));

            //InformationManager.DisplayMessage(new InformationMessage("PT stringID: " + pt.StringId, TaleWorlds.Library.Color.White));
            
            PartyTemplateObject pt = clan.DefaultPartyTemplate;
            
            //InformationManager.DisplayMessage(new InformationMessage("Clan stringID: " + clan.StringId, TaleWorlds.Library.Color.White));
            //theParty.SetCustomHomeSettlement(finalSet);
            //theParty.ActualClan = clan;
            InitializeMobilePartyWithPartyTemplate(theParty, pt);
            theParty.Position2D = finalDest;
            theParty.Party.UpdateVisibilityAndInspected();
            theParty.Party.Visuals.IsVisibleOrFadingOut();
            theParty.Party.Visuals.SetMapIconAsDirty();
            theParty.Party.Visuals.ValidateIsDirty(theParty.Party, 0f, 0f);
            
        }

        private static void InitializeMobilePartyWithPartyTemplate(MobileParty theParty, PartyTemplateObject pt)
        {
            FillPartyStacks(theParty, pt);
            theParty.Party.Visuals.OnStartup(theParty.Party);
        }

        private static void FillPartyStacks(MobileParty thisParty, PartyTemplateObject pt)
        {
            for (int i = 0; i < pt.Stacks.Count; i++)
            {
                int numberToAdd = pt.Stacks[i].MaxValue;
                if (numberToAdd > 0 && pt.Stacks[i].MaxValue > pt.Stacks[i].MinValue)
                {
                    float playerProgress = Campaign.Current.PlayerProgress;
                    int RandStack = rnd.Next(pt.Stacks[i].MinValue, pt.Stacks[i].MaxValue);
                    //InformationManager.DisplayMessage(new InformationMessage("Warmup: Player Progress = " + playerProgress, TaleWorlds.Library.Color.White));
                    if (playerProgress < 0.1f)
                    {
                        //Lowers spawn sizes quite a lot at lower levels.
                        numberToAdd = MathF.Round(0.25f * RandStack);
                        //Makes sure that the first slot is always populated!
                        numberToAdd = Math.Max(1, numberToAdd);
                    }
                    if (playerProgress >= 0.1f && playerProgress < 0.5f)
                    {
                        numberToAdd = MathF.Round(0.5f * RandStack);
                        //Makes sure all slots are always populated!
                        numberToAdd = Math.Max(2, numberToAdd);
                    }
                    if (playerProgress >= 0.5f && playerProgress < 0.75f)
                    {
                        numberToAdd = RandStack;
                        //Makes sure all slots are always populated!
                        numberToAdd = Math.Max(4, numberToAdd);
                    }
                    if (playerProgress >= 0.75f)
                    {
                        numberToAdd = MathF.Round(1.5f * RandStack);
                        //Makes sure all slots are always populated!
                        numberToAdd = Math.Max(8, numberToAdd);
                    }
                }

                CharacterObject character = pt.Stacks[i].Character;
                thisParty.AddElementToMemberRoster(character, numberToAdd, false);
            }
        }
 
Further thoughts about generating MobileParty objects:

1. Most of the code in TaleWorlds.CampaignSystem.CampaignEventReceiver doesn't appear to operate (in 1.1.5) despite showing up in the code.

For example, writing a method that inherits DailyTick() just doesn't operate, ever! This is true of most of the code there dealing with time passing in the CampaignSystem context. This feels like a bunch of unfinished stub code that's never actually called. Thankfully WeeklyTick() is operational, but this is bad practice.

If public-facing code's not operational in the main branch, it shouldn't have passed release review; QA should be catching this. I understand that, realistically, this was probably written long after most of the QA team moved on from the Bannerlord project, but you might want to conduct an internal review to see what other code is dead. In this case, repairing it so that the callbacks run properly should be trivial.

2. There should definitely be methods in place for getting Clan objects, PartyTemplate objects, Settlement objects, etc. by their StringId values. This should be utility code for modders to use (it'll also help you internally by getting rid of elaborate workarounds). A simple example:

C#:
Clan clanById = getClanById("looters");

//Returns a valid Clan object, or null.
public static Clan getClanById(String stringId){
  foreach(Clan clan in Clan.All){
    if(clan.StringId == stringId){
       return clan;
    }
  }
  return null;
}

There should be public static methods for all of this kind of thing somewhere Really Obvious; I'd like to humbly suggest creating a namespace called TaleWorlds.ModdingUtilityCode where you can start building classes like that that are deliberately meant to be public-facing utility code to improve the modding capabilities of the engine and greatly shrink the amount of coding skill required to start building a mod.
 
In general, using static classes is evil, and the practice should be curtailed down to a bare minimum in BL's public-facing codebase.

When you use static on a class in C#, it's also sealed; it therefore cannot be extended via inheritance, and none of its methods may be altered without using reflection. This also applies to methods (which is really, really annoying in C#, since you can do that just fine in Java).

I'm wondering what the real overhead of Garbage Collection would be if things that really shouldn't be static (because you want the engine to actually, you know, be properly moddable) were instantiated when necessary.

For example, instead of situations like this:

C#:
public static class MissionCombatMechanicsHelper
    {
        public static bool DecideAgentShrugOffBlow(Agent victimAgent, AttackCollisionData collisionData, in Blow blow)
        {
            bool result = false;
return result;
        }
    }

Where absolutely none of the code within the class can be extended except via reflection...

Instead, have the class be merely public, instantiate MissionCombatMechanicsHelper via new when calling a method within it, and run the method or methods, resulting object to be GC'd later. Heck, you could store the object in a convenient place so that all calls to said class can just go to that Object, and never need to orphan it. For example, MissionCombatMechanicsHelper probably needs to be instantiated once, and only once, when a Mission launches.

Then modders could simply inherit from and extend that class to modify its behavior and when your own code calls it, it's all fine.

I doubt if the performance overhead is so terrible that it cannot be allowed, especially for things that aren't running terribly often, like damage-result code. I presume there's a fair amount of GC overhead already from killing the parts of the engine that are multithreaded, so it probably won't impact performance in a mission-critical way.
 
I noted some of these suggestions and we will discuss them at the next meeting.
Meanwhile, I hope you find answers to some of the points useful.

1. The Editor's interactions with Atmospheres (in particular, the way light addition appears to be implemented) isn't correct vs. the game. Materials using pbr_shading (see: burned_wood Material) when set up in the same way for weapons bloom into giant blobs of pure white in-game.
The TOD atmospheres that are visible at the editor may not be the same as the game version. To check the exact same atmosphere, you need to go into the Curved Atmosphere Editor and load the atmosphere sets for each faction. Also, since the lighting values are very different at different TOD's (a couple of magnitudes higher/lower), you have to use the Exposure Compensation feature. We have an internal task for creating documentation on Exposure Compensation, but it is a low priority.

2. If a modder moves a folder to a new sub-directory or changes the name, the metadata the Editor uses gets badly messed up. Suggest that there's a feature to "clean metadata" that, if the Editor can no longer find <Texture, FBX, Etc.> in the AssetSources sub-directory where it used to be, recursively searches through all of the sub-directories in AssetSources trying to find a match. If found, the asset's metadata is corrected automatically. If it can't be found at all (for example, user has deleted a file) then it should mark the object with the exclamation-point as normal.
Moving and renaming asset folders should be done via the "Resource Browser" inside the modding toolkit. Once done that way, there should not be any issues.

6. The Editor has some sort of memory leak or other serious problem leading to a sudden crash. It's not a matter of whether it's unstable and going to crash, but when. I've run it for hours, and I've also had situations where it crashed pretty quickly. It's much, much more likely to crash if you exit Edit Mode to enter the game and come back again more than once, but it has often crashed merely because I left it running overnight and un-minimized it, lol.
We would appreciate dumps for these kinds of crashes, if you can provide them, would be very useful for us reproducing them.

8. Articles I found online claimed that bow animations involved painting the bow's vertexes blue, and the string in aqua (RGB 0, 255, 255). This isn't correct. It appears bows use vertex animations, but there are no examples or documentation of this feature and how if any it differs from Warband.
Is this from the official documentation? If so, we would appreciate it if you pointed us to the exact part where this is mentioned incorrectly.

9. The Editor appears to use a lot more GPU memory than the regular game does. I've seen corrupted textures and other things that indicate the GPU ran out of memory... and, uh, I have a 3080 TI with 12 GB of VRAM, lol. There might be something wrong there, lol.
It is normal for the editor to use more GPU memory since it also supports the editing of content. However, it should not be as high as 12 GB. Either very big textures are being loaded into the engine or there is a leak. If you can provide us with some more info regarding the total size of the module you are working on, that would be great.

10. The Editor has a method to make a new Shader... but it's totally black-box:
There isn't any documentation for this at all and it appears we're supposed to be providing relative filepaths to... er... something?
We have an internal task for creating documentation on creating shaders.

There needs to be simple, straightforward methods to create new MobileParty objects for modders to manipulate.
In MobileParty class -> public static MobileParty CreateParty
This is used to create parties without components or custom components. This should probably be not used in most cases since this is an advanced usage.
The more basic approach would be to create parties through their components:
BanditPartyComponent[B].[/B]CreateBanditParty CaravanPartyComponent.CreateCaravanParty GarrisonPartyComponent.CreateGarrisonParty LordPartyComponent.CreateLordParty MilitiaPartyComponent.CreateMilitiaParty VillagerPartyComponent.CreateVillagerParty
and
CustomPartyComponent.CreateQuestParty
 
Thanks for taking a moment to respond.

I totally understand re: documenting some of the more-obscure stuff.

On the coding issue wrt Parties: that's already resolved on my end, I've just been too lazy to post known-good code for others yet. My other general notes on coding for this engine are largely still relevant.

On shaders: other than reading through some of the code, I'm nowhere near ready to write any shaders for Bannerlord and it's pretty clear they'll be harder to deploy and maintain due to how many dependencies they now appear to require.

Other than the particular use-case I pointed out (i.e., having a way to support additive behavior) I'm really pretty impressed by some of the features I have played around with. I'm looking forward to seeing whether I can make translucent materials at some point, for example, since all of the required engine-side things appear to be available.

On this point:
Moving and renaming asset folders should be done via the "Resource Browser" inside the modding toolkit. Once done that way, there should not be any issues.
This is all fine... provided it's all working properly, and the end-user makes no mistakes. However, when things get messed up in the metadata due to any mistake (and, well, I made a couple, lol), there's no undo and no easy way for end-users to correct these things. At one point, I merely renamed a mod's folder in Windows, re-opened the Editor... and broke dozens of assets :sad:


********************************************

Some other comments / feature requests about the Editor and Resource Browser in particular:

1. There is no way to rename meshes... and the whole way naming works is a bit odd. The names for the FBX in the actual file system don't matter; only the internal names for the meshes in the FBX matter. Shouldn't all of that be matched up during import? It's confusing to the end-user if a file named, "foo.fbx" can contain meshes "bar, bar.1" and "barfoo" (which can cause unexpected behavior).

As a side-note to this, there isn't any official documentation about multi-material setups for export, naming conventions, or how vertex animations should be set up (for animated meshes that use that technique, like gloves). I've figured out multi-material (and thus far I've been too lazy to document that for others, but I'll get there) but vertex animations look like something a few people on Discord have done, but there isn't a good user guide for Bows, Quivers, Ammo (axes and so forth), Scabbards, Shields or Gloves.

On your question re: vertex colors for bows; that's in one of the YouTube videos linked here on the Forum, IIRC, and it was noted in that document I posted a link to, I think. I'll try and find a link to that again.

I realize now that's wrong; you're still using vertex animations (which is great, btw). But I pointed that document out, etc. because it was one of the few semi-credible sources of detailed documentation I could find and the early stages of figuring this out were very frustrating. At this point, I'm trying to remember to write down anything I figure out as I go.

2. I don't understand why Native meshes cannot be copied or exported in the Editor. The impression I have, now that I've looked at this a bit more... is that this was just a minor oversight, not intended behavior. When Native's built, it's using the Editor option for publishing to Steam that's supposed to keep people from further modifying anything.

Can you consider, for the next official build, shipping Native with the setting that allows export instead? This would be helpful for learning, and I don't think that has any performance ramifications in your engine.

3. The only way to move a Material from one directory or another appears to be to Cut and Paste it. There is no Copy option; the end-user must use Duplicate. While this does work, this isn't a very standard way to handle this issue.

Meshes and Textures, however, can only be Cut and moved via Paste. There is no Duplicate function (this goes back to the issue with internal names, etc.). This is inconvenient: what if a modder wants to quickly make duplicates of an armor and put a different Material on it, for example. Right now, that involves renaming everything in Blender and re-exporting the FBX, which is cumbersome.

4. Re: resource / GPU usage. My mod's total textures and total for geometry at this time are just a few megabytes in size; I'm mainly porting over Warband assets at this time with texture sizes that are, by modern standards, tiny. It's not me. But if you load a save-game from the Editor to test something, you can expect to see textures that are messed up, or the occasional video-memory-related GPU crash. This doesn't happen in 1.1.6 while running the shipping version of the game, so I'm not sure what's going on there, but it's Editor-specific.

As for the Editor's long-term stability, it's definitely suffering some sort of memory leak. I'll try and capture a crash-dump next time. It's not a matter of whether it's going to crash, but when, and it's often running my GPU at maximum speeds, even when minimized, which is bizarre. I've had it running the GPU so hard that I had a hard-crash of the entire computer at one point. Now, if I'm using the Editor, I close it immediately, if I'm not going to keep using it in the next 30 minutes or so, and pay attention to the GPU temps.

Speaking of that (temps) I can't have been the only one having that problem, because there's a GPU-temp indicator display built into the Editor itself that starts displaying numbers when the GPU temp gets above 150 F, IIRC. I haven't the foggiest idea why running an empty scene, viewing icons in the Resource Browser or having a weapon rendered in the Model Viewer would be this intensive, though.
 
Just in case somebody in Engineering is like, "no way does the Editor crank up GPUs to max temps" here's a screenshot showing the built-in temp gauge coming on while my poor 3080 TI is trying to stay cool.

I think I know what's causing this. The Editor doesn't appear to frame-limited, and is rendering scenes at 1000+ FPS when there isn't much in the scene. That's a problem easily fixed; it should be locked to 60 FPS; unlike the actual game, we never need higher FPS than that.

BL_Temperature_Gauge.jpg
 
It would be very nice if body armors could have an ItemFlag no_pauldron boolean Attribute, that, if true, does not allow a Pauldron / Cape to be worn with it.

Reasons:

1. Aesthetics. Many body armors had pauldrons specifically created for them; they're part of the artistic or practical design. It's not doable to make every body armor work well with every possible pauldron / cape without excessive clipping, etc., and players will complain.

2. There are no female versions of the pauldrons and capes, so a lot of them just don't fit right. Their geometry is floating in mid-air, they're clearly designed for bigger shoulders, etc. I presume these issues are never going to get addressed, as it's a major ask for whoever's still working on game art at this point, and it'd be cheaper / faster to provide a route for modders to work around this issue, imo; the slot-blocking code already exists for Civilian items, so this would be a minor extension of that idea.

3. The only real workaround for female armors that don't look great with floating pauldrons that exists right now would be to create duplicate entries for each armor that has sex variants; one for males, one for females specifically referencing the _converted mesh, and then make most / all pauldrons non-wearable by females. This could be done but it's a rather large amount of extra work, has balance implications, etc.
 
Using reflection via Harmony on DefaultPartyWageModel.GetCharacterWage(CharacterObject character) can cause an engine crash, due to an object not being properly locked before access in multithreading code. The code that crashes appears to be used by the Lords to determine what Troops in their Party may be upgraded; it does something really odd where it hits a div-by-zero, presumably because something's very briefly not returning the correct state while Harmony is accessing the variable.

C#:
    internal class fixWagesOfTroops
    {
        [HarmonyPatch(typeof(DefaultPartyWageModel))]
        [HarmonyPatch("GetCharacterWage")]
        private class newGetCharacterWage
        {
            private static void Postfix(ref CharacterObject character, ref int __result)
            {

                if (character != null)
                {

                    if (character.IsHero && !character.IsPlayerCharacter)
                    {
                        __result = character.Level;
                    } else
                    {
                        //CAN CRASH
                        switch(character.Level){

                        }
                        //THIS CODE CAN RUN WITHOUT CRASHES (THUS FAR)
                        if (character.IsRanged)
                        {
                            __result = Math.Max(3,(__result * 3) / 2);
                        } else
                        {
                            __result = Math.Max(3, character.Level / 2);
                        }
                        
                        //Allows "elite pricing" for high-level Troops.
                        if (character.Tier > 6)
                        {
                            __result *= 2;
                        }
                        //Bumps/nerfs for whether the troop is mounted or is a Bandit scrub.
                        if (character.IsMounted) { __result *= 2; }
                        if (character.Occupation.Equals(Occupation.Bandit)) { __result /= 2; }
                    }
                }
            }
        }
    }
 
The Editor doesn't appear to frame-limited
Can you check whether the vsync is enabled in the config? The easiest way would be to go to the options from either the main menu or esc menu and change it from the UI. (works in the modding toolkit build as well).
It would be very nice if body armors could have an ItemFlag no_pauldron boolean Attribute, that, if true, does not allow a Pauldron / Cape to be worn with it.
We will bring this up internally.
 
Can you check whether the vsync is enabled in the config?
VSync is now enabled and I've verified that it works in the Modding Toolkit.

That really helps keep things cool and stable, in general, but in the Modding Toolkit, it's exceptionally important, because the CPU / GPU load is often so low that the GPU may be rendering hundreds of frames a second otherwise, which isn't good for it, lol. Honestly, in Modding Toolkit we simply shouldn't need FPS higher than 60, imo.

We will bring this up internally.

Thanks. These are all just minor things that would help modders shape the way gear equips work. In general, to have a system that's flexible and transparent for players, it's easier to exclude gear that's not desired by the artist.

But! Perhaps, instead of the idea of "no_pauldron", there should be an ItemFlag:

armor_incompatibilty = <string type enum>

Values would be "all", "all_but_helmet", "pauldron", "boots", "gloves".


This would cover 99% of use cases. In the vast majority of cases, "all_but_helmet" would give artists pretty complete freedom w/ body armors. For really extreme armors (say, somebody wants to put Batman's armor into the game) "all" would allow that.

But they could also, for example, have armor with built-in gloves, but have armor_incompatibility="gloves" and hides_hands="true".

So, the concept of "armor that can exclude other armor types from being worn" would be very useful for artists. As things stand, I'll just have to tell players that if they wear certain armors, that they'll look absolutely terrible w/ pauldrons and gloves, or are meant to be paired with <helmet>, but it can't be helped.

I still want an ItemFlag for "hides head" like we had in Warband, too. A full-face helmet that completely encloses the head and neck shouldn't need to be designed around the head / neck mesh being present and possibly clipping.
 
For the Model Viewer, I have several feature requests:

1. Under File or a new menu, "reset camera". Does what it says; the camera's origin, position and orientation are reset to the defaults. Reason: right now, if you ever move the camera around with the middle-mouse button and get the origin moved somewhere odd, you can end up in situations where you literally cannot see what you're working on and can't fix the camera without restarting the entire application (because Model Viewer's state is saved even when you close that window).

2. The camera in Model Viewer really needs a smoother zoom function. Middle-mouse scrolling is incredibly jumpy and it often makes it difficult to do screen-captures, etc.

Suggestion: keep middle-mouse wheel behavior as present, for rough adjustments, and right-click + LSHIFT for a very smooth, slow movement of the camera forwards and backwards. It'd be helpful if there was a similar function for panning, but it's less of an issue than zoom. This probably seems like a minor thing, but I'm sure your own staff has sometimes had trouble setting up a screenshot of a new asset, and had to resort to moving it around with Translate / Rotate, rather than use the camera controls.
 
As an example of what I was talking about for armors (being able to exclude wearing of <category>), this suit cannot be implemented properly in the current Bannerlord system at all:

bl_narf_04.jpg


1. It uses gloves that aren't Giant Elbow-Length Gloves. They're... just gloves. Or, in this case, gauntlets. They need the "no_slim" tag I mentioned earlier, otherwise they'd trigger the _slim meshes, which were clearly designed for Bannerlord's gloves, but don't work for anything that merely covers the hands.

2. The armor's not designed for _slim, and will look pretty bad with a _slim mesh and other gloves.

3. The armor, while it can technically operate with Bannerlord's boots, is really designed for these particular shynbaulds.

4. The helmet, which is one of the best we ever had from Warband, was designed for "hides_head". I've made it "work" here, as you can see, but I've had to make some artistic compromises I don't like: the neck area had to be made almost absurdly large to avoid clipping with the new body / head meshes and I've had to rig the bottom part of the mesh to the neck bone, which looks less than ideal, but it at least prevents the head mesh from clipping through the geometry.

5. Obviously, this kind of full suit of body armor, which is designed with explicit, unique pauldrons, cannot possibly be made to work with Bannerlord's pauldron / cape armors properly.

So, if I had the features I've requested, I'd probably declare the armor as "all_but_head"; the shynbaulds and gloves would be integrated into the suit; the suit would have the "hides_hands" flag enabled (I'm just going to guess here, but I'll bet your code doesn't even check that flag if it's body armor) and the head would use "hides_head" so that it didn't have to make so many compromises to its aesthetics (this might result in it "floating" above Bannerlord-standard armors at the neck join, but that's easier to fix than having to do a rig that makes it flex in inappropriate ways).

In short, this is, in one suit of armor, why I want much more control over how armors are presented, and I think this would be good for everybody, including you folks, should you choose to develop more content for the game. Armors should be able to hide all body parts, hand armors should be able to both hide hands and not trigger _slim, armors need to be able to exclude other types of body armor being worn. I think that most of these features should be (fairly) trivial, since pretty much all of the code exists (or can be easily re-purposed):

1. There's already code to exclude Item A if Item B isn't installed (horses and saddles); extending that to not allow certain equipment and uninstall it if an armor with <type exclusion> is installed by the user via the Inventory UI should be straightforward.

Essentially:

C#:
  switch(ItemFlag.armor_compatibility){
    "all":
        remove all other armor items by iteration on the Troop's ItemRoster; removes the items that aren't <this armor type> and places them back into the player's inventory
        alert user by marking the other item boxes in red
        break;
    "all_but_helmet"
        remove all non-helmet armor items
        alert user by marking the other item boxes in red
        break;
    
    etc.
        
    default:

        do nothing;

        break;

  }


2. "hides_head" is just another boolean Item Flag; hiding other parts is already done, so this is just one more binary check (I presume this check's done once, and only once, when the Agent spawns w/ their equipment, rather than every frame during a Mission- even in that case, it's trivial and non-expensive vs. how it works now, where the entire head, eyes, etc. are all being drawn or are depth-tested for culling, etc.). hides_head should, of course, default to false, to preserve current behaviors.

3. "no_slim" should be pretty straightforward:

If(ItemFlag.no_slim) --> use mesh mesh_name for males / mesh_name_converted for females. no_slim should default to false, of course, to preserve existing behaviors.
 
Last edited:
Back
Top Bottom