Hiding older, deprecated items/buildings/things as you advance research.

Started by Cookiestomps, September 04, 2017, 11:53:30 AM

Previous topic - Next topic

Cookiestomps

The situation:
I'm working on a little mod that aims to add GMO crops. After researching f.i. "Genetic Engineering", I want to patch the vanilla crops to have slightly increased yields. But before this technology is researched, the old values should still be in effect.

What I've been able to do:
I've managed to clone the vanilla crops and change the values. After researching "Genetic Engineering", the cloned and improved set of crops becomes available. (This was quite simply done through changing and adding defs) The issue is that the old unimproved and deprecated crops still show up. So now you have two sets of identical crops cluttering up your UI.

How to continue?
I don't know. My Idea so far is to move the vanilla crops to a research node, let's call it "hidden helper", with itself as a prerequisite.  That would effectively hide the crops when "hidden helper" is not known to the colony. Then the problem would reduce to finding a way to unlearn "hidden helper" as soon as "genetic engineering" is researched. Any thoughts?

PS: I am focusing on just the crops here, but once we know how to do it here it's probably usable elsewhere.

kaptain_kavern

I think the old CCL framework had this function. Let's hope someone C# versed could do this, because indeed it will be useful for lots of us.

Good luck for your mod BTW

Cookiestomps

Okay, so I dug a little into the Tech advancing mod as it also checks what research you did. For someone versed in C# this might indeed be pretty doable, but I don't know as I am in over my head here.

In tech advancing, this line loops over all research Defs:
foreach (var researchProjectDef in DefDatabase<ResearchProjectDef>.AllDefs)

Then it checks if there's a certain tag that says the research is disabled:
if (researchProjectDef?.tags?.Contains("ta-ignore") == true)

Later it also checks if a certain bit of research was completed:
  if (researchProjectDef.IsFinished)

Now I think the answer is in using these. Perhaps it is possible to add a tag in the defs of your research, f.i. <tag><li>disable-nameofresearch</li></tag>. When the script reads that tag it should interpret that we want to disable nameofresearch, i.e. change its ResearchProjectDef.IsFinished state to false

I'm looking into it a bit more, but I have very little experience in the real programming languages.

Cookiestomps

Change of plan: I am now looking into what makes Rimworld decide whether a thing is available or not. Perhaps, I can add a rule that's the opposite of the <ResearchPrerequisite> def. Let's call it the <MadeObsoleteBy> def.

Digging deeper
I've done some looking with ILSpy and found an interesting bit of code in Verse.Command_SetPlantToGrow -> IsPlantAvailable(ThingDef plantDef):

// Verse.Command_SetPlantToGrow
private bool IsPlantAvailable(ThingDef plantDef)
{
List<ResearchProjectDef> sowResearchPrerequisites = plantDef.plant.sowResearchPrerequisites;
if (sowResearchPrerequisites == null)
{
return true;
}
for (int i = 0; i < sowResearchPrerequisites.Count; i++)
{
if (!sowResearchPrerequisites[i].IsFinished)
{
return false;
}
}
return true;
}


Here it checks whether all elements in the researchprerequisite list have been completed to determine if the plant should be sowable. So what I or some other volunteer has to figure out is how to modify that bit of code to confirm that some research hasn't been completed. I imagine it should add something like this (here ResearchMadeObsoleteBy is the def that lists the techs that make our plant obsolete):

List<ResearchProjectDef> SowResearchMadeObsoleteBy = plantDef.plant.sowResearchMadeObsoleteBy;
if (sowResearchMadeObsoleteBy == null)
{
return true; // if nothing makes the plant obsolete, keep it available
}
for (int i = 0; i < sowResearchMadeObsoleteBy.Count; i++)
{
if (sowResearchMadeObsoleteBy[i].IsFinished)
{
return false;
}
}


I've found similar pieces of code for buildings/items so if I can append the functionality here, I know the answer for the others too. I am going to keep trying to get it working, I figure it's a nice challenge for me.

Next up
Finding out how to mod the IsPlantAvailable function in Verse.Command_setPlantToGrow

CannibarRechter

What you are looking for is a Harmony Prefix style patch that overrides the method. Should be pretty straightforward bit of code for the patch part. The git repo is here: https://github.com/pardeike/Harmony. Many mods make use of Harmony, so it's just a case of downloading some of the trickier type mods, looking into their source directories, and learning from how they use Harmony.
CR All Mods and Tools Download Link
CR Total Texture Overhaul : Gives RimWorld a Natural Feel
CR Moddable: make RimWorld more moddable.
CR CompFX: display dynamic effects over RimWorld objects

Cookiestomps

Thanks, that seems to be exactly what I want to do. In essence I need to replace the private bool IsPlantAvailable(ThingDef plantDef) method by a modified method that includes the Obsolete check. I suppose the following method would work?


private bool IsPlantAvailable(ThingDef plantDef)
{
// Grab the Prereqs and ObsBys from the XML defs
List<ResearchProjectDef> sowResearchPrerequisites = plantDef.plant.sowResearchPrerequisites;
List<ResearchProjectDef> sowResearchMadeObsoleteBy = plantDef.plant.sowResearchMadeObsoleteBy;

// If there are no Prereqs or ObsBys, the thing should be available
if (sowResearchPrerequisites == null && sowResearchMadeObsoleteBy == null)
{
return true;
}

// At this point there are either prereqs or obsbys
// Therefore, if there are prereqs, check them.
if (sowResearchPrerequisites != null)
{
for (int i = 0; i < sowResearchPrerequisites.Count; i++)
{
if (!sowResearchPrerequisites[i].IsFinished) // Any uncomplete Prereq will result in unavailability
{
return false;
}
}
}

// Likewise, if there are obsbys, check those
if (sowResearchMadeObsoleteBy != null)
{
for (int i = 0; i < sowResearchMadeObsoleteBy.Count; i++)
{
if (sowResearchMadeObsoleteBy[i].IsFinished) // Any completed ObsBy will result in unavailability
{
return false;
}
}
}

return true;
}


