We-the-People-civ4col-mod / Mod

This is the repository where the mod resides.
90 stars 37 forks source link

CivEffects #93

Open Nightinggale opened 6 years ago

Nightinggale commented 6 years ago

I developed a system in Medieval Conquest called CivEffects. While intended to give more xml freedom, it's also a cache system, which should improve overall performance. Note that what I write here is not precisely the same as what is implemented in M:C. The difference is based on experience and should provide the same result, but with significantly reduced coding time.

xml configuration abilities

The idea is that we add a new info class called CivEffects. Other info classes like traits, founding fathers, events and so on can then get a tag, which makes it provide a CivEffect (though None should be ok). The idea is that tags can be moved from any of those into CivEffects, which makes them reachable by all classes using CivEffects. In other words it removes the issue of needing a tag for traits, but you stall because it's implemented in FFs. Adding ways for a player to get CivEffects seems easy and will not affect the CivEffect code itself or the places, which relies on data from CivEffects, meaning it's a system, which is easy to expand.

Performance (cache)

The entire structure of CivEffect is based around a cache system. Vanilla frequently loop info classes to figure out xml data while CivEffect looks up an int or int array to do the same task. This means it's not only fast, it will not be slowed down by increasing the number of elements in xml. Even better, the cache is incremental, meaning CivEffects are only looped at game startup and game load. Otherwise it's just one at a time and performance will be completely unaffected by the total number of CivEffects.

The idea is as follows. Say we take the function

bool CvPlayer::canUseUnit(UnitTypes eUnit) const;

It's a lookup in an int array (with 8 or 32 bit depending on what we agree on. It can always be changed later). It returns true if the cache is > 0. There is an autogenerated CivEffect, which provides +1 if no CivEffect is higher than 0, meaning anything not mentioned explicitly as unlockable by xml will be allowed. The value is the sum of all CivEffect owned by the player in question. Coding note: maybe make the default -1 if there is something higher than 0 and then make the cache default 1. That way if everything is allowed, the array can be an unallocated JIT array, which would save both memory and lookup time.

Boosting performance of vanilla code

There is more to this though. Units can be disabled by vanilla code in CivilizationInfos. This mean to figure out if a player can use a unit, the code has to look up the UnitInfo to get the unit class and then use the player to look up CivilizationInfo to see which unit that unitclass has for the civ in question. On CivEffect cache reset, we can do this check for each unit and if CivilizationInfo disables the unit, we can grant it an allow value of -50. That way regardless of which CivEffects are active, the combined allow for the unit in question will always be negative. This mean the code can just look up eUnit in the array and compare with 0 rather than looking up two different info classes. This should boost runtime performance even if no CivEffect will enable or disable any unit. The same approach can be used with buildings.

CivEffects aren't saved

