Skip to content

Commit 8103cdf

Browse files
XxFran10xXclaude
andcommitted
Tag crafted items with the model scheme they wear
ArmourShop needs to know which metal a crafted armour piece is, but every crafted piece shares one *_custom_* MMOItems template per weight, so the item id cannot tell steel from mythril. The craft record now also stores advancedcrafting:ac_craft_model_scheme: the scheme whose look the item got (iron, steel, bronze, ...). Alloys use their base ingredient's scheme, so an alloy piece carries its base metal. The scheme choice moved into ModelSchemeResolver, which the station and the craft record share. Join/chest refreshes re-apply the craft record, so refreshed items keep the tag and older crafted items gain it when they refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2c527dc commit 8103cdf

6 files changed

Lines changed: 165 additions & 18 deletions

File tree

‎src/main/java/net/tfminecraft/advancedcrafting/objects/crafting/CraftingStation.java‎

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import net.tfminecraft.advancedcrafting.utils.CraftTierLore;
5454
import net.tfminecraft.advancedcrafting.utils.MMOStatApplicator;
5555
import net.tfminecraft.advancedcrafting.utils.MajorityTierResolver;
56+
import net.tfminecraft.advancedcrafting.utils.ModelSchemeResolver;
5657
import net.tfminecraft.advancedcrafting.utils.ProfessionPermissions;
5758
import net.tfminecraft.advancedcrafting.lifecycle.CraftLifecycle;
5859

