-
-
Notifications
You must be signed in to change notification settings - Fork 506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Loonium with data-driven structure loot #4639
base: 1.20.x
Are you sure you want to change the base?
Loonium with data-driven structure loot #4639
Conversation
Supersedes #4313, but is only loosely based on that PR. |
8813fdc
to
272d340
Compare
0819447
to
2fed3ca
Compare
7344086
to
a088bac
Compare
a088bac
to
ca9e6a4
Compare
Loonium figures out which structures(s) it was placed in and uses a corresponding list of loot tables with the id `botania:loonium/<structure_namespace>/<structure_path>`. If the flower is placed in overlapping pieces of multiple structures with configured loot tables, it randomly chooses one of the tables whenever it spawns a mob. The Loonium drop capability is now attached to all mob entities, in preparation of customizable mob pools. Default equipment slot drops are turned off. TODO: Define loot tables for relevant vanilla structures. TODO: Define structure configuration data (JSON, likely separate registry) for the spawnable mob pool and whether the flower must be placed within a structure piece or anywhere within the structure's overall bounding box, maybe even the amount of mana per spawned mob. TODO: Move default configuration to a default structure configuration entry.
(can't be detected reliably)
- tweak ancient table selection city weights - fix reference to elder guardian drops loot table - remove type specification on Loonium loot tables, as it caused validation warnings
(includes a generic reloadable config data manager, which currently only covers Loonium configurations)
(covers e.g. the skeleton from a spider jockey or the bonus chicken for some baby zombies)
(such as skeletons from a spider jockey or chickens from a zombie jockey, provided they are in the spawned mobs list)
- move inner config classes to own files - add builders where appropriate - simplify optional field definitions in codecs where possible, and remove constructors with Optional arguments
Equipment tables are not vanilla yet, so they currently use the "selector" loot table type and are applied manually by the Loonium. Nested loot table definitions also currently need to be loot table references, since embedded tables as pool entries are not supported by vanilla yet either.
- mobs are spawned on a unique team, which means even Loonium-spawned zoglins don't attack other Loonium-spawned mobs - new challenge advancement "King" requires killing each of the currently 22 different mobs the Loonium spawns in different structures
ca9e6a4
to
1c516fd
Compare
Writing this comment as i'm going through the review:
Why not Actually, what if we made the loot table specifiable in the config? Then, if you're just referencing something, you can point to it directly without creating a new loot table file, and if you want to make something custom, you can reference any custom loot table. Then the config becomes the only "magic" resource location, instead of having two. Thoughts?
What's different? Will/should our implementation change to match more closely with vanilla in 1.21? Or will we be able to reuse vanilla code?
Realizing on a second read that this means any individual piece of a structure, right? Vs the cuboid bounding box that encompasses the whole structure. But the way i first interpreted this made me think: Is it possible to detect which structure piece you're in? I guess it would depend on the structure type, i don't know too much about structures, but there's at least jigsaw placement with pieces. Are there other, non-jigsaw ways to make a structure? Anyway, different configs for individual structure pieces would be cool to have, though maybe that could be an additional change later. |
Also, archaeology loot tables need to be handled carefully, due to their limited loot pool options. | ||
*/ | ||
|
||
tables.put(getLootId(BuiltinStructures.ANCIENT_CITY), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could use a helper function, probably. Nicer to author and edit. Maybe an interface like:
put(tables, BuiltinStructures.ANCIENT_CITY,
LootTableReference.lootTableReference(BuiltInLootTables.ANCIENT_CITY).setWeight(4),
LootTableReference.lootTableReference(BuiltInLootTables.ANCIENT_CITY_ICE_BOX).setWeight(1)
)
I could do this myself, sounds like a fun refactor :3
) | ||
); | ||
|
||
var output = new ArrayList<CompletableFuture<?>>(tables.size()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's cleanest if this is it's own function. Or maybe this stays in the run
function, and a separate function is made that handles creating and populating tables
. Again, i can handle this
|
||
@NotNull | ||
private static LootTable.Builder buildOceanRuinLootTable(ResourceLocation archaeology) { | ||
// Note: since the Loonium does not supply a location, treasure maps will roll as empty maps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we make it supply a location? would be cool
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could supply the Loonium's (effective) location, or the mob's death location. However, generating a treasure map can come with a significant performance impact, and the usefulness of the generated would probably depend on how much you can manipulate the location.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the mob's death location sounds fun, then you could bring them with you places to roll treasure maps for that area
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds interesting. I'm just a little worried about the performance impact of frequently generating maps, because locating structures can take several seconds. Also, the loot table rolling needs to be moved from mob spawn to mob death.
@@ -179,7 +179,7 @@ public CompletableFuture<?> run(@NotNull CachedOutput cache) { | |||
for (var e : tables.entrySet()) { | |||
Path path = pathProvider.json(e.getKey()); | |||
LootTable.Builder builder = e.getValue(); | |||
LootTable lootTable = builder.setParamSet(LootContextParamSets.EMPTY).build(); | |||
LootTable lootTable = builder.setParamSet(LootContextParamSets.ALL_PARAMS).build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this ideally be changed to the context parameters we actually supply/use?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC there's validation happening when the loot table is written, loaded, or evaluated. And while you don't need to set any parameters, having them not defined in the first place can lead to warnings or errors if the type of the referenced loot requires them.
The Loonium loot is generated in an empty context (it could probably be a different context that provides the flower location and mob reference somehow, but that could influence the type of items you get, e.g. due to cooking food drops if the mob was on fire, or whether it was a player kill with or without looting), but there are references to e.g. entity, chest, or archaeology type loot tables, which may have different parameter sets.
Long story short: This commit changed it from the actually supplied set (empty) to the widest available set (all params) because something didn't work as expected.
@@ -262,7 +266,7 @@ public void readFromPacketNBT(CompoundTag cmp) { | |||
super.readFromPacketNBT(cmp); | |||
if (cmp.contains(TAG_LOOT_TABLE)) { | |||
var lootTableString = cmp.getString(TAG_LOOT_TABLE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a String right? should just say String
instead of var
attuneType = "multiple_structure_drops"; | ||
} | ||
String attuned = I18n.get("botaniamisc.loonium." + attuneType).formatted(flower.lootTables.length); | ||
int filterWidth = mc.font.width(attuned); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filter? is this copied from hopperhock?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is, oops.
@@ -132,6 +132,9 @@ | |||
"botaniamisc.rannuncarpus.state_sensitive": "Match Exact State", | |||
"botaniamisc.rannuncarpus.state_insensitive": "Match Block Only", | |||
"botaniamisc.lokiRingLimitReached": "Selection limit reached", | |||
"botaniamisc.loonium.generic_drops": "Not attuned", | |||
"botaniamisc.loonium.structure_drops": "Attuned to structure", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to say which structure it's attuned to if it's only one? The name could be specified in the config maybe. Another reason to require config i guess. It would result in twice the files together with my other idea (config references a loot table, and we have loot table functions that limit to one drop), but maybe not a huge issue? It feels cleaner and the most customizable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That part changed in a later commit to "Generic loot", "Structure loot", or "Custom loot" (if the Loonium BE NBT is edited to have a custom loot table assigned), but either way, structures do not have display names. The only thing we could show is the structure resource location.
Codec.list(MobEffectToApply.CODEC).fieldOf("effectsToApply").forGetter(o -> o.effectsToApply) | ||
).apply(instance, LooniumStructureConfiguration::new) | ||
); | ||
public static final Codec<LooniumStructureConfiguration> OPTIONAL_CODEC = RecordCodecBuilder.create( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think i'd call this one WITH_PARENT_CODEC
or something, to more clearly get across the difference
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was the first shot at implementing the codec logic. Turns out I didn't need the second one.
public class LooniumStructureConfiguration { | ||
public static final Codec<LooniumStructureConfiguration> CODEC = RecordCodecBuilder.create( | ||
instance -> instance.group( | ||
ExtraCodecs.POSITIVE_INT.fieldOf("manaCost").forGetter(o -> o.manaCost), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think i'd prefer this to be NON_NEGATIVE_INT, for more customizability
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should work without further modifications, as far as I can tell.
ExtraCodecs.validate(Codec.INT, | ||
duration -> duration > 0 | ||
? DataResult.success(duration) | ||
: DataResult.error(() -> "Invalid effect duration")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not ExtraCodecs.POSITIVE_INT
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't remember if that works with the -1 (infinite) default value. Maybe it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current one also doesn't work with -1, does it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fire resistance is an infinite effect for most mobs, so it should be working in the current state.
return true; | ||
} | ||
var parentConfig = map.get(parent); | ||
return parentConfig != null && findTopmostParent(map, parent, parentConfig.parent, visitedEntries); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what i can see, specifying a missing ID as the parent, will lead to the Ignoring Loonium structure configuration(s) without top-most parent
error message. Or is that caught earlier? If not, maybe we should have an error specifically for that also
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The config loading logic should be validating that any specified parent ID actually exists.
@@ -33,6 +33,7 @@ | |||
import vazkii.botania.api.corporea.CorporeaNodeDetector; | |||
import vazkii.botania.api.internal.DummyManaNetwork; | |||
import vazkii.botania.api.internal.ManaNetwork; | |||
import vazkii.botania.common.config.ConfigDataManager; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(forgive me if this changes in a later commit)
I don't think you can import a non-api class in the api. Someone depending on just the API wouldn't be able to call this method anyway.
Also, i'm not entirely clear on the purpose of this method. From what i can tell, the api impl starts with an instance of the config manager, and another instance is registered as a listener from the class itself. Then, whenever data is reloaded, setConfigData is called with that instance, which has the correct data. But, the instance stays there and doesn't need to be set again on next reload, so this all just seems a bit weird.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right. Probably need to extract an API interface for the ConfigDataManager to use in those methods.
The way data pack reloading (for vanilla, and also for this) works is as follows: The game starts without any loaded data, which is an empty accessible instance of the data manager. When data packs are reloaded, a new instance (not yet accessible outside the loading logic) is created, and when loading is done, that instance replaces the previous accessible one.
I'm not sure if there's any better place to put this, but BotaniaAPI seemed like a good place for the method that returns the currently valid config data instance. I'm also not quite sure if the setter should be publicly accessible, but I couldn't figure out a clean way to set the instance to return.
Xplat/src/main/java/vazkii/botania/common/block/flower/functional/LooniumBlockEntity.java
Show resolved
Hide resolved
Xplat/src/main/java/vazkii/botania/common/block/flower/functional/LooniumBlockEntity.java
Show resolved
Hide resolved
if (e != mob && e instanceof Mob otherMob) { | ||
// prevent armor/weapon drops on player kill, also no nautilus shells from drowned: | ||
Arrays.stream(EquipmentSlot.values()).forEach(slot -> otherMob.setDropChance(slot, 0)); | ||
LooniumComponent otherLooniumComponent = XplatAbstractions.INSTANCE.looniumComponent(otherMob); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't apply to the root mob anymore, does it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case of mob stacks (e.g. chicken jockey, or spider jockey), this prevents secondary mobs from dropping anything. In a chicken jockey, the "root vehicle" is a chicken with a jockey flag set, while in a spider jockey the "root vehicle" is the spawned spider, but it has a skeleton as passenger. So no random feathers, bones, or armor.
In later versions you will see an expansion of that piece of code to cover various edge cases, such as not spawning pillagers as captains, or actually assigning loot drops to known mob types.
var key = structureMap.firstKey(); | ||
detectedStructures = Object2BooleanMaps.singleton(key, structureMap.getBoolean(key)); | ||
} else { | ||
detectedStructures = new Object2BooleanArrayMap<>(structureMap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this check necessary? Can't you just use the constructor like this in the empty and singleton cases too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it could be considered premature optimization. The same would be true for the parts where that map is serialized to/from NBT.
"botaniamisc.loonium.multiple_structure_drops": "Attuned to %d structures", | ||
"botaniamisc.loonium.generic_loot": "Generic loot", | ||
"botaniamisc.loonium.structure_loot": "Structure loot", | ||
"botaniamisc.loonium.custom_loot": "Customized loot", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think i don't actually like this. It's not really relevant to the player (in my eyes at least), whether the loot is from a structure or set via NBT. I think i liked "Attuned" better. Maybe it could be "Attuned to <structure>", (grabbing the structure name from the config (only if we make the config mandatory)) and you could choose your own string for that if you're setting the mobs with nbt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think structures have a translated name by default. Displaying the structure resource location feels wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, which is why the name would need to be specified in the config
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Config is server side, the display name would need to be in a language file (i.e. resource pack) on the client.
consumer.accept(comp.getDrop()); | ||
} | ||
if (comp != null) | ||
if (!comp.getDrop().isEmpty() || comp.isDropNothing()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if
only fails if the component is set to drop an empty stack, without dropNothing being set, right? Does that ever happen? To me, it looks like the case when the set drop is empty works like if dropNothing is set anyway, so maybe we don't need dropNothing at all and can just set the component with an empty stack?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The component should always be attached to mobs on Fabric, so we need to somehow specify whether the mob was spawned by a Loonium or not. (I believe on NeoForge the data attachment can be created later on-demand, but the CCA logic for Fabric runs at the end of the entity's constructor.)
Maybe a better way to structure the data would be to have an "is Loonium-spawned" field that specifies that the toDrop
item stack should be used, even if it's empty.
@@ -198,10 +216,16 @@ public void tickFlower() { | |||
world.addFreshEntity(mob); | |||
mob.spawnAnim(); | |||
|
|||
addMana(-COST); | |||
addMana(-DEFAULT_COST); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, this needs to use the cost from the config, doesn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's changed in a later commit. One thing that doesn't change later, though, is the mana capacity. That is currently fixed at the default cost, so technically no structure could use a higher mana cost.
- var -> explicit type, unless it's obvious from a constructor call - extracted API interface for ConfigDataManager - replace "drop nothing" flag in LooniumComponent with "override drop"; mobs will drop whatever is defined as "to drop" when that flag is set - don't bother creating specialized map types for detected structures - adjust variable names in renderHUD code - replaced some manually validated attribute CODECs with corresponding predefined ExtraCodecs variants - allow configured mana cost for a structure to be zero - add default translation for botania:loonium_offhand_equipment tag
Implementation for structure detection and structure-specific loot tables and mob spawning pools, customizable via data packs.
minecraft:pillager_outpost
the custom Loonium loot table is located atdata/botania/loot_tables/loonium/minecraft/pillager_outpost.json
), with a default loot table corresponding to dungeon loot (located atdata/botania/loot_tables/loonium/default.json
)minecraft:pillager_outpost
the custom configuration is located atdata/minecraft/config/loonium/pillager_outpost.json
) with a default configuration (located atdata/botania/config/loonium/default.json
) acting as fallback