Say we have a CivEffect, which grants +1 food production if the plot produces at least 5 food (that's a CivEffect tag in M:C right now). Somebody change the xml file to make it trigger at 4 instead of 5. On game load, the CivEffects are applied and it reads 4. There is no 5 in the cache anywhere and the effect is then applied to all the existing plots. In other words the degree of freedom to modify xml without breaking savegames is really high. Vanilla (and hence current codebase) has issues with cache being saved, meaning certain xml changes can corrupt savegames (which is another issue we should deal with eventually).

Implementation

M:C has classes for most of this. This means the CivEffect class reads from xml is a one line operation and it's very picky about what it reads. You can't write a unit if it expects a unit class and so on. On failure, it can actually tell Type it failed in, tag, what it read and what kind it expected. In other words the assert message should be enough to fix the problem without debugging (unlike most of such issues in vanilla).

Applying the CivEffect is similarly simple from a coding perspective. It's a function, which takes a pointer to the CivEffect in question and an int, which is 1 or -1 depending on adding or removing. Each cache class in CvPlayer is then called, being given the 1/-1 variable and a const pointer to the storage object from the CivEffect. One line operations for 1D, 2D and 3D caches. (avoid 3D caches if possible since they are big, but the code supports them). If it's simple int values (not arrays), it's just a matter of cache += int from CivEffect * (1/-1)

This allows easy to read and code implementation while it still gets the job done regardless of complexity.

At the end of the apply code, it has to have some code to conditionally mark stuff dirty for recalculating caches elsewhere or screen redrawing. It's fairly simple and obvious if and how this should be done when you work on a specific tag.

Nightinggale commented 6 years ago

Implementation details

Adding a new xml file (not CivEffect specific. This is how any file is done)

Make an info class, which inherits the base info class (providing a certain set of variables etc, mainly Type) Add this to CvGlobals. It needs a get(index), get() and getNumElements. The get without arguments is getting the entire vector for xml reading. I want to change this eventually #53 Add the file to CvXMLLoadUtility::readXMLfiles() (in CvXMLLoadUtilitySet.cpp)

The CivEffectInfo class then needs a ::read(pXml) function, which can be a copy paste from another info::read() and then delete the individual tag read lines.

That's it for adding a new xml file in the DLL. The actual xml file needs to be added and likely needs the xml schema file to be updated as well.

CivEffect base code

It consist of 3 functions in CvPlayer

Reset: Restore default value to each cache variable.

Init: Reset and then loop all conditions, which can provide a CivEffect. Say we only have traits, which can, then the function would be looping all traits and for each trait that the player has, get the CivEffect from CvTraitInfo and if any, call apply with that CivEffect and iChange = 1. Init is called on game start and game load, not during the game.

The autogenerated CivEffect should also be applied here.

Apply(iChange, *pCivEffect): Apply the CivEffect to the cache or remove it if iChange is -1.

Adding a tag to CivEffects

Say we add AllowsUnitClasses. We want it to be of type InfoArrayMod (a class from M:C), though we can use vanilla code too. We init it to


m_info_AllowsUnitClasses(JIT_ARRAY_UNIT_CLASS, JIT_ARRAY_ALLOW)

This tells it that it is an array of types from UnitClassInfos and each index has a variable of type allow.

In read:


m_info_AllowsUnitClasses.read(pXML, getType(), "AllowsUnitClasses");

It's the instance, pXML and getType() is the same for all (info arrays expect this) and the last is the tag name in xml. The info array does the rest of the work and will assert if set up incorrectly. Note: info arrays are fully python exposed and both DLL and python can gain read only access to the data, though only xml reading can write to it.

In apply CivEffect:

m_ja_iCacheAllowsUnits.addCache(iChange,  pCivEffect->getAllowsUnitClasses());

It's a JIT array and it can figure out what to do by just being given the info array. However this is the standard form used by most tags, but here we need to convert from UnitClass to Unit. In other words we add some extra variables.

m_ja_iCacheAllowsUnits.addCache(iChange,  pCivEffect->getAllowsUnitClasses(), this, &CvPlayer::getUnitType, NULL, JIT_ARRAY_UNIT_CLASS);

This will give the reading code the ability to convert from unit class to unit for the player in question. There is a similar function for buildings already written in M:C.

Last, but not least, we want it to remove all the units the player can't use because of CivilizationInfo. In reset (or init, doesn't really matter which one), loop all units and for each the player will never be able to build, add -50 to that index in m_ja_iCacheAllowsUnits. Since addCache can't affect units the civ can't build, the number will stay negative forever and forever be banned for the player.

That's it. Now it's possible to use bool CvPlayer::canUseUnit(UnitTypes eUnit) const anywhere in the code and get the result virtually instantly, particularly compared to the current approach of looking up xml data.

I should make a new ticket about JIT_ARRAY_UNIT_CLASS, or rather the system behind it. In short it assigns an enum value to each xml file with a Type. The enum value can then be used to get length of the file and getType of each index in that file. This is very useful for detecting invalid xml configurations and when preserving savegames #46 . Since the system also has overloaded functions, which takes an xml enum type as argument and returns the matching JIT_ARRAY enum, the system allows functions with template arguments, which really benefits code readability and code reusability.

abmpicoli commented 6 years ago

To tell you the truth... I didn't get it. :) Maybe because I don't code C++ that much...

Nightinggale commented 6 years ago

Summary: it's the most awesome C++ code you will ever see. Since there is a working implementation and you don't get the code, you can't argue against it :)

I will try to make it more simple. I think the issue is that when I developed this, I spent ages on it. Explaining everything, which went through my mind in a single post is likely impossible. I will try to make a simple explanation. Just know that by explaining it simple, some of the true power in it will not be explained. In other words CivEffects can do better than the following explanation.

CivEffects has 3 functions in CvPlayer. Reset: set all cache to default values (usually 0) Init: apply all CivEffects owned by the player at game startup or load.

Apply(int iChange, const CivEffectInfo *pInfo) This is the core of CivEffects. It's used when a player gains ownership of a CivEffect and it applies the xml data into the cache in CvPlayer. For arrays:

arrayName.addCache(iChange, pInfo->getSomething());

