Nomi-CEu / Nomi-Labs

The coremod for Nomi-CEu, providing custom GT changes, items, recipe changes, and more!
https://www.curseforge.com/minecraft/mc-mods/nomi-labs
GNU Lesser General Public License v3.0
10 stars 15 forks source link

[Enhancement] Proposed solution to how to increment DME data model NBT in the Simulation Supercomputer + addtional problems and their (proposed) solutions #19

Closed AncientShotgun closed 5 months ago

AncientShotgun commented 7 months ago

As I made my way through your team's codebase for this mod, I noticed that you haven't yet implemented a way to increase DME data models' kill/simulation counts in the Simulation Supercomputer yet. I propose the following solution to this issue:

tl;dr: Custom MultiblockRecipeLogic class, look through the input buses for the data model then use DME's implementation to increment the model's NBT data. I also found and listed additional problems and possible solutions.

Long and thorough explanation:

The MultiblockRecipeLogic class in Gregtech CEu exposes a protected method by the name of getInputBuses(). This is for distinct-bus multiblocks; the Simulation Supercomputer is one of these. This method returns a List, a list which contains the inventories of all the input buses on the multiblock. Iterate through these inventories until you have located the ItemStack representing the DME data model.

The MultiblockRecipeLogic class also exposes the protected void method completeRecipe(), which (as you can probably guess) handles the logic that is to be done at the end of each completed recipe (actuates Muffler output, places products in the output buses, resets the processing time, etc.)

But wait, there's more. Let's examine DME's implementation of this process, to see if we can implement the same or similar here.

The DataModelHelper class in DME is a helper class wrapping a number of methods and functions useful for manipulating DME data model items and their NBT metadata. It contains the public static void method addSimulation(Itemstack dataModel), which adds 1 to the passed data model's data count (tracks progress to the next data model tier), adds 1 to the passed data model's sim count (tracks nothing relevant for us) and also automatically tiers up the model if it has a data count exceeding the amount necessary to achieve the next tier. The only thing it does not do is first verify if the ItemStack it recieves as a parameter is, indeed, a DME data model. Note that it directly modifies the data model's ItemStack's NBT data, and does not remove or edit anything else about the data model ItemStack.

My solution is as such:

Write a custom RecipeLogic class (DMESimChamberRecipeLogic would be a good, although long, name) extending MultiblockRecipeLogic. Override completeRecipe() and inside of it, comb through the Simulation Supercomputer's input buses' inventories from getInputBuses() using a finder algorithm of your choice to find the data model's ItemStack and verify that it is actually a data model. After verification succeeds, use the previously-imported DataModelHelper.addSimulation(Itemstack dataModel) to increase the data model's counts.

Additional possible problems and my proposed solutions:

P: The DME Simulation Chamber singleblock machine automatically cancels a running simulation if the player removes the data model from its slot. If we want/need to, how do we replicate this behaviour in the multiblock? A: The MultiblockRecipeLogic class exposes the canProgressRecipe() method, a protected method returning a boolean that confirms if the recipe can continue being processed. Override this method in DMESimChamberRecipeLogic and insert an additional check to see if the data model is still in one of the input buses, once again using getInputBuses(). If it is not (means it has either been consumed as recipe input, which is impossible according to the registered recipes for the Simulation Supercomputer, or was removed from the bus by the player), then return false. This will make the recipe reverse if the data model is ever removed while the recipe is in progress, until the data model is placed back in the bus. Please note that the canProgressRecipe() method runs once every recipe progress update, so the finder algorithm must be performant (cache the bus the model is in to ease repeated reference, perhaps?)

P: The DME Simulation Chamber singleblock machine only has space for one data model to be simulated at a time. If we want/need to, how do we replicate this behaviour in the multiblock? A: The MultiblockRecipeLogic class exposes the canWorkWithInputs() method, a protected method that returns a boolean that confirms if a multiblock can work with the items in its input buses. (Notably, this method returning true is one of the prerequisites to begin recipe lookup to start processing.) Override this method in DMESimChamberRecipeLogic and count up the number of data models in the input buses using getInputBuses(). If there's more than one, return false. This will prevent recipe lookup from happening, and the multiblock will not start processing at all. Could also implement similar in canProgressRecipe() to prevent recipe from progressing while more than one datamodel is in the input buses, if a player happens to insert a second data model while the first is being simulated (caching data model input bus may also be of use here).

If you are going to implement either of these additional fixes, I would recommend writing (an) appropriate warning message(s) to the display in the multiblock's right-click interface with addWarningText(List<ITextComponent> textList) for player QoL.

IntegerLimit commented 7 months ago

There is one change I would make from this:

Instead of modifying the DME Models at the end of a recipe, modify it at the beginning. This solves the first problem. Hopefully, there is some method that we can use to modify the recipe ingredients.

This would remove the need for expensive checks in canProgressRecipe, which I assume runs every tick, or whenever inputs are changed.

[Edit]: Probably Every tick, it probably handles energy check as well.

AncientShotgun commented 7 months ago

If you want to model the Simulation Supercomputer's processing behavior exactly on how the DME Simulation Chamber singleblock does it, the data model's NBT increment has to be done at the end of the process. I would make a mockup RecipeLogic class for you, but I have a uni exam in the near future and can't spare any time to code it. The expensive checks in canProgressRecipe() only need to be made if you or your team want to require the player to keep the data model in the mutiblock's input buses for the entire time the recipe is processing, and not for actual incrementation of the data model's relevelant NBT data. The incrementation/tier increase only relies on the DataModelHelper class from DME. As far as I can tell, using the DataModelHelper class and its methods is the native way DME increments and tiers up data models, in- and outside of its own machines, so it should theoretically be race condition-proof, thread-safe and deterministic, so I can't really see any reason why we should use a clunky, hacky and likely non-deterministic custom solution when DME's is sitting right there.