@@ -506,20 +507,7 @@ private StationFeedback createItem(Player p, Double forcedQualityPercent, boolea
506507
mmo.setData(ItemStats.LORE, lore);
507508
ItemStack finalItem = mmo.newBuilder().build();
508509
if(scheme != null) {
509-
// A recipe-specific model on the main material wins over the secondary model ingredient.
510-
if(scheme.getModel(recipe.getId()) == null && !recipe.getModelType().equalsIgnoreCase("none")) {
511-
for(String s : currentMaterials.keySet()) {
512-
String modeltype = s.split("\\.")[0];
513-
String modelId = s.split("\\.")[1];
514-
if(modeltype.equalsIgnoreCase("ingredient")) {
515-
Ingredient ing = IngredientLoader.getByString(modelId);
516-
if(ing.getIngredientData().getType().getId().equalsIgnoreCase(recipe.getModelType())) scheme = ing.getIngredientData().getModelScheme();
517-
} else if(modeltype.equalsIgnoreCase("alloy")) {
518-
Alloy a = AlloyManager.getAlloyById(modelId);
519-
if(a.getData().getType().getId().equalsIgnoreCase(recipe.getModelType())) scheme = a.getData().getModelScheme();
520-
}
521-
}
522-
}
510+
scheme = ModelSchemeResolver.forModel(recipe, scheme, currentMaterials);
523511
finalItem = applyModel(finalItem, scheme);
524512
}
525513
CraftProvenance provenance = CraftProvenance.from(recipe, currentMaterials, q);

‎src/main/java/net/tfminecraft/advancedcrafting/objects/data/CraftProvenance.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import net.tfminecraft.advancedcrafting.objects.crafting.CraftingRecipe;
1919
import net.tfminecraft.advancedcrafting.objects.crafting.Quality;
2020
import net.tfminecraft.advancedcrafting.objects.ingredients.Ingredient;
21+
import net.tfminecraft.advancedcrafting.objects.schemes.ModelScheme;
2122
import net.tfminecraft.advancedcrafting.objects.stats.StatTemplate;
23+
import net.tfminecraft.advancedcrafting.utils.ModelSchemeResolver;
2224
import net.tfminecraft.advancedcrafting.utils.PDCKeys;
2325

2426
public class CraftProvenance {
@@ -101,6 +103,11 @@ public void applyTo(ItemStack item) {
101103
meta.getPersistentDataContainer().set(PDCKeys.craftInputs(), PersistentDataType.STRING, GSON.toJson(inputs));
102104
meta.getPersistentDataContainer().set(PDCKeys.craftStatTemplateRevision(), PersistentDataType.INTEGER,
103105
statTemplateRevision);
106+
ModelScheme scheme = ModelSchemeResolver.resolve(RecipeLoader.getByString(recipeId), inputs);
107+
if (scheme != null) {
108+
meta.getPersistentDataContainer().set(PDCKeys.craftModelScheme(), PersistentDataType.STRING,
109+
scheme.getId());
110+
}
104111
item.setItemMeta(meta);
105112
}
106113

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package net.tfminecraft.advancedcrafting.utils;
2+
3+
import java.util.HashMap;
4+
import java.util.List;
5+
import java.util.Map;
6+
7+
import net.tfminecraft.advancedcrafting.loaders.IngredientLoader;
8+
import net.tfminecraft.advancedcrafting.managers.AlloyManager;
9+
import net.tfminecraft.advancedcrafting.objects.alloys.Alloy;
10+
import net.tfminecraft.advancedcrafting.objects.crafting.CraftingRecipe;
11+
import net.tfminecraft.advancedcrafting.objects.data.CraftInput;
12+
import net.tfminecraft.advancedcrafting.objects.ingredients.Ingredient;
13+
import net.tfminecraft.advancedcrafting.objects.schemes.ModelScheme;
14+
15+
/**
16+
* Picks the model scheme whose look a crafted item wears: the main material's scheme, unless that scheme
17+
* has no model for the recipe and a material of the recipe's model type brings its own. Alloys use their
18+
* base's scheme, so an alloy piece wears (and is tagged with) its base metal.
19+
*/
20+
public final class ModelSchemeResolver {
21+
private ModelSchemeResolver() {
22+
}
23+
24+
/** The scheme of a recorded craft, or null when the recipe or inputs are unknown. */
25+
public static ModelScheme resolve(CraftingRecipe recipe, List<CraftInput> inputs) {
26+
if (recipe == null || inputs == null || inputs.isEmpty()) {
27+
return null;
28+
}
29+
HashMap<String, Integer> materials = new HashMap<>();
30+
for (CraftInput input : inputs) {
31+
materials.put(input.getKind().toLowerCase() + "." + input.getId().toLowerCase(), input.getAmount());
32+
}
33+
ModelScheme main = schemeOf(MajorityTierResolver.resolveMajorityKey(recipe, materials), null, null);
34+
return forModel(recipe, main, materials);
35+
}
36+
37+
/** A recipe-specific model on the main material wins over the secondary model ingredient. */
38+
public static ModelScheme forModel(CraftingRecipe recipe, ModelScheme scheme, Map<String, Integer> materials) {
39+
if (scheme == null || scheme.getModel(recipe.getId()) != null
40+
|| recipe.getModelType().equalsIgnoreCase("none")) {
41+
return scheme;
42+
}
43+
for (String key : materials.keySet()) {
44+
scheme = schemeOf(key, recipe.getModelType(), scheme);
45+
}
46+
return scheme;
47+
}
48+
49+
/** The scheme of one material key if it exists and (when given) has the wanted type, else the fallback. */
50+
private static ModelScheme schemeOf(String key, String wantedType, ModelScheme fallback) {
51+
String[] split = key.split("\\.");
52+
if (split.length < 2) {
53+
return fallback;
54+
}
55+
if (split[0].equalsIgnoreCase("ingredient")) {
56+
Ingredient ing = IngredientLoader.getByString(split[1]);
57+
if (ing != null && (wantedType == null
58+
|| ing.getIngredientData().getType().getId().equalsIgnoreCase(wantedType))) {
59+
return ing.getIngredientData().getModelScheme();
60+
}
61+
} else if (split[0].equalsIgnoreCase("alloy")) {
62+
Alloy a = AlloyManager.getAlloyById(split[1]);
63+
if (a != null && (wantedType == null || a.getData().getType().getId().equalsIgnoreCase(wantedType))) {
64+
return a.getData().getModelScheme();
65+
}
66+
}
67+
return fallback;
68+
}
69+
}

‎src/main/java/net/tfminecraft/advancedcrafting/utils/PDCKeys.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ public static NamespacedKey craftStatTemplateRevision() {
2424
return new NamespacedKey(AdvancedCrafting.plugin, "ac_craft_stat_template_revision");
2525
}
2626

27+
/** Model scheme the crafted item wears (iron, steel, ...); alloys carry their base's. Read by ArmourShop. */
28+
public static NamespacedKey craftModelScheme() {
29+
return new NamespacedKey(AdvancedCrafting.plugin, "ac_craft_model_scheme");
30+
}
31+
2732
public static NamespacedKey scrapBase() {
2833
return new NamespacedKey(AdvancedCrafting.plugin, "ac_scrap_base");
2934
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package net.tfminecraft.advancedcrafting;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.util.*;
6+
import net.tfminecraft.advancedcrafting.loaders.*;
7+
import net.tfminecraft.advancedcrafting.managers.AlloyManager;
8+
import net.tfminecraft.advancedcrafting.objects.alloys.Alloy;
9+
import net.tfminecraft.advancedcrafting.objects.crafting.CraftingRecipe;
10+
import net.tfminecraft.advancedcrafting.objects.data.*;
11+
import net.tfminecraft.advancedcrafting.objects.ingredients.*;
12+
import net.tfminecraft.advancedcrafting.objects.schemes.ModelScheme;
13+
import net.tfminecraft.advancedcrafting.utils.ModelSchemeResolver;
14+
import org.junit.jupiter.api.Test;
15+
16+
class ModelSchemeResolverTest extends CoverageSupport {
17+
static CraftInput in(String kind, String id, int amount) {
18+
return new CraftInput(kind, id, amount, 0);
19+
}
20+
21+
static String scheme(CraftingRecipe recipe, CraftInput... inputs) {
22+
ModelScheme scheme = ModelSchemeResolver.resolve(recipe, List.of(inputs));
23+
return scheme == null ? null : scheme.getId();
24+
}
25+
26+
Alloy alloy(String id, Ingredient base) {
27+
var alloy = new Alloy(id, id, new AlloyData(base, new StatData(), new HashMap<>(), null));
28+
AlloyManager.addAlloy(alloy);
29+
return alloy;
30+
}
31+
32+
@Test
33+
void recordedCraftsResolveTheSchemeTheirLookCameFromAndAlloysUseTheirBase() throws Exception {
34+
TypeLoader.map.put("paper", new IngredientType("paper", yaml("name: Paper")));
35+
SchemeLoader.models.put("iron", new ModelScheme("iron", yaml("models: ['smith(v.iron_sword.1)']")));
36+
SchemeLoader.models.put("own", new ModelScheme("own", yaml("models: ['sword(v.iron_sword.2)']")));
37+
SchemeLoader.models.put("cloth", new ModelScheme("cloth", yaml("models: ['smith(v.golden_sword.3)']")));
38+
ingredient("iron", "model-scheme: iron");
39+
ingredient("own", "model-scheme: own");
40+
var cloth = ingredient("cloth", "type: paper\nmodel-scheme: cloth");
41+
alloy("darksteel", IngredientLoader.getByString("iron"));
42+
alloy("clothsteel", cloth);
43+
var noModel = recipe("recipe: ['metal.1', 'paper.1']\nmodel-type: none");
44+
var byPaper = recipe("recipe: ['metal.1', 'paper.1']\nmodel-type: paper");
45+
46+
assertNull(ModelSchemeResolver.resolve(null, List.of(in("ingredient", "iron", 1))));
47+
assertNull(ModelSchemeResolver.resolve(noModel, null));
48+
assertNull(ModelSchemeResolver.resolve(noModel, List.of()));
49+
50+
// The main material decides; an alloy wears its base metal's scheme.
51+
assertEquals("iron", scheme(noModel, in("ingredient", "iron", 3), in("ingredient", "cloth", 1)));
52+
assertEquals("iron", scheme(noModel, in("alloy", "darksteel", 3)));
53+
// A model-type material takes over when the main scheme has no model for the recipe ...
54+
assertEquals("cloth", scheme(byPaper, in("alloy", "darksteel", 3), in("ingredient", "cloth", 1)));
55+
assertEquals("cloth", scheme(byPaper, in("ingredient", "iron", 3), in("alloy", "clothsteel", 1)));
56+
// ... but not when it has one of its own.
57+
assertEquals("own", scheme(byPaper, in("ingredient", "own", 3), in("ingredient", "cloth", 1)));
58+
// Unknown, malformed and foreign inputs leave the main scheme alone.
59+
assertEquals(
60+
"iron",
61+
scheme(
62+
byPaper,
63+
in("ingredient", "iron", 3),
64+
in("alloy", "darksteel", 1),
65+
in("ingredient", "gone", 1),
66+
in("alloy", "gone", 1),
67+
in("ingredient", "", 1),
68+
in("misc", "x", 1)));
69+
assertNull(scheme(noModel, in("ingredient", "gone", 1)));
70+
assertNull(scheme(noModel, in("alloy", "gone", 1)));
71+
}
72+
}

‎src/test/java/net/tfminecraft/advancedcrafting/StationCoverageTest.java‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,11 @@ void mainMaterialRecipeModelWinsAndSecondaryIngredientRemainsFallback() throws E
404404
specific
405405
? Material.DIAMOND_SWORD
406406
: modelType.equals("none") ? Material.IRON_SWORD : Material.GOLDEN_SWORD;
407+
var craftRecipe = recipe("recipe: ['metal.1', 'paper.1']\nmodel-type: " + modelType);
408+
RecipeLoader.map.put(craftRecipe.getId(), craftRecipe);
407409
var station =
408410
new CraftingStation(
409-
loc(),
410-
recipe("recipe: ['metal.1', 'paper.1']\nmodel-type: " + modelType),
411-
new HashMap<>(Map.of(key, 1, secondary, 1)),
412-
new HashMap<>());
411+
loc(), craftRecipe, new HashMap<>(Map.of(key, 1, secondary, 1)), new HashMap<>());
413412
station.getCurrentMaterials().put("unknown.removed", 1);
414413
try (var nbt = mockStatic(NBTItem.class);
415414
var models = mockStatic(LegacyModelData.class);
@@ -425,6 +424,13 @@ void mainMaterialRecipeModelWinsAndSecondaryIngredientRemainsFallback() throws E
425424
var result = station.buildCompletedItem(p, 50.);
426425
assertEquals(expectedMaterial, result.getType());
427426
models.verify(() -> LegacyModelData.set(any(), eq(expectedModel)));
427+
// The tag names the scheme whose look the item got; alloys carry their base's.
428+
assertEquals(
429+
expectedModel == 3 ? "cloth-look" : "metal-look",
430+
result
431+
.getItemMeta()
432+
.getPersistentDataContainer()
433+
.get(PDCKeys.craftModelScheme(), PersistentDataType.STRING));
428434
}
429435
}
430436
}

0 commit comments

Comments
 (0)