Think of this as

for (int i = 0; i < arrayName.size(); ++i)
{
    arrayName[i] += iChange * pInfo->getSomething()[i];
}

It's not precisely what goes on in the code, but in theory that's pretty much it. pInfo->getSomething() will always be an array from xml. iChange is 1 if adding the CivEffect and -1 if it's being removed.

The other common type in the cache is ints. They can be changed like:

m_iCacheVar += iChange * pInfo->getSomeInt();

Here m_iCacheVar will be an int in CvPlayer and contain the combined value from all owned CivEffects.

CvPlayer then has some get functions, like

int getIntCache() const;
int getArrayCache(int iIndex) const;

In other words the get functions will tell the combined contribution from all owned CivEffects. Since no CivEffect looping takes place, it runs really fast even with a very high number of CivEffects.

Next question: what can we use such a combined int value for? Well we can do:

int getMovementBonus(ProfessionTypes eProfession)
int getPopulationGrowthThreshold()

The classes behind the cache can handle 1D, 2D and 3D arrays with similar looking code, we can write something like

bool hasFreePromotion(ProfessionTypes eProfession, PromotionTypes ePromotion)
{
    return cache[eProfession][ePromotion] > 0;
}

Not only did it use a 2D array, it also did another trick. Since each CivEffect will add 1 if granted, we ask if one or more contributes this promotion for the profession in question.

This brings us back to units:

bool canUseUnit(UnitTypes eUnit)
{
      return cache[eUnit] > 0;
}

Again it tells if one or more CivEffects allows the unit. However there is more to using units than just CivEffects. There are Units and UnitClasses. Each UnitInfo has a UnitClass and each UnitClassInfo has a default unit. Each player has a CivilizationInfo. In here there is an array, where if mentioned, a UnitClass can become a different unit, even NO_UNIT.

The question for if a player can use a unit becomes: Look up UnitInfo for eUnit and get UnitClass, which we store in eUnitClass. Look up CivilizationInfo for the player and look up eUnitClass in the array. If it's there, we get eAllowedUnit. If it's not there look up UnitClassInfo for eUnitClass and read DefaultUnit and store it in eAllowedUnit. Now to answer the question, can the player use eUnit. The answer is eUnit == eAllowedUnit.

The CivEffect has a trick in this regard. During CivEffect cache init, run that calculation for each unit. If the unit isn't allowed to be used, set the cache to something negative. Now during runtime, whenever the code use the vanilla approach, all it has to do is call canUseUnit(eUnit) and get the cached answer. It will not only check if CivEffects turns the unit on or off, it will also check if it's turned on or off by a combo of 3 xml files and it's all just looking up an int in a normal array.

In short: the CivEffect cache is simple and very fast access to checking a lot of xml data, possibly from multiple files in one function call.

Another part of the idea is that the player can be granted CivEffects from multiple sources, be it traits, founding fathers, events or some other source. It doesn't really matter how many different ways they can be granted, the cached code will work the same. This mean by moving tags from traits to CivEffect, traits can still use them, but suddenly xml modding can make FFs provide something, which is currently only available to traits. The more tags, which are moved from one file into CivEffects, the greater the freedom becomes for xml modding. Needless to say all new tags should ideally be placed in CivEffects.

Is this explaining the idea better?

abmpicoli commented 6 years ago

@Nightinggale This issue have no milestone... I'm in doubt if this is 2.7.1 or 2.7.2 ... Would you kindly specify it?

Nightinggale commented 6 years ago

List of ways to gain CivEffects

@orlanth in #152

That sounds good. For the tags that can apply a civ effect to all players/Europeans/Natives/Kings, a good place for these could be HandicapInfos.xml, since this would allow modders to easily tweak/balance effects across difficulty levels for each of these categories.

If you wanted to be able to mod a "Wonder" type building with a civ-wide effect, it could be neat to have Buildings be able to apply one too.

I guess it could make sense to add CivEffects to HandicapInfos. However I'm not sure it makes sense to add all 4, particularly if we want to add a human and AI version of each. It seems a bit bloated. Just an AI would be enough.

Nightinggale commented 6 years ago

TODO list for the CivEffect branch

It's not certain all will be implemented or implemented like written here.

Engine

Ways to gain CivEffects

Items to allow/disable with CivEffects

Note: all are in xml and cached in CvPlayer

City modifiers

Growth modifiers:

Plot modifiers

Unit modifiers (applied to all units owned by player)

ShadesOT commented 6 years ago