(Please don't quote me on this, I'm not an expert on DME's API!)

Regarding interacting with multiblock inputs: As far as I can tell, MultiblockRecipeLogic.getInputBuses() is the only way to interact with recipe inputs during operation of the multiblock. There is also MultiblockRecipeLogic.getInputinventory(), but this function is ignored or otherwise deprioritized by distinct-bus multiblocks in favor of the MultiblockRecipeLogic.getInputBuses() method.

If we want to register recipes for the multiblock that take the data model's tier into account when calculating the chance for pristine matter: I believe the only way to make that happen in a way that wouldn't require rewriting GTCEu RecipeBuilders from scratch is to iterate through the list of extant data models four times, once for each tier of data model, registering a separate recipe for each combination of mob type and model tier. That means four times the NomiCEu 1.6.1a amount of recipes for this multiblock, which is a bit icky, but at least we have automation tools to make it easier.

Oh yeah, the corrected formula for the percent chance of pristine matter based upon model tier is as follows: percentChance = Math.min(30, 5*Math.pow(2, tierNumber)); This formula outputs the following values as whole numbers: 5 for tierNumber = 0, 10 for tier number = 1, 20 for tierNumber = 2, and 30 for tierNumber = 3. These 4 values that tierNumber can take correspond each to a data model tier: 0 for Basic, 1 for Advanced, 2 for Superior and 3 for Self-Aware. If we don't clamp the percentage with Math.min(), a data model of tier Self-Aware would instead be associated with a pristine chance of 40% in the multiblock (this might be a good benefit/bonus you and your team can implement, then present to players to incentivise them building the Simulation Supercomputer over a bunch of DME Simulation Chamber singleblocks). I do have time to code a mockup in Groovy of how the automated recipe registry would work, if you would give me a couple of days to do so.

Incidentally, does your team have a multiblock version of the DME Loot Fabricator defined? I saw a controller that might be that while puttering around this codebase. If you don't, would you like one? I also think I saw that GCYM doesn't add a multiblock version of the Gas Collector; would you like one of those too? They will take me some time to complete, so I can only do them after my uni exam, but it is possible.

IntegerLimit commented 7 months ago

I understand that the singleblock iterates the level at the end, but after thought, doing it at the start seems much more appropriate.

IntegerLimit commented 7 months ago

As for the other multiblocks, they are not needed. Thank you for your consideration.

AncientShotgun commented 7 months ago

Alright, I'll leave those multiblocks for my own instance of Nomi-Ceu then.

Ok, so if you want to increment your model's sim and data counts at the start of the process instead of at the end, override MultiblockRecipeLogic.prepareRecipeDistinct(Recipe recipe) in your custom RecipeLogic class. Call super.prepareRecipeDistinct(recipe) as usual, but search the local IItemHandlerModifiable variable called currentDistinctInputBus (distinct-bus multiblocks treat each of their input buses as if they are each holding a separate set of ingredients for their own separate recipe, and that is what is being tracked here) for the data model and modify it with DME's method I keep mentioning, either before or after the super call depending on whether you want to increment the model before reading it into the recipe or afterwards. If you have one data model per input bus, this will iterate through all of them and only update the model in that bus once the recipe associated with it begins to be processed. I don't really have the time to check, but I believe this action is deterministic/safe enough. Might cause weird recipe lookup behaviour, not sure though. Fingers crossed!

IntegerLimit commented 7 months ago

I was thinking on running on setupAndConsumeRecipeInputs instead, by first calling super, checking if the return value was true, then going from there. They are both called similarly though.

This would work better, working even if we for some reason reverted DME Simulation Supercomputer to use Non-Distinct.

AncientShotgun commented 7 months ago

Excellent idea.

Edit: I just realized that incrementing the data model's sim and data counts at the start of the process instead of at the end could be exploited by players to farm data model progress very quickly at the cost of pulsating polymer clay and forsaking pristine or living matter; by simply alternating putting the model in the input bus's inventory, then taking it out again and waiting for the recipe to fail completely, the recipe is started and the data model's NBT is incremented over and over again. Is this exploit desired/does it matter if this exploit is used by players?

AncientShotgun commented 7 months ago

Since I don't have much else to say on this matter I will close this issue thread with this comment. The archived file included in this comment is a recipe populator script written in Groovy, to automate populating the Sim Chamber multiblock's RecipeMap with simulation recipes. I hope it will help you and your team further. multiblock_sim_chamber_recipemap_populator.zip

IntegerLimit commented 6 months ago

I will leave this as open until we implement this. Your assistance was very helpful.

IntegerLimit commented 6 months ago

Excellent idea.

Edit: I just realized that incrementing the data model's sim and data counts at the start of the process instead of at the end could be exploited by players to farm data model progress very quickly at the cost of pulsating polymer clay and forsaking pristine or living matter; by simply alternating putting the model in the input bus's inventory, then taking it out again and waiting for the recipe to fail completely, the recipe is started and the data model's NBT is incremented over and over again. Is this exploit desired/does it matter if this exploit is used by players?

They would have to break the controller, but it is a possible idea. However, it should be fine.

Will consider this.

IntegerLimit commented 5 months ago

Implemented in https://github.com/Nomi-CEu/Nomi-Labs/commit/bc138df9e1c7d70f05136b9e4fdb58a4f4ead977.

IntegerLimit commented 5 months ago

If your interested, the implemented groovy script for recipes can be seen here.