Skip to content

Commit 49a2f10

Browse files
authored
Update to Minecraft 26.3 (#6624)
1 parent 43cb76a commit 49a2f10

17 files changed

Lines changed: 2344 additions & 24 deletions

File tree

‎.claude/skills/mc-update/SKILL.md‎

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
---
2+
name: mc-update
3+
description: Update EssentialsX for a new Minecraft version. Use when the user asks to update to, bump, or support a new Minecraft or Paper version (for example "update to 26.4", "add 26.3 mobs", "bump supported version"), or asks what must be checked for new mobs, items, wood types, armor, enchantments, potions, or Bukkit API changes.
4+
---
5+
6+
# Minecraft update
7+
8+
This checklist comes from every Minecraft update PR from 1.14 to 26.3, and from the fix PRs that followed them.
9+
10+
Paths are relative to the repository root. `ESS` means `Essentials/src/main/java/com/earth2me/essentials`.
11+
12+
## Procedure
13+
14+
1. Find the old and new `paper-api` jars in `~/.gradle/caches/modules-2/files-2.1/io.papermc.paper/paper-api/`. If the new jar is not there, set `paperVersion` in `build-logic/src/main/kotlin/essentials.base-conventions.gradle.kts` and run `./gradlew :EssentialsX:compileJava` to download it.
15+
2. Diff the constants of these classes between the two jars: `org.bukkit.entity.EntityType`, `org.bukkit.Material`, `org.bukkit.enchantments.Enchantment`, `org.bukkit.potion.PotionEffectType`, `org.bukkit.potion.PotionType`, `org.bukkit.TreeType`, and each `*$Variant` class under `org.bukkit.entity`. Use this command for each class:
16+
17+
```sh
18+
unzip -p <jar> org/bukkit/entity/EntityType.class > /tmp/x.class && javap -p /tmp/x.class | grep 'public static final'
19+
```
20+
21+
3. Sort each added, removed, or renamed constant into a section of part 2 or part 3 below.
22+
4. Do part 1. Then do each section that applies.
23+
5. Apply part 4 to every line you write.
24+
6. Run `./gradlew :EssentialsX:compileJava`. Fix errors.
25+
7. Do not commit unless the user asks.
26+
8. Report per section: what changed, and what you skipped and why.
27+
28+
---
29+
30+
## 1. Every update
31+
32+
### 1.1 Version constants
33+
34+
1. Open `ESS/utils/VersionUtil.java`.
35+
2. Add a constant `vXX_Y_R01 = BukkitVersion.fromString("XX.Y-R0.1-SNAPSHOT")`.
36+
- For a patch release, change the string of the existing minor constant. Example: `v26_1_R01` holds `26.1.2`.
37+
- For a minor release, add a new constant. Do not rename or delete an old constant. Old constants gate features. Deleting `v1_20_4_R01` caused #6460.
38+
3. Add the constant to `supportedVersions`. Keep the `ImmutableSet.of(...)` syntax. `build-logic/src/main/kotlin/VersionDataTask.kt` parses it with a regex.
39+
4. Start a server. Confirm `BukkitVersion.fromString(Bukkit.getBukkitVersion())` parses. Spigot 1.14.3 and Paper 26.1 both changed the string format.
40+
41+
### 1.2 Documentation
42+
43+
1. Update the supported versions sentence in `README.md`.
44+
2. Regenerate `items.json` with ItemDbGenerator.
45+
46+
---
47+
48+
## 2. New content
49+
50+
### 2.1 New mob
51+
52+
1. Add an entry to `ESS/Mob.java`: `NAME("CamelName", Enemies.X, "ENTITY_TYPE")`.
53+
- Use the string constructor. Do not use `EntityType.X`. The constant does not exist on old servers.
54+
- Set the plural suffix if it is not `s`. Fish use `""`. Fox uses `es`.
55+
- The display name must not contain a space.
56+
- Boats and rafts are also `Mob` entries.
57+
2. Add `EntityType` constant to `ESS/MobCompat.java` only if `MobData` needs it.
58+
3. Add the mob to `protect.prevent.spawn` in `Essentials/src/main/resources/config.yml`.
59+
4. Check `entityToDisplayName` in `providers/BaseProviders/.../SpawnerItemProvider.java` if the enum name does not title-case well.
60+
61+
### 2.2 New mob variant or data
62+
63+
1. Add entries to `ESS/MobData.java`. Use the string form `"kind:VARIANT"`. Example: `"wolf:PALE"`.
64+
2. Add a `case "kind":` to the `setData` switch.
65+
3. Add `setKindVariant(Entity, String)` to `MobCompat.java`. Gate it with `VersionUtil.getServerBukkitVersion().isLowerThan(vXX_Y_R01)`.
66+
4. Use `RegistryUtil.valueOf(Variant.class, name)` for registry types. Do not use `Enum.valueOf`.
67+
5. Do not call `NewClass.Variant.values()` in an enum initializer. Axolotl did this. It threw `NoClassDefFoundError` on 1.16 (#4376).
68+
6. Catch `Throwable` around reflective lookups. Cow variant threw NPE on old servers (#6121).
69+
7. For a boolean flag, add a `Data` constant and a branch in `setData`. Example: `GOAT_SCREAMING`.
70+
71+
### 2.3 New rideable or saddle mob
72+
73+
1. Add `SADDLE_<MOB>` to `MobData.java` with a `Data.<MOB>SADDLE` constant.
74+
2. Add `set<Mob>Saddle(Entity, Player)` to `MobCompat.java`. Tame the mob. Set the owner. Set the saddle.
75+
3. Check `ESS/craftbukkit/Inventories.java` if the mob adds a player inventory slot. Body and saddle slots (41, 42) broke `/clearinventory` in 1.21.6 (#6218).
76+
77+
### 2.4 New item or block
78+
79+
1. Check each `MaterialUtil` set that lists names by hand. See table.
80+
2. Check `ESS/utils/LocationUtil.java`:
81+
- Add a block that hurts a standing player to `DAMAGING_TYPES`. Magma, campfire, berry bush and wither rose were missed for 2 years (#3537).
82+
- `HOLLOW_MATERIALS` comes from `Material.isTransparent()`. Add a passable block that is not transparent. Example: `LIGHT` (#4601). Remove a transparent block that is solid. Examples: `BARRIER`, `DIRT_PATH`, `FARMLAND`.
83+
3. Check for a new event that reveals player position. Sculk sensor needed vanish handling in `EssentialsPlayerListener` (#5262).
84+
85+
| `ESS/utils/MaterialUtil.java` set | Add when |
86+
|---|---|
87+
| `HELMETS`, `CHESTPLATES`, `LEGGINGS`, `BOOTS` | New armor tier |
88+
| `LEATHER_ARMOR` | New dyeable armor |
89+
| `BEDS` | New bed. `STRAW_BED` in 26.3. |
90+
| `BANNERS` | New banner colour |
91+
| `SIGN_POSTS`, `WALL_SIGNS`, `HANGING_SIGNS`, `HANGING_WALL_SIGNS` | New wood type |
92+
| `MOB_HEADS` | New head. `PIGLIN_HEAD` in 1.20. |
93+
| `POTIONS` | New potion container |
94+
| `EDITABLE_BOOKS` | New book type |
95+
| `FIREWORKS`, `FIREWORK_CHARGE` | New firework item |
96+
97+
`EnumUtil.getAllMatching` skips a name that does not exist. Adding a name is safe on old servers.
98+
99+
### 2.5 Renamed item, block or entity
100+
101+
1. Grep `ESS` for `Material.OLD_NAME` and `EntityType.OLD_NAME`. Replace with `EnumUtil.getMaterial("NEW", "OLD")` or a `MobCompat` constant. `EntityType.PIG_ZOMBIE` in a static map broke 1.16 (#3412). `EntityType.FIREWORK` broke `/firework` in 1.20.6.
102+
2. For a renamed `EntityType`, add `getEntityType("NEW", "OLD")` to `MobCompat.java`. Update `Mob.java`, `MobData.java` and `SpawnMob.java`.
103+
104+
### 2.6 New wood type
105+
106+
1. Add `<WOOD>_SIGN`, `<WOOD>_WALL_SIGN`, `<WOOD>_HANGING_SIGN`, `<WOOD>_WALL_HANGING_SIGN` to the four sign sets in `MaterialUtil.java`. Crimson and warped were missed in 1.16 (#3487).
107+
2. Add `<WOOD>_BOAT` to `Mob.java`. Boats are separate entity types since 1.21.3.
108+
3. Add `<WOOD>` to `MobCompat.BoatVariant` for servers before 1.21.3.
109+
4. Add `<WOOD>_BOAT` to `MobData.java`.
110+
5. Add `<WOOD>_CHEST_BOAT` to `Mob.java`.
111+
112+
### 2.7 New tree type
113+
114+
1. `Commandtree` reads `TreeType.values()`. It needs no code change.
115+
2. Add the name to `treeCommandUsage` and `treeCommandUsage1` in `messages.properties`.
116+
3. `Commandbigtree` has four hard-coded types. Add a case if the tree has a big variant.
117+
4. Bukkit adds `TreeType` late. 26.3-pre-2 has no `POPLAR`.
118+
119+
### 2.8 New armor tier
120+
121+
1. Add the four pieces to `HELMETS`, `CHESTPLATES`, `LEGGINGS`, `BOOTS` in `MaterialUtil.java`.
122+
2. Add a branch to the armor keyword chain in `ESS/SpawnMob.java`. The chain has `netherite`, `copper`, `diamond`, `gold`, `leather`, `iron`.
123+
3. For horse armor, add `<TIER>_HORSE_ARMOR` to `MobData.java`. Example: `NETHERITE_HORSE_ARMOR`.
124+
125+
### 2.9 New enchantment
126+
127+
1. Add a `try { // XX.Y }` block to `ESS/Enchantments.java`.
128+
2. Look up by name: `Enchantment.getByName("NAME")`. Check for null. Do not use `Enchantment.NAME`.
129+
3. Add `ENCHANTMENTS.put("name", e)` and `ALIASENCHANTMENTS.put("alias", e)`.
130+
4. Swift sneak (1.19) and lunge (1.21.11) were missed. Contributors added them months later.
131+
5. Display and permission names come from `Enchantments.getRealName()`. It uses the key on 1.13+.
132+
133+
### 2.10 New potion effect or potion type
134+
135+
1. Add a `try { // XX.Y }` block to `ESS/Potions.java`. Catch `Throwable`.
136+
2. Add `POTIONS.put("name", PotionEffectType.NAME)` and `ALIASPOTIONS.put(...)`.
137+
3. `ModernPotionMetaProvider` maps `LONG_` and `STRONG_` prefixes. Check a new potion follows that pattern.
138+
139+
### 2.11 New banner pattern
140+
141+
No code change. `PatternTypeProvider` reads the registry.
142+
143+
### 2.12 New colour family
144+
145+
Check `MaterialUtil.getColorOf` for a colour name that is not a `DyeColor`.
146+
147+
### 2.13 New spawn egg
148+
149+
No code change. `FlatSpawnEggProvider` derives the egg from `EntityType.name() + "_SPAWN_EGG"`.
150+
151+
### 2.14 New command for new content
152+
153+
1. Gate the command with `VersionUtil`. Return `tl("unsupportedFeature")` on old servers. Examples: `/beezooka`, `/ice`.
154+
2. Add the command to `plugin.yml` and `messages.properties`. Check message parameters. `iceOther` shipped without `{0}`.
155+
156+
---
157+
158+
## 3. API changes
159+
160+
### 3.1 Enum becomes a registry
161+
162+
Enchantment, PotionEffectType, Sound, PatternType, Villager.Profession, Cat.Type, Biome and Boat.Type all changed.
163+
164+
1. Replace `EnumUtil.valueOf` with `RegistryUtil.valueOf(Class, names...)`. Replace `.values()` with `RegistryUtil.values(Class)`.
165+
2. If a static method moved from an enum to an interface, code compiled against the new API throws `IncompatibleClassChangeError` on old servers. `catch (Exception)` does not catch it. `PatternType.getByIdentifier` did this (#6553).
166+
3. For such a case, add a provider pair:
167+
- Interface in `providers/BaseProviders/src/main/java/net/ess3/provider/`.
168+
- `Modern*Provider` in BaseProviders with `@ProviderData(weight = 1)` and a `@ProviderTest`.
169+
- `Legacy*Provider` in `providers/1_12Provider` or `providers/1_8Provider`. These compile against the old API.
170+
- Register with `providerFactory.registerProvider(Legacy.class, Modern.class)` in `Essentials.java`.
171+
4. In a `@ProviderTest`, use `getMethod`, not `getDeclaredMethod`. 1.21 moved spawner methods to a super-interface (80bdc5269).
172+
173+
### 3.2 Removed API class
174+
175+
`PotionData`, `Potion`, `BannerMeta.setBaseColor` and `Achievement` were removed.
176+
177+
1. Move code that uses the removed class to a provider in `1_12Provider` or `1_8Provider`.
178+
2. Add a modern provider in BaseProviders.
179+
3. Remove the method from `OfflinePlayer`, `FakeWorld` and `FakeServer` if it was an override.
180+
181+
### 3.3 Changed method signature
182+
183+
1. Read the Bukkit diff for changed return types on methods EssentialsX calls.
184+
2. A changed return type changes the call descriptor. The call throws `NoSuchMethodError` on old servers. `PlayerPickupArrowEvent#getArrow` did this (#3175). `InventoryView` becoming an interface did this (#5851).
185+
3. Use a provider or reflection for the changed call.
186+
187+
### 3.4 New API methods on Player, World, Server
188+
189+
1. Add stubs to `providers/1_8Provider/.../OfflinePlayer.java` and `providers/1_12Provider/.../FakeWorld.java` if they fail to compile.
190+
2. Tests use MockBukkit since 1.21.6. `FakeServer` no longer needs updates.
191+
192+
### 3.5 New event
193+
194+
1. Put the handler in a separate `*Listener_XX_Y` class.
195+
2. Register it behind a `VersionUtil` check or `Class.forName`.
196+
3. Do not put a new event in a listener that loads on old servers. `EntityTransformEvent` failed on 1.13.1 (#2942).
197+
198+
### 3.6 Spigot NMS obfuscation
199+
200+
1. Add `VXX_Y_RN` to `providers/NMSReflectionProvider/.../ReflUtil.java` if Spigot bumps the NMS revision.
201+
2. Update the obfuscated `MinecraftServer#isRunning` name table in `ReflServerStateProvider.java`. This broke on 1.18, 1.19, 1.19.3, 1.20.6 and 1.21.10.
202+
3. Paper uses `PaperServerStateProvider`. Test on Spigot to see this fail.
203+
204+
### 3.7 Paper internal classes
205+
206+
Paper config classes move. `ReflOnlineModeProvider` broke on 1.19 when config moved to `io.papermc.paper.configuration.GlobalConfiguration`. Check every reflective Paper access.
207+
208+
### 3.8 World height
209+
210+
1. Do not hard-code `0`, `-1`, `128` or `256` as a Y bound.
211+
2. Use `WorldInfoProvider.getMinHeight`, `getMaxHeight` and `getLogicalHeight`.
212+
3. Grep `LocationUtil`, `RandomTeleport`, `Commandtop`, `Commandsethome`, `Teleport`, `AsyncTeleport` and `SpawnMob`. 1.18 needed three fix PRs (#4657, #4667, #4715).
213+
214+
### 3.9 Item data components
215+
216+
1. Check `MetaItemStack.parseStringMeta`. The `[...]` component branch calls `UnsafeValues#modifyItemStack`. Its argument shape changed in 1.20.5 and again in 26.1 (#6570).
217+
2. Check `Commandbook`. Writable and written books have separate meta since 1.20.6 (#6064).
218+
3. Check `Commandskull`. Skull owner needs a resolved profile since 1.20.4 (#6188).
219+
4. Check `MetaItemStack.canSpawn()`.
220+
221+
### 3.10 Vanilla messages
222+
223+
Check the vanilla changelog for a new client message that duplicates an EssentialsX message. 1.15 added "Respawn point set". `EssentialsPlayerListener` now gates `bedSet` (#2943).
224+
225+
### 3.11 Version string format
226+
227+
1. `BukkitVersion.VERSION_PATTERN` in `VersionUtil.java` must parse the new string.
228+
2. Supported suffixes: `-snapshot-N`, `-pre-N`, `-rc-N`, `.build.N-channel`, `.local`, `-R0.1-SNAPSHOT`.
229+
3. `compareDevSpecifier` orders `snapshot < pre < rc < release`.
230+
4. `isSupportedVersion` compares major, minor and patch. A patch release needs a constant change.
231+
5. 26.1 needed four fix PRs for this (#6480, #6483, #6500, #6501).
232+
233+
### 3.12 Adventure and Paper native paths
234+
235+
Each Paper API shift produced a gate:
236+
- `Essentials.initAdventureFacet` gates the Paper facet at `v1_18_2_R01` (#6562).
237+
- `EssentialsSpawnPlayerListener` gates `getRespawnLocation` at `v1_20_4_R01` (#6460).
238+
- `EssentialsPlayerListener` gates configuration events at `v1_21_8_R01`.
239+
240+
Check each gate against the new version.
241+
242+
---
243+
244+
## 4. Compatibility rules
245+
246+
EssentialsX runs on 1.8.8 through the current version. Every update PR must keep that.
247+
248+
1. Do not reference a new `Material`, `EntityType`, `Enchantment` or `PotionEffectType` constant in a static field or enum initializer. Use `EnumUtil.getMaterial`, `EnumUtil.getEntityType` or a name lookup.
249+
2. Catch `Throwable`, not `Exception`, around reflective lookups and provider selection. A swallowed `NoSuchFieldError` selected the wrong provider (7d6195375).
250+
3. Use only Guava and Gson methods that exist in 1.8.8. `Ints.constrainToRange` and `JsonArray#add(String)` do not (#4708, 576866540).
251+
4. Compile legacy providers in `1_8Provider` or `1_12Provider`. Code compiled against the new API can carry the wrong bytecode.
252+
5. Gate with `isLowerThan(<next major>)`, not `isLowerThanOrEqualTo(<patch>)`. Forks use nonstandard revision numbers (43eff69a2).
253+
6. Do not delete a `VersionUtil` constant.
254+
7. Test on Paper latest, Spigot latest, 1.12.2 and 1.8.8. Most follow-up fixes in this history came from old servers.
255+
256+
---
257+
258+
## 5. Test
259+
260+
1. Run `./gradlew test`.
261+
2. Run `./gradlew runServer`.
262+
3. Run `/ess version`. Confirm the support status is FULL.
263+
4. Run `/spawnmob <newmob>`, `/spawnmob <mob>:<variant>`.
264+
5. Run `/give @s <newitem>`, `/give @s minecraft:<newitem>`.
265+
6. Run `/enchant <newenchant>`, `/potion <neweffect>`.
266+
7. Place an EssentialsX sign on each new sign type.
267+
8. Run `/tree <newtree>` if Bukkit has the `TreeType`.
268+
9. Run `/kit` with the new armor. Confirm auto-equip.
269+
10. Teleport onto each new damaging block. Confirm `/tp` refuses.
270+
11. Repeat on Spigot for `ReflServerStateProvider`.

‎.idea/checkstyle-idea.xml‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Essentials/src/main/java/com/earth2me/essentials/Enchantments.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ public final class Enchantments {
313313
ENCHANTMENTS.put("density", density);
314314
}
315315
final Enchantment windBurst = Enchantment.getByName("WIND_BURST");
316-
if (breach != null) {
316+
if (windBurst != null) {
317317
ENCHANTMENTS.put("windburst", windBurst);
318318
ALIASENCHANTMENTS.put("wind", windBurst);
319319
ALIASENCHANTMENTS.put("burst", windBurst);

‎Essentials/src/main/java/com/earth2me/essentials/Mob.java‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public enum Mob {
6060
CHERRY_BOAT("CherryBoat", Enemies.NEUTRAL, "CHERRY_BOAT"),
6161
BAMBOO_RAFT("BambooRaft", Enemies.NEUTRAL, "BAMBOO_RAFT"),
6262
PALE_OAK_BOAT("PaleOakBoat", Enemies.NEUTRAL, "PALE_OAK_BOAT"),
63+
POPLAR_BOAT("PoplarBoat", Enemies.NEUTRAL, "POPLAR_BOAT"),
6364
MINECART("Minecart", Enemies.NEUTRAL, EntityType.MINECART),
6465
MINECART_CHEST("ChestMinecart", Enemies.NEUTRAL, MobCompat.CHEST_MINECART),
6566
MINECART_FURNACE("FurnaceMinecart", Enemies.NEUTRAL, MobCompat.FURNACE_MINECART),
@@ -118,7 +119,17 @@ public enum Mob {
118119
FROG("Frog", Enemies.FRIENDLY, "FROG"),
119120
TADPOLE("Tadpole", Enemies.FRIENDLY, "TADPOLE"),
120121
WARDEN("Warden", Enemies.ENEMY, "WARDEN"),
121-
CHEST_BOAT("ChestBoat", Enemies.NEUTRAL, "CHEST_BOAT"),
122+
CHEST_BOAT("ChestBoat", Enemies.NEUTRAL, MobCompat.OAK_CHEST_BOAT),
123+
ACACIA_CHEST_BOAT("AcaciaChestBoat", Enemies.NEUTRAL, "ACACIA_CHEST_BOAT"),
124+
DARK_OAK_CHEST_BOAT("DarkOakChestBoat", Enemies.NEUTRAL, "DARK_OAK_CHEST_BOAT"),
125+
BIRCH_CHEST_BOAT("BirchChestBoat", Enemies.NEUTRAL, "BIRCH_CHEST_BOAT"),
126+
JUNGLE_CHEST_BOAT("JungleChestBoat", Enemies.NEUTRAL, "JUNGLE_CHEST_BOAT"),
127+
SPRUCE_CHEST_BOAT("SpruceChestBoat", Enemies.NEUTRAL, "SPRUCE_CHEST_BOAT"),
128+
MANGROVE_CHEST_BOAT("MangroveChestBoat", Enemies.NEUTRAL, "MANGROVE_CHEST_BOAT"),
129+
CHERRY_CHEST_BOAT("CherryChestBoat", Enemies.NEUTRAL, "CHERRY_CHEST_BOAT"),
130+
BAMBOO_CHEST_RAFT("BambooChestRaft", Enemies.NEUTRAL, "BAMBOO_CHEST_RAFT"),
131+
PALE_OAK_CHEST_BOAT("PaleOakChestBoat", Enemies.NEUTRAL, "PALE_OAK_CHEST_BOAT"),
132+
POPLAR_CHEST_BOAT("PoplarChestBoat", Enemies.NEUTRAL, "POPLAR_CHEST_BOAT"),
122133
CAMEL("Camel", Enemies.FRIENDLY, "CAMEL"),
123134
SNIFFER("Sniffer", Enemies.FRIENDLY, "SNIFFER"),
124135
ARMADILLO("Armadillo", Enemies.FRIENDLY, "ARMADILLO"),
@@ -132,6 +143,7 @@ public enum Mob {
132143
ZOMBIE_NAUTILUS("ZombieNautilus", Enemies.NEUTRAL, "ZOMBIE_NAUTILUS"),
133144
PARCHED("Parched", Enemies.ENEMY, "PARCHED"),
134145
SULFUR_CUBE("SulfurCube", Enemies.FRIENDLY, "SULFUR_CUBE"),
146+
CUSHION("Cushion", Enemies.FRIENDLY, "CUSHION")
135147
;
136148

137149
private static final Map<String, Mob> hashMap = new HashMap<>();

‎Essentials/src/main/java/com/earth2me/essentials/MobCompat.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public final class MobCompat {
8888
public static final EntityType END_CRYSTAL = getEntityType("END_CRYSTAL", "ENDER_CRYSTAL");
8989
public static final EntityType FIREWORK_ROCKET = getEntityType("FIREWORK_ROCKET", "FIREWORK");
9090
public static final EntityType OAK_BOAT = getEntityType("BOAT", "OAK_BOAT");
91+
public static final EntityType OAK_CHEST_BOAT = getEntityType("CHEST_BOAT", "OAK_CHEST_BOAT");
9192

9293
private MobCompat() {
9394
}
@@ -409,6 +410,7 @@ public enum BoatVariant {
409410
OAK("GENERIC", "OAK"),
410411
SPRUCE("REDWOOD", "SPRUCE"),
411412
MANGROVE("GENERIC", "MANGROVE"),
413+
POPLAR("GENERIC", "POPLAR"),
412414
;
413415

414416
private final String treeSpecies;

0 commit comments

Comments
 (0)