Ways to gain CivEffects

Buildings? !

orlanth commented 6 years ago

Looks nifty! :) Here are some other thoughts on CivEffects features that could be cool to have for moddability:

Plot modifiers: Production modifiers for: Feature type, Bonus type, Terrain type, Domain type, Hills, Peaks, Rivers (+/- int for a Yield)

Bonuses: Would be neat to be able to separately unlock visibility of a Bonus type, and then unlock benefits from its Bonus yield modifiers (like how some Bonuses in Civ4 start out entirely hidden until you discover the appropriate tech)

Civics: When eventually enabling Civics, could be cool to reactivate some of the civic options from vanilla Civ4BtS (modifiers to unit upkeep costs and civic upkeep costs from number of cities and distance from capitol)

BuildInfos: Something I think we'd discussed in M:C is the overlapping nature of unlocking BuildInfos and unlocking Improvements/Routes. I.e. letting CivEffects directly allow/disable constructing individual Improvements/Routes makes sense; but in addition to this each BuildInfo entry can include a specific Improvement/Route linked to things like cost, time, whether or not a Feature type is cleared, the yield from clearing that Feature, etc. So for mods like 2071 with lots of Features, Techs/CivEffects that can unlock Feature clearance could get messy.

I think the best way around that might be for the 2071 mod to make separate BuildInfos for clearing each Feature and building each Improvement/Route. Then CivEffects could unlock BuildInfos for construction and feature clearances, without any change to the BuildInfos system being needed. (So when building a Farm on Fungal Forest, you'd need to first clear the Forest and build it, or have a Civeffect that allows the BuildInfo for building Farms on that Feature.) The AI would need to know to first clear the feature and then build the Improvement; I'm not sure whether that represents a problem for it or not. Do you think this is the best approach or have any other thoughts on this?

Censures: A neat system from M:C was Censures, which are basically CivEffects that can be applied by your King or Pope civ if they get to a certain diplomatic relationship value with you. I think it's probably possible to do this through the existing Event system, if an EventTrigger can check for your relationship with your King (or Pope) and trigger an Event that applies a Civeffect and adjusts the relationship.

Nightinggale commented 6 years ago

Plot modifiers: Production modifiers for: Feature type, Bonus type, Terrain type, Domain type, Hills, Peaks (+/- int for a Yield) I was thinking something like that as well. Civics: Oddly enough civics are part of the code and I already added support for CivEffects in it. Oddly enough it seems that the AI can make use of civics, yet human players just lack a button to do so. This however is pending investigation. BuildInfos: There is already support for enable/disable improvement and route infos. However when it came to terraforming, it got complex and I picked the quick solution of adding BuildInfo as well. WTP has quite flexible build infos. You can set which terrain/feature is required for the build to be valid and you can set the resulting:

Drain marsh requires marsh terrain and results in a different terrain (Savannah I think). This means there is a lot of flexibility in what can be done from xml. The only issue is that the build info array is looped quite a lot, meaning adding 100 new builds doesn't seem to be good for performance. I will ignore this performance issue for now, but it might require caching at some point. I imagine something like pre generated lists for each terrain type or something, meaning drain marsh will not even be looked at unless the unit is on a marsh. Something like this should be possible if builds turns out to be a performance issue.

We should make a subset of clear features. If there is a feature on the plot, canBuild should do:

This way it's not possible to build a farm until you have unlocked the feature removal.

Building something, which requires the feature to be removed shouldn't be one task, but two tasks in queue. The first one being feature removal. That will make the build handling code much cleaner.

Censures: I absolutely hate censures. The M:C implementation clashed big time with CivEffect and ended up not working at all. It will not be implemented, though it might be possible

LibSpit commented 6 years ago

What went wrong with the censures? Is it not possible to have an event apply a civeffect? (I could understand the current implementation causing issues, but could the same/similar outcome be achieved by a different method?)

I feel like they would be good especially in relation to civics, if your king dislikes a civic choice you have made you can relent or have some negative event occur. (This could also be expanded to events like tax hikes instead of just losing a resource.)

Nightinggale commented 6 years ago

Censures were hardcoded and making them compatible with CivEffects was more or less a rewrite from scratch. Currently events can't provide a CivEffect, but that's a planned feature. I just don't know how to do it yet and I would rather have some effect from owning CivEffects than just adding new ways of gaining CivEffects with no purpose.

The concept of censures isn't the issue, it was the implementation. It should be based on a completely different design if implemented again.