Question: is the initial nullcheck with the and operator required? If both Prereqs and ObsBys are null then the code will skip the next null checks and go straight to the final return true;, right?

Either way, I'm now looking into harmony, but it is quite intimidating for a newbie like me.

Oh and I'll also have to edit Rimworld.PlantProperties to include a sowResearchMadeObsoleteBy list

CannibarRechter

> If both Prereqs and ObsBys are null then the code will skip the next null checks and go straight to the final return true;, right?

Well, not exactly. If BOTH are null, it will bail out early. If either one is not null, you're going to have to check to see which one is null, so you need the checks.
CR All Mods and Tools Download Link
CR Total Texture Overhaul : Gives RimWorld a Natural Feel
CR Moddable: make RimWorld more moddable.
CR CompFX: display dynamic effects over RimWorld objects

Cookiestomps

Ah so technically it's not needed, but it allows Rimworld to skip having to do the other checks if nothing's there. Is that useful? I guess it could speed up reading items that have neither prereqs nor obsbys.

Cookiestomps

I am trying to make sense of Harmony but it is really confusing me and the tutorials are slightly too abstract for me to be honest. So far I've determined that I need to

A) Transpile public List<ResearchProjectDef> sowResearchMadeObsoleteBy; into Rimworld.PlantProperties
B) Prefix private bool IsPlantAvailable(ThingDef plantDef) with my code and possibly skip it?

Does anyone know of any mods which have straightforward harmony implementations?

Cookiestomps

Some further progress:

To my -limited- knowledge it is not possible to use a prefix/postfix to patch the IsPlantAvailable method. This is because it is used in a conditional statement in Command_SetPlantToGrow.ProcesInput(). Prefix can't return true or false for this purpose; the return bool of prefix is used for determining whether to run the original or not. Postfix are void and don't return anything. Therefore I will need to transpile and replace the if statement that calls IsPlantAvailable.

What I will try next is to implement a very simple patch that replaces the statement by if(true). If all goes well I should be able to see the effect ingame quite easily, since devilstrand will no longer be hidden without the respective researchnode completed. From there on I can attempt the harder task of replacing the code to do what I want.

I'll continue to try solve this and log my efforts. I'm still hoping a helpful soul who is well versed in C# and harmony shows up, because that would speed up progress tremendously.

Mehni

QuoteA) Transpile public List<ResearchProjectDef> sowResearchMadeObsoleteBy; into Rimworld.PlantProperties

There's no need to transpile: you can use ModDefExtension to add your own XML tag to the Defs. Transpiling is complicated stuff and in most cases not necessary.

QuotePrefix can't return true or false for this purpose; the return bool of prefix is used for determining whether to run the original or not.
The last part is true, but a Prefix in Harmony can still have a __result which can set a value.

QuotePostfix are void and don't return anything.
Like Prefixes, just because they don't return anything doesn't mean you can't set anything.

The order in which something can get executed is Prefix --> Original Method --> Postfix. Prefix allows you to do something before a Method gets called, and then you either decide to execute your modifications + the original Method, or just your modification if you return false. A Postfix works after the function.

Note that returning false on a prefix is a detour and does not play nice with other mods that also want to edit those parts of the code. Avoid it if possible.

Cookiestomps

Quote from: Mehni on September 07, 2017, 05:26:44 PMThere's no need to transpile: you can use ModDefExtension to add your own XML tag to the Defs.
That'll definitely help speed up things. Thank you very much, it's great to know an easier way exists.

Quotea Prefix in Harmony can still have a __result which can set a value. ... Like Prefixes, just because [postfixes] don't return anything doesn't mean you can't set anything.
I believe harmony had a tutorial on patching chickens to spawn 100 eggs instead of 1. In a postfix they set the variable containing the egg amount to 100. This is what you mean right? 

The problem I have is that the method I'm trying to adjust is called in an if statement. I understand how to pass variables onwards from the voids, but I don't see how to fix the if statement that way. This is the relevant bit that I'm talking about:

public override void ProcessInput(Event ev)
{
... some preceding code
if (this.IsPlantAvailable(current))
{
                 ... Code that makes plant show up in menu comes here
                 }
}


The code wants a return true or return false from IsPlantAvailable, and I'm thinking that's not really possible with postfix/prefix unless I'm missing something. As a result I think the way to go here is to transpile into ProcessInput and modify the if statement to refer to a modified IsPlantAvailable written in my mod's code.

These seem to be the relevant lines in CIL:

IL_0076: ldarg.0
IL_0077: ldloc.1
IL_0078: call instance bool Verse.Command_SetPlantToGrow::IsPlantAvailable(class Verse.ThingDef)
IL_007d: brtrue IL_0087

Is it possible (and preferably not a crime against programming etiquette) to redirect IL_0078 to call my own modified method?

Edit: By setting these specific opcodes to Nop, I've managed to remove the prerequisite research check for devilstrand. I am so excited, I did not expect myself to be able to do this so quickly! Next up: redirecting the code somehow and finding a proper anchor for this patch!

Edit2: Can anyone point me in the right direction for how to change the method call to IsPlantAvailable to call my own instead?