Skip to content

Comments

Add Dynamic ToolStatType to datapack#5573

Open
Qikahome wants to merge 13 commits intoSlimeKnights:1.20.1from
Qikahome:1.20.1
Open

Add Dynamic ToolStatType to datapack#5573
Qikahome wants to merge 13 commits intoSlimeKnights:1.20.1from
Qikahome:1.20.1

Conversation

@Qikahome
Copy link
Contributor

@Qikahome Qikahome commented Jan 6, 2026

It seems work well;
TEST THINGPACK:
testpack.zip

@Qikahome
Copy link
Contributor Author

Qikahome commented Jan 6, 2026

I know nothing about the test, so it cant build now, but it can run client

Copy link
Member

@KnightMiner KnightMiner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the start of something that could work, but it does seem you made some design decisions that will make this very hard to maintain. I left some comments proposing a cleaner design. Feel free to ping me on discord and we can discuss it more in depth.

/**
* Clears all dynamic stat types..
*/
void clearDynamicStatTypes();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be publicly exposed. We only need to clear then before stats load.

* Fallback is not supported for dynamic stat types.
* @param type Stat type
*/
void registerDynamicStatType(FlexMaterialStatType type);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seem necessary. Its loaded in JSON, why must it be registered?

If it has to be registered, a JSON only addon can't use it.

Comment on lines 106 to 109
materialStatTypesLoader = new MaterialStatTypesLoader(() -> {
statsLoaded = true;
checkAllLoaded();
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? Just put the loading at the start of the material stats loader. You may need to override a method for that. Also ensures stat types are guaranteed to load before we need them instead of being non-deterministic.

Also, setting the same boolean as another loader is just wrong. But since this is the wrong approach don't worry about that part of the comment.

@@ -0,0 +1,5 @@
package slimeknights.tconstruct.library.materials.stats;

public record IMaterialRegister() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there an empty record named like an interface here? This seems like an accidental commit.

New classes and public methods should have javadocs explaining how they are used.

* Another registry, cleared on reload.
*/
@Getter
private IdAwareComponentRegistry<FlexMaterialStatType> dynamicStatTypes = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably does not need to be publicly exposed as the other is only exposed so you can register to it.

Part of me thinks it would be better to make this a Map instead of the registry as I am not sure its saving much using that class. Feel free to justify though where its making the code easier.

public class FlexMaterialStatType extends MaterialStatType<FlexMaterialStat> {

public FlexMaterialStatType(MaterialStatsId id, boolean canRepair,
LinkedHashMap<String, Supplier<Stat<?, ?>>> stats) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should take a List<DynamicStatField> in the constructor.

Also, canRepair should be computed from that list, not passed in separately.


public FlexMaterialStatType(MaterialStatsId id, boolean canRepair,
LinkedHashMap<String, Supplier<Stat<?, ?>>> stats) {
super(id, (FlexMaterialStat) null, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't like passing in null to super here, we should be able to create the default from the List<DynamicStatField>, by giving each field a default value.

@Override
public void registerDynamicStatType(FlexMaterialStatType type) {
throw new UnsupportedOperationException("No registration possible in test mock");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These need not even exist in our tests. Entirely internal to the stats manager


import slimeknights.tconstruct.library.tools.stat.ToolStatId;

class ToolStatTypeNotMatchException extends RuntimeException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, why the custom exception? Looks to me like another json parse exception.

);

UpdateMaterialStatsPacket packetToDecode = sendAndReceivePacket(materialToStats);
Collection<FlexMaterialStatType> dynamic = new HashSet<>();// I dont know how to test so just an empty set
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way you test is you write JSON for custom stat types, and then use them to load stats with those types.

From the packet, make sure:

  • The stat type you send in comes back out the same
  • stats using the stat type you send in come back out the same

But it also needs tests for JSON parsing:

  • make sure you can parse a stat type as expected.
  • make sure stats using the custom stat type parse as expected.

@KnightMiner KnightMiner added Feature For pull requests adding in new content, and occasionally larger enhancements Technical Pull requests making changes to workspace or targeted versions 1.20 Issue affects 1.20 labels Jan 9, 2026
@Qikahome Qikahome requested a review from KnightMiner January 9, 2026 14:20
@Qikahome
Copy link
Contributor Author

Qikahome commented Jan 9, 2026

I wrote the code for the test part, but it seems that ToolStats wasn't registered in the test, which caused my code to throw a JsonParseException.

@Qikahome
Copy link
Contributor Author

Qikahome commented Jan 9, 2026

newtestpack.zip

@Qikahome
Copy link
Contributor Author

Qikahome commented Jan 9, 2026

Tests are completed

public static final List<DynamicStatField<?>> statFields = List.of(
new FloatDynamicStatField("test1",ToolStats.DURABILITY,1f,FloatDynamicStatField.Operation.PERCENT, "desc1", "info1"),
new FloatDynamicStatField("test2",ToolStats.MINING_SPEED,2f,FloatDynamicStatField.Operation.UPDATE, "desc2", "info2"),
new TierDynamicStatField("test3",ToolStats.HARVEST_TIER,Tiers.STONE,"desc3")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting, we do use these stats in other tests. You might just need to ensure static init is run before the deserialize method runs.

But yeah, fake stats are fine for the tests. I sometimes use "test" as a namespace for them to distinguish them.

@Qikahome
Copy link
Contributor Author

May I ask how this PR is going?

@KnightMiner
Copy link
Member

This is not the only thing I have to work on nor the only PR I have to review right now. You don't have to keep commenting on it requesting an update.

Copy link
Member

@KnightMiner KnightMiner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main comment on this is you seem to be reinventing the wheel in how DynamicStatField works. You can simplify that a lot if you take advantage of the Loadable API we already use elsewhere.

Feel free to talk to me on #coding on our discord if you have questions about how to do that.

*/
@Nullable
public DynamicStat getStat(String name)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but be consistent on formatting. In this case, brackets go on same line, not newline.

* @return Stat, or null if not found
*/
@Nullable
public DynamicStat getStat(String name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what this method does from the javadoc, but not why it is needed. Does any other class use it or is it just used internally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In network transmission or serialization, we need specific property values, but it's not feasible to create a getter for every DynamicStat.
So I get the value from the name.


@Override
public List<Component> getLocalizedInfo() {
return stats.values().stream().map(stat->stat.getLocalizedInfo()).collect(Collectors.toList());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth caching these two lists as part of the constructor since they are not changing.

Also, you can use DynamicStat::getLocalizedInfo for the map call, IDEA has a warning in place if you don't do that so I tend to do it.


@Override
public void apply(@Nonnull ModifierStatsBuilder builder, float scale) {
stats.values().forEach(stat -> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to above, IDEA is configured to warn of single line lambda statements; can make this compact as stat -> stat.apply(builder, scale)

* A record class that represents a dynamic material stat.
*/
public record DynamicMaterialStatRecord(MaterialStatType<?> type, List<DynamicStatField<?>> statFields)
implements RecordLoadable<DynamicMaterialStat> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not a line length limit for this repository; keep implements on the same like as the class declaration unless its seriously long (in which case I tend to prefer splitting the record constructor instead).

private boolean canRepair = false;
@Getter
@Setter
@Accessors(fluent = true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Builders should chain, use chain=true.

* @return The built material stat type.
*/
public DynamicMaterialStatType build() {
return new DynamicMaterialStatType(new MaterialStatsId(registryName), this.canRepair, fields);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well make the field be MaterialStatsId instead of waiting to do so. Add name overload for ResourceLocation if you wish.

* stat types is "data/foobar/materials/stat_types".
*/
@Log4j2
public class MaterialStatTypesLoader extends MergingJsonDataLoader<MaterialStatTypeBuilder> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loaders should not be merging, we do not need multiple sources of what a stat type looks like.

Take a look at MaterialManager for reference.

onLoaded.run();
}

public void setDynamicStatTypes(Map<MaterialStatsId, DynamicMaterialStatType> dynamicStatTypes) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a javadoc. I assume this is set from the packet? If so, an @Internal is a good idea too.

/** Map of material stat types, keyed by material ID */
@Getter
@Setter
private Map<MaterialStatsId, DynamicMaterialStatType> statTypes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This being nullable is just leading to a bunch of redundant checks. Just make it default to Map.of() (empty map).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.20 Issue affects 1.20 Feature For pull requests adding in new content, and occasionally larger enhancements Technical Pull requests making changes to workspace or targeted versions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants