diff --git a/AGENTS.md b/AGENTS.md
index 13e69ee..e55cafa 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -55,6 +55,13 @@ Registration is composed through `RetoldSubsystems` and the classes under `modul
### Architecture And Ownership
+- Design new systems with mod and datapack compatibility in mind from the start. Prefer standard
+ tags when their full meaning matches, Retold-owned semantic tags/data for Retold concepts, and
+ small optional hooks over hard-coded third-party integrations.
+- When materially changing an existing subsystem, audit compatibility assumptions on the touched
+ path and improve them when safe and testable. Do not perform unrelated compatibility rewrites;
+ document and defer unresolved cases. Preserve standalone defaults unless the developer explicitly
+ approves a gameplay change, and regression-test those defaults when adding extension points.
- Keep gameplay rules in named Retold system classes. Mixins should normally be hooks, guards, accessors, or narrowly scoped vanilla interception points.
- Route world-stage changes through `RetoldStageManager.setStage` so persistence, synchronization, and transition effects remain consistent.
- Keep server gameplay state authoritative. Client code should render, present feedback, or consume synchronized state rather than decide progression.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2a5541..602bede 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,9 @@ Each release should be readable in two passes:
### Technical
+- Added the initial stable `cz.xefensor.retold.api` compatibility surface. `RetoldRecipeKnowledge` now gives vanilla and optional viewers one synchronized per-player visibility authority, keeps unknown custom recipe types visible by default, and lets integrations opt types into discovery with removable registrations. `RetoldWorldProtection` adds default-allow, deny-capable mutation rules with exact affected bounds and now gates Retold-owned mob block changes, Gale Core breaking, Aender regeneration/portals, and delayed structure retrogen. Focused tests cover unchanged standalone behavior, per-player recipe isolation, client payload round trips, custom recipe-type registration, denied mutations, and registration removal; concrete EMI, JEI, and claim-mod adapters remain pending.
+- Completed a behavior-preserving exact item/block compatibility audit. Datapack extension tags now cover environmental mob resources, ordinary forage and dropped-food families, Spider-lair web counting, Illager village signals, Nether-remnant and Ocean-Monument guard anchors, protected monument blocks, consumable Campfire igniters, and leaf-preserving tools. Exact-match vanilla and NeoForge common tags are composed where safe, while Retold's former vanilla memberships remain the defaults. Fixed progression, paired transformation, portal/structure, unique loot/replacement, and unverified Sniffer checks remain exact by design. Added focused registry tests and a modpack-author compatibility reference.
+- Made faction classification data-driven through additive entity-type tags while retaining the fixed Retold relationship matrix. The default tags preserve prior members, compose standard Illager and Undead classifications, and keep Witch-style loose alliance separate; ambiguous full memberships fail closed. Cached membership and faction goals now follow server tag reloads, and focused tests cover defaults, targeting, retaliation, loose allies, conflicts, and tamed undead mounts.
- Added the 48-durability Flint Multi-tool, Flint Spear, and provisional Steel material with data-driven mining, repair, common-ingot, Spear, enchantment-family, and armor tags. A global loot modifier supplies scaled Sticks from leaves and woody bushes, and placed-feature overrides reduce both ordinary and Dripstone Copper attempts from sixteen to six per chunk. Data-driven chest and Villager-trade overrides enforce fixed acquisition tiers without per-player state, while enchantment tags remove Mending from new trade/random-loot generation. Exact Wooden/Stone recipes are removed during recipe loading; Armorer, Toolsmith, and Weaponsmith lessons follow the material ladder. Focused GameTests cover scaled Stick supply, acquisition tables and prices, Mending source removal, reduced Copper placement, Campfire behavior, harvest tiers, Spear/starter/equipment crafting, station processing, and Furnace availability.
- Added tag-driven dynamic Diamond durability through a narrow `ItemStack.getMaxDamage` hook and a named progression policy. Diamond Horse and Nautilus Armor receive a 528-durability component and server-side BODY-slot wear from non-bypassing hits. Focused coverage enumerates all twelve enchantable Diamond items and verifies fragile values, enchantment restoration, removal regression, real animal-armor wear, vanilla-tag preservation, and the over-damaged stripped-item safeguard.
- Added data-driven Chestplate enchantment compatibility and material-matched enchantability components for all twelve vanilla animal armors. A Wolf-specific incoming-damage bridge applies vanilla enchantment protection before Wolf Armor's special durability-absorption path bypasses ordinary living-entity mitigation. Focused coverage verifies every supported-enchantment set plus equipped Protection-family mitigation, Fire Protection burn duration, Thorns, Unbreaking, Mending, Binding, and Vanishing behavior across Wolf, Horse, and Nautilus armor paths.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a76f11b..eef5db5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -26,9 +26,14 @@ The current target is recorded in [`gradle.properties`](gradle.properties). Usef
1. Start from current `master` and create a focused branch.
2. Keep unrelated formatting and refactors out of the change.
-3. Update tests, documentation, and `CHANGELOG.md` when affected.
-4. Run appropriate validation and describe it honestly.
-5. Open a pull request and complete its template.
+3. Design new systems with mod and datapack compatibility in mind. When materially changing an
+ existing subsystem, audit compatibility assumptions on the touched path and improve them when
+ safe without turning the contribution into an unrelated rewrite.
+4. Preserve Retold's standalone defaults when adding extension points, unless a gameplay change was
+ explicitly agreed, and add focused regression coverage for those defaults.
+5. Update tests, documentation, and `CHANGELOG.md` when affected.
+6. Run appropriate validation and describe it honestly.
+7. Open a pull request and complete its template.
Small changes are easier to verify. A technically sound proposal may still be declined when it conflicts with Retold's design.
diff --git a/README.md b/README.md
index 0fb8108..aac265f 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,7 @@ Some systems still need broader real-world validation, especially dimension trav
- [`docs/README.md`](docs/README.md) — documentation index
- [`docs/design-overview.md`](docs/design-overview.md) — world and gameplay design
+- [`docs/compatibility.md`](docs/compatibility.md) — supported datapack extension tags and integration boundaries
- [`ROADMAP.md`](ROADMAP.md) — public Now / Next / Later roadmap
- [`docs/internal/README.md`](docs/internal/README.md) — developer and technical documentation
- [`docs/internal/retold_mod_system.md`](docs/internal/retold_mod_system.md) — implementation architecture
diff --git a/docs/README.md b/docs/README.md
index 3faefd2..fc822f0 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -9,6 +9,7 @@ This directory separates player-facing design information from detailed develope
- [`../CHANGELOG.md`](../CHANGELOG.md) — released and unreleased changes
- [`../SUPPORT.md`](../SUPPORT.md) — help and problem reporting
- [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — contribution workflow
+- [`compatibility.md`](compatibility.md) — supported datapack extension tags and integration boundaries
## Developer And Technical Documents
diff --git a/docs/compatibility.md b/docs/compatibility.md
new file mode 100644
index 0000000..6a8fedf
--- /dev/null
+++ b/docs/compatibility.md
@@ -0,0 +1,249 @@
+# Retold Mod And Datapack Compatibility
+
+Retold is designed to run without hard dependencies on other content mods. Datapacks and
+compatibility addons can extend selected Retold-owned gameplay concepts through tags while the
+default Retold behavior remains unchanged.
+
+Compatibility means that projects can run together safely. It does not promise that another mod's
+items, machines, dimensions, or progression will preserve Retold's intended balance.
+
+## Compatibility-First Development
+
+New Retold systems must consider mod and datapack compatibility as part of their initial design,
+not as a retrofit after their implementation is complete. During design and review, identify which
+concepts should use standard Minecraft or NeoForge tags, which Retold-owned semantics need their own
+data or tags, and which integration points require a small stable hook. Unknown third-party content
+must fail safely and must not be taken over merely because it resembles a vanilla implementation.
+
+Existing systems do not need broad compatibility rewrites in isolation. When an existing subsystem
+is materially changed, audit the touched item, block, entity, recipe, world-mutation, and API
+assumptions and improve their extension points in the same focused change when that can be done
+safely. If a safe compatibility design depends on an unresolved gameplay decision or cannot be
+properly tested, document the limitation and defer it instead of weakening the current behavior.
+
+Compatibility work must preserve Retold's built-in defaults and standalone behavior. New extension
+points need focused regression coverage for those defaults, and should remain data-driven or
+optional so installing no compatibility addon produces the same gameplay as before.
+
+## Block And Item Extension Tags
+
+The following block and item tags are supported extension points:
+
+| Tag | Meaning |
+| --- | --- |
+| `retold:armadillo_grub_soils` | Exposed ground from which a hungry Armadillo may obtain grubs without removing the block. |
+| `retold:armadillo_scrub_range_blocks` | Exposed terrain that can anchor an Armadillo scrub range. |
+| `retold:panda_bamboo_blocks` | Blocks a hungry Panda may seek and consume. Successful feeding removes the block, subject to `mobGriefing`. |
+| `retold:turtle_beach_blocks` | Exposed beach terrain a Turtle may use when it is near water. |
+| `retold:desert_browse_blocks` | Renewable, non-destructive habitat forage for hungry Camels and Rabbits. |
+| `retold:goat_scrape_blocks` | Renewable, non-destructive habitat forage for hungry Goats. |
+| `retold:mooshroom_grazing_blocks` | Renewable, non-destructive habitat forage for hungry Mooshrooms. |
+| `retold:forage_crops` | Crop blocks grazers and small passive animals may consume. Tagged blocks are destroyed, subject to `mobGriefing`. |
+| `retold:forage_flowers` | Flower blocks Bees may visit and herbivores may consume. Animal consumption destroys the block, subject to `mobGriefing`; Bee visits do not. |
+| `retold:grazer_forage_plants` | Other plant blocks grazers may consume, including the default grasses and ferns. |
+| `retold:small_passive_forage_plants` | Other plant blocks small passive animals may consume. |
+| `retold:turtle_forage_blocks` | Blocks hungry Turtles may consume. |
+| `retold:hoglin_forage_blocks` | Blocks hungry Hoglins may consume. |
+| `retold:piglin_forage_blocks` | Blocks hungry Piglins may consume. |
+| `retold:strider_forage_blocks` | Blocks hungry Striders may consume. |
+| `retold:spider_lair_web_blocks` | Blocks counted toward a Spider lair's 50-web cap. Spiders still place vanilla Cobwebs. |
+| `retold:illager_village_signal_blocks` | Blocks that make nearby Illagers recognize an area as village-like while roaming. |
+| `retold:nether_remnant_guard_anchor_blocks` | Blocks Wither Skeletons and Blazes may use to recover their Nether-remnant guard post. |
+| `retold:ocean_monument_guard_anchor_blocks` | Blocks Guardians may use to recover their monument guard post. |
+| `retold:ocean_monument_protected_blocks` | Blocks whose mining can trigger Guardian pressure when they are part of a valid Ocean Monument. |
+| `retold:weak_mob_barriers` | Blocks desperate eligible mobs may break through under Retold's normal ownership, timing, and `mobGriefing` rules. |
+| `retold:campfire_consumable_igniters` | Items that light an unlit Campfire and are consumed one at a time in Survival. This is separate from durability-based igniters such as Flint and Steel. |
+| `retold:leaf_preserving_tools` | Tools that suppress Retold's supplemental Stick drops from leaves and woody bushes. Ordinary block loot remains unchanged. |
+| `retold:meat_foods` | Meat accepted by Retold predators, hungry Nether mobs, hungry undead, and Animal Feeders. It includes `minecraft:meat`. |
+| `retold:fish_foods` | Fish accepted by Retold predators, Nautiluses, Guardians, and Animal Feeders. It includes `minecraft:fishes`. |
+| `retold:berry_foods` | Berries accepted by Foxes and Animal Feeders. It includes `c:foods/berry`. |
+| `retold:grazer_foods` | Dropped or feeder food accepted by Retold grazers. |
+| `retold:small_passive_foods` | Dropped or feeder food accepted by Retold small passive animals. |
+| `retold:flower_foods` | Dropped or feeder flower items accepted by Bees and eligible herbivores. |
+| `retold:nether_fungus_foods` | Fungi accepted by hungry Nether mobs. It includes `c:mushrooms` and the vanilla Hoglin and Strider food tags. |
+| `retold:bat_foods` | Dropped food accepted by hungry Bats. |
+| `retold:feline_scavenge_foods` | Extra scavenged food accepted by Cats and Ocelots in addition to meat and fish. |
+| `retold:torch_igniters` | Items players may use to relight Retold's extinguished torches. |
+
+Only add content whose behavior matches the tag's complete meaning. For example, a block added to
+`retold:panda_bamboo_blocks` must be safe to remove when consumed; a decorative prismarine-like
+block should not enter `retold:ocean_monument_protected_blocks` unless it should participate in
+monument mining pressure. An item added to `retold:campfire_consumable_igniters` must be safe to
+consume one at a time; durability-based tools belong in their own interaction path instead.
+
+The three renewable habitat-forage tags do not remove their blocks. They remain usable when
+`mobGriefing=false`, use Retold's normal bounded forage search, and observe the existing 600-tick
+per-mob repeat-use cooldown. Add only blocks that are safe to treat as an indefinitely renewable
+food source.
+
+Ordinary forage tags are destructive: successful consumption removes the tagged block and requires
+`mobGriefing`. Compatibility packs must not add valuable, container-like, protected, or
+state-bearing blocks unless that destruction is intended. Newly tagged foods receive the existing
+generic relief for their diet category; established high-value Wheat/Hay and crop relief remains
+unchanged.
+
+Retold also respects the standard `minecraft:armadillo_food`, `minecraft:turtle_food`, and
+`minecraft:panda_food` item tags in its dropped-food path. `retold:leaf_preserving_tools` includes
+NeoForge's `c:tools/shear`, so ordinary modded shears work without an additional Retold-specific
+entry.
+
+## Adding A Block Or Item
+
+Add a tag file under the compatibility datapack's namespace. This example allows Turtles to treat
+`examplemod:black_sand` as beach terrain:
+
+```json
+{
+ "replace": false,
+ "values": [
+ "examplemod:black_sand"
+ ]
+}
+```
+
+Save it as:
+
+```text
+data/retold/tags/block/turtle_beach_blocks.json
+```
+
+Use the corresponding `data/retold/tags/item/.json` path for an item tag. Keep `replace`
+false so Retold's defaults and entries from other compatibility packs remain available.
+
+## Faction Entity Tags
+
+Compatibility datapacks can classify an entity type into one Retold faction by appending it to one
+of these tags:
+
+| Tag | Retold identity and inherited relationship rules |
+| --- | --- |
+| `retold:factions/nether_remnants` | Nether Remnant guards and territory members. |
+| `retold:factions/illagers` | Full Illager members, including territory and raid relationships. |
+| `retold:factions/undead` | Undead hostility and tolerance. Retold's default composes `minecraft:undead` and adds Ghasts. |
+| `retold:factions/slimes` | Slime and Magma Cube diplomacy. |
+| `retold:factions/aquatic_hostiles` | Guardian-family hostility. |
+| `retold:factions/creepers` | Creeper identity used by global target-safety rules. |
+| `retold:factions/arthropods` | Spider-family diplomacy. This intentionally does not include every entity in Minecraft's broader arthropod tag. |
+| `retold:factions/silverfish` | Silverfish diplomacy and same-species swarm boundary. |
+| `retold:factions/endermites` | Endermite diplomacy and same-species swarm boundary. |
+| `retold:factions/nether_beasts` | Hoglin-style faction relationships. |
+| `retold:factions/breezes` | Breeze faction relationships. |
+| `retold:factions/wardens` | Warden faction relationships. |
+| `retold:factions/bosses` | Boss faction relationships. |
+| `retold:factions/creakings` | Creaking faction relationships. |
+| `retold:factions/village_defenders` | Village Defender targeting and retaliation. |
+| `retold:factions/enders` | Ender faction relationships. |
+| `retold:alliances/illager_loose_allies` | Permanent non-hostile Illager alignment with combat cooperation only while both entities share the same active raid. This does not grant Illager territory membership. |
+
+Faction describes diplomacy, target selection, assistance, retaliation, and configured territory
+membership. It does not assign a Retold mob profile or take over the entity's daily-life AI. A
+compatibility pack may therefore classify a third-party mob without opting it into hunger,
+foraging, homes, or another profile. Unknown and untagged entity types remain unfactioned.
+
+Add an entity type exactly as for the block example, using the entity-type path. For example, this
+classifies `examplemod:ashen_guard` as an Undead entity:
+
+```json
+{
+ "replace": false,
+ "values": [
+ "examplemod:ashen_guard"
+ ]
+}
+```
+
+Save it as `data/retold/tags/entity_type/factions/undead.json`. Use `replace: false` so built-in and
+other compatibility-pack members remain available.
+
+An entity type must belong to at most one full `retold:factions/*` tag. If it appears in multiple
+full faction tags, Retold logs an error and treats it as unfactioned so datapack order cannot choose
+its diplomacy silently. If it is both a full member and an Illager loose ally, Retold logs a warning
+and full membership takes precedence. Classification caches and installed faction combat goals are
+updated after server tag reloads, including for already-loaded mobs.
+
+Players and a tamed Wolf that is actively defending are dynamic identities and are not represented
+by entity-type tags. Tamed entities covered by the standard Undead tag, such as Skeleton Horses,
+do not inherit generic hostile Undead behavior while tamed; their untamed forms do. Tags classify
+identity only: entity classes that cannot target, retaliate, assist, or use territory behavior do
+not gain those capabilities merely from membership.
+
+## Recipe Discovery Integrations
+
+Retold exposes one per-player recipe-visibility authority through
+`cz.xefensor.retold.api.recipe.RetoldRecipeKnowledge`. Vanilla's recipe book and optional recipe
+viewers must ask this authority instead of inferring discovery from their own recipe lists.
+Crafting, smelting, blasting, smoking, campfire cooking, stonecutting, and smithing recipes are
+managed by default and remain hidden until that player knows them. Knowledge is synchronized to
+the client on login, respawn, dimension change, and whenever a recipe is learned.
+
+Unknown third-party recipe types deliberately fail open: installing a machine mod does not make
+all of its recipes disappear merely because Retold does not understand how the machine teaches
+them. An integration that owns a reliable discovery route may register its `RecipeType` through
+`RetoldRecipeKnowledge.registerDiscoveryManagedType`, retain the returned registration for as long
+as the integration is active, and teach recipes through `teachAndUnlock`. The same registration
+must be present on the logical server and client. Closing it restores fail-open behavior.
+
+EMI and JEI adapters are not bundled yet. Their eventual adapters must filter through
+`RetoldRecipeKnowledge.isVisibleTo` and refresh when the synchronized knowledge snapshot changes;
+they must not create a separate discovery store or expose a managed recipe before vanilla would.
+Both viewers must remain optional dependencies.
+
+## World Protection Integrations
+
+`cz.xefensor.retold.api.world.RetoldWorldProtection` is Retold's generic permission layer for
+claim and protection addons. An addon registers one rule under its own unique identifier. Every
+registered rule must allow a mutation; any denial prevents it. With no registered rules, all
+checks allow the action and Retold behaves exactly as it did before this API existed. Closing a
+registration removes that rule. A rule that throws unexpectedly is logged and fails closed for
+that mutation so protected terrain is not damaged because an integration failed.
+
+Rules receive the server level, an exact representative block position, inclusive affected block
+bounds, mutation category, optional responsible entity, and optional subject identifier. Single
+block actions use one-block bounds; portals and whole-chunk operations expose their full possible
+area so an adapter can reject overlaps rather than checking only the center. Current categories
+distinguish mob breaking and placement, other entity breaking, Aender portal creation,
+delayed-structure retrogen, Aender chunk regeneration, and generic Retold world changes. Retold
+currently routes the following owned operations through this layer:
+
+- destructive animal forage, Panda bamboo consumption, weak-barrier breaking, Spider lair webs,
+ and Villager torch maintenance
+- Gale Core terrain damage
+- Aender stale-chunk blanking and regeneration
+- generated Aender counterpart portals and player-built portal activation
+- delayed structure retrogen
+
+The normal NeoForge entity-griefing hook and `mobGriefing` rule still apply before Retold's
+position-aware mob checks. Protection rules supplement those standard checks; they do not replace
+them. Concrete claim-mod adapters and their dedicated-server/multiplayer test matrix remain future
+work.
+
+## Public Java API Boundary
+
+Only types below `cz.xefensor.retold.api` are public integration contracts. The initial supported
+surface contains recipe knowledge/visibility and world-mutation protection. Other Retold packages
+remain implementation details and may change during ordinary development; addons must not edit
+Retold saved data or call internal managers directly.
+
+Public API additions should remain source-compatible within a Retold minor release line. A
+breaking public-API change requires an explicitly documented version transition and changelog
+entry. Query methods do not expose mutable storage, registrations are removable handles, and
+optional integrations must isolate references to absent third-party classes so Retold can always
+start without them.
+
+## Current Boundaries
+
+Faction tags do not add Retold mob profiles to third-party entities. Documentation and explicit
+opt-out support for modded-mob profiles, concrete EMI/JEI adapters, and claim-mod-specific
+world-protection adapters remain planned work. Stage, faction, mob-profile, and Aender-specific
+Java API surfaces are still under evaluation and are not public contracts yet.
+
+The exact item/block audit intentionally retains checks that define a fixed progression currency
+or ritual item, a specific tool tier, a lit/extinguished counterpart mapping, a portal or structure
+invariant, or a block with unique replacement/loot behavior. Sniffer diggable terrain also remains
+exact for now: a tag-backed attempt preserved registry membership but made the existing navigation
+survival test unreliable, so Retold will not expose that extension point until its behavior can be
+verified without weakening the regression test.
+
+Report reproducible compatibility failures through Retold's normal support channels. Include both
+mod versions, the relevant datapacks, whether the failure also occurs with only Retold installed,
+and dedicated-server details when applicable.
diff --git a/docs/internal/design_implementation_status.md b/docs/internal/design_implementation_status.md
index b049b54..cd999ac 100644
--- a/docs/internal/design_implementation_status.md
+++ b/docs/internal/design_implementation_status.md
@@ -188,6 +188,7 @@ Largest missing or partial design areas:
| Design item | Status | Current implementation |
| --- | --- | --- |
| Remove or heavily restrict recipe book | Partial / implemented foundation | `ServerRecipeBookMixin`, `RetoldKnownRecipeData`, recipe unlock context. |
+| Shared recipe-viewer discovery authority | Implemented core / viewer adapters pending | `api.recipe.RetoldRecipeKnowledge` exposes the same per-player visibility used by the vanilla recipe-book mixin. The seven existing crafting/cooking/stonecutting/smithing types are managed by default, the complete known-id snapshot synchronizes to clients, unknown custom types fail open, and optional integrations can register a custom type plus teach it through the official facade. Two focused GameTests cover per-player isolation, vanilla unlock preservation, payload round trips, fail-open behavior, and removable custom-type registration. EMI and JEI adapters plus their in-game client refresh checks are not implemented yet. |
| Hide advancements / reduce guide-like UI | Partial | `AdvancementVisibilityEvaluatorMixin`, data overrides. |
| In-world recipe learning | Partial | Villager teaching system implemented. Other discovery methods are not. |
| Gamerule to bring recipe book back | Not planned | Developer clarified this should not be added. |
@@ -203,7 +204,8 @@ Largest missing or partial design areas:
| Design item | Status | Current implementation |
| --- | --- | --- |
-| Delayed Stage 2 structures | Implemented foundation | `RetoldDelayedStructureRetrogen`, attachments, tags. |
+| Generic Retold world-protection layer | Implemented core / adapters pending | `api.world.RetoldWorldProtection` combines removable, uniquely identified deny rules with immutable mutation contexts and preserves default-allow standalone behavior. Position-aware destructive forage, weak barriers, Panda bamboo, Spider webs, Villager torch maintenance, Gale Core terrain damage, Aender regeneration and portal creation, and delayed retrogen are routed through it. A focused test proves denial leaves forage and hunger unchanged, category separation, duplicate-id rejection, registration removal, and restoration of the original allowed path. No third-party claim adapter, multiplayer, or dedicated-server integration has been verified. |
+| Delayed Stage 2 structures | Implemented foundation | `RetoldDelayedStructureRetrogen`, attachments, tags. Retrogen now asks the generic world-protection API before writing each chunk part; no concrete claim-mod adapter has been tested. |
| Mansions delayed to Stage 2 | Implemented / needs verification | `delayed_until_stage_2` includes `minecraft:mansion`. |
| Pillager outposts delayed to Stage 2 | Implemented / needs verification | Tag includes `minecraft:pillager_outpost`. |
| Mansion size/light redesign | Not implemented |
@@ -243,8 +245,8 @@ Largest missing or partial design areas:
| Herd and school ecology | Partial / implemented core, developer in-game verified | `RetoldAnimalSocialGroups` now gives Cows and Mooshrooms one bovine range group, retains the mixed equine and llama groups, and keeps other ordinary land herds species-specific. Cod, Salmon, Tropical Fish, and Pufferfish use data-driven `AQUATIC_SCHOOL` profiles; isolated members acquire low-priority `AQUATIC_SCHOOL`/`REGROUP` ownership, use cached exact-species scans, and require a real reachable aquatic path toward the school center. Squid and Glow Squid use `LOOSE_AQUATIC_GROUP` profiles and share successful-damage panic only with their exact species through one bounded cached broadcast plus the existing receiver-side fallback. GameTests cover group compatibility and a persisted mixed bovine range, exact-species fish routing, cross-species exclusion, and Squid/Glow Squid panic isolation. On 2026-08-03, the developer reported that the ordered natural acceptance pass works, including the land-group boundaries, exact-species schools, mixed-fish panic, exact-species Squid panic, bucketed fish, crowding, vertical water obstacles, and route recovery. This does not establish multiplayer, dedicated-server, long-session, or existing-world behavior. Fish diets, seagrass/kelp consumption, Squid hunger, migration, and the player-defined domesticated enclosure/range mechanism remain unspecified or unimplemented and must not be inferred from these profiles. |
| Hunger-satisfaction animal breeding | Implemented / representative developer in-game verified | All 26 current vanilla breedable entity types are selected through `retold:automatic_breeders`. Player and Villager breeding food now relieves hunger instead of immediately entering love mode. An adult that remains in the `FULL` hunger stage without panic, damage, or an active target for 6,000 loaded ticks becomes ready; two compatible ready animals within eight blocks are armed for vanilla mating, with a one-minute retry after a miss. Vanilla still owns movement, genetics, tame ownership, mixed Horse/Donkey births, Turtle eggs, Frog pregnancy, and special offspring behavior. A successful birth adds 40 hunger to each parent and retains the vanilla age cooldown. Persisted accumulated satisfaction is capped to the current loaded dispatcher interval after a gap, so unloaded time cannot complete readiness; retry and armed state also persist. Four focused GameTests cover all tag/profile entries, final-item player feeding, hunger gating, interruption, persistence including an unloaded gap, retries, special compatibility, actual birth, and parent cost. On 2026-08-04, the developer confirmed automatic breeding in-game using a temporary 10-second readiness gate; the confirmed five-minute production value was then restored. Natural every-species, the full five-minute wait, crowded pens, multiplayer, dedicated-server, and existing-world behavior remain unverified. |
| Loaded starvation for hunger-aware mobs | Implemented / needs in-game verification | `RetoldStarvationBehavior` applies one point of starvation damage on every species-specific hunger interval that reaches or remains at 100. The ordinary `PathfinderMob`, separate non-pathfinding Bat, and Villager communal-food hunger owners all use it; profiles with a zero hunger interval remain excluded. There is no health floor or loaded-world exception for named, tamed, hostile, or Villager mobs, so continued starvation can kill them. Feeding below 100 stops future pulses. Slimes and Magma Cubes retain their existing critical split/size-one death rule instead of also taking generic damage. Two focused GameTests assert that every loaded positive-hunger profile has an owner and cover all three runtime paths, terminal death, and a no-hunger exclusion; the existing Cube Mob starvation test passes. Cow, Bat, and Villager TPS checks pass all 15 phases below 50 ms/tick, peaking at 6.852, 11.856, and 10.129 ms/tick. Natural timing, death drops, feeding recovery, named/tamed behavior, Villagers, hostile hunger profiles, multiplayer, dedicated servers, long sessions, and existing worlds remain unverified. |
-| Natural loaded feeding viability for every hunger profile | Implemented / automated habitat matrix, needs natural verification | The patched 26.2 spawn-biome definitions and all 40 positive-hunger profiles were audited together. `RetoldHungerSurvivalGameTests` now gives each species a representative production habitat source or living prey instead of using dropped items as an ecology substitute; its 41st registry guard prevents uncovered positive-hunger profiles. Added routes cover renewable dead-bush scrub for Camels/desert Rabbits while retaining cactus as a hazard, renewable alpine scraping for Goats, renewable mycelium grazing for Mooshrooms, red-sand/terracotta grubs for Armadillos, Nether mushrooms for Piglins, Frog prey for Cats, jungle Chicken prey for Ocelots, Bat prey for cave Spiders, abstract nighttime cave insects for Bats, caravan sustenance for linked Trader Llamas, and independent nearby-prey acquisition for hungry Slimes/Magma Cubes. Existing aquatic, flower, crop, soil, bamboo, prey-kill, lava, and Villager-storage routes remain. Environmental forage is non-destructive, works with `mobGriefing=false`, and has a 30-second repeat-use cooldown. A bounded weak fair-claim queue prevents deterministic starvation under the unchanged eight-per-tick shared block-search cap; deferred foragers hold position rather than wandering away before admission. Two final different-order focused matrix runs pass 41/41; affected-path Bat, Camel, Rabbit, Slime, and Magma Cube TPS checks pass all 25 phases below 50 ms/tick, with a 10.061 ms/tick overall peak. This proves one loaded meal in representative spawn contexts, not indefinite population balance, every generated terrain arrangement, multiplayer, dedicated-server, existing-world, or unloaded survival. |
-| Confirmed mob/faction behavior contract | Partial | The detailed contract is recorded in `retold_mob_ai_system.md`. Existing profiles and behavior systems cover portions of it, but most species rules and the unloaded ecosystem simulation remain partial or missing. |
+| Natural loaded feeding viability for every hunger profile | Implemented / automated habitat matrix, needs natural verification | The patched 26.2 spawn-biome definitions and all 40 positive-hunger profiles were audited together. `RetoldHungerSurvivalGameTests` now gives each species a representative production habitat source or living prey instead of using dropped items as an ecology substitute; its 41st registry guard prevents uncovered positive-hunger profiles. Added routes cover renewable dead-bush scrub for Camels/desert Rabbits while retaining cactus as a hazard, renewable alpine scraping for Goats, renewable mycelium grazing for Mooshrooms, red-sand/terracotta grubs for Armadillos, Nether mushrooms for Piglins, Frog prey for Cats, jungle Chicken prey for Ocelots, Bat prey for cave Spiders, abstract nighttime cave insects for Bats, caravan sustenance for linked Trader Llamas, and independent nearby-prey acquisition for hungry Slimes/Magma Cubes. Existing aquatic, flower, crop, soil, bamboo, prey-kill, lava, and Villager-storage routes remain. Environmental forage is non-destructive, works with `mobGriefing=false`, and has a 30-second repeat-use cooldown. Ordinary forage and dropped-food classifications are now exposed through behavior-preserving Retold tags, composed with exact-match vanilla/Common food tags where available; former registry-path checks remain fallbacks. A bounded weak fair-claim queue prevents deterministic starvation under the unchanged eight-per-tick shared block-search cap; deferred foragers hold position rather than wandering away before admission. Two final different-order focused matrix runs pass 41/41; affected-path Bat, Camel, Rabbit, Slime, and Magma Cube TPS checks pass all 25 phases below 50 ms/tick, with a 10.061 ms/tick overall peak. This proves one loaded meal in representative spawn contexts, not indefinite population balance, every generated terrain arrangement, multiplayer, dedicated-server, existing-world, or unloaded survival. |
+| Confirmed mob/faction behavior contract | Partial | The detailed contract is recorded in `retold_mob_ai_system.md`. Static faction identity is now supplied by additive entity-type tags with exact default coverage, standard Illager/Undead composition, fail-closed conflicts, a separate loose-Illager-alliance tag, reload-aware classification/goals, and focused targeting/retaliation tests. Faction and datapack profile selection remain independent. Existing profiles and behavior systems cover portions of the larger contract, but most species rules and the unloaded ecosystem simulation remain partial or missing. |
| Independent TPS coverage for every profiled mob | Implemented / automated verification | `RetoldPerMobTpsGameTests` registers 77 isolated tests with 50 subjects each and measures five 80-tick behavior phases: idle/rest, dropped food/forage, hunt/target, danger/social, and habitat/day-night. Appropriate habitat, prey, threat, and species stimuli are supplied per fixture. The original 68-profile clean baseline passed all 340 phases and exposed the fixed Sniffer nested block scan. Six aquatic ecology profiles grew the matrix to 74 tests/370 phases, and Villager to 75/375; that complete 2026-08-04 baseline passed every phase below 50 ms/tick. Strider and Nautilus now bring registration to 77/385; their focused ten phases and a representative Cow's five phases pass below 50 ms/tick. The complete 77-profile matrix was not rerun because the maintained focused-selection policy did not justify repeating 74 unaffected species. See `mob_tps_benchmark.md` for commands, earlier baselines, and limitations. |
| Active food search and path-backed deliberate movement | Partial / implemented broadly, developer in-game verified | Hungry managed ground mobs now use a shared `FOOD`/`SEARCH` owner to travel between bounded reachable search points when immediate dropped food or forage is absent; existing predator and specialized search owners remain responsible for their profiles. Shared ground movement reports and caches only navigation requests that actually started. Bats use bounded vanilla 3D paths for colony return, food, search, hunting, evasion, and panic. Exact-species fish cohesion now uses ordinary aquatic navigation, and its contention-aware GameTest retries budget backpressure while still requiring owned `REGROUP` plus a reachable path. Focused GameTests cover a ground forager path, Bat food search and wall detours, and Cod school routing. On 2026-08-03, the developer reported that the ordered natural movement audit works across aquatic mobs, non-Bat flying mobs, large Cube Mobs, crowded groups, vertical terrain, barriers, and movement-ownership release. This remains an in-game report rather than multiplayer, dedicated-server, profiler, long-session, existing-world, or exhaustive automated path coverage. |
| Dropped food preferred over ordinary prey hunting | Implemented / needs in-game verification | `RetoldFoodBehaviorEvents` now lets hunger-driven profiles consider valid dropped food while under ordinary `HUNT` control. Its higher-priority `FOOD`/`FEED` claim clears the prey target, chase sprint, navigation, and pending predator strike before pursuing the item. Retaliation and territory-attack ownership are explicitly protected, while higher-priority flee, defense, special combat, and territory control cannot be replaced. Slimes and Magma Cubes are the explicit appetite exception: `RetoldMobRules.wantsDroppedFood` remains true even at zero hunger, so both the shared food owner and longer-range swarm scavenger continue seeking items without enabling living-prey combat. Because Cube Mobs ignore ordinary path coordinates, `RetoldCubeMobMovement` now converts those shared movement requests into controller-facing and hop-speed commands while owned movement suppresses vanilla random heading changes. Deterministic GameTests cover an ordinary Wolf hunt, fed Slime item appetite and consumption, fed Slime target rejection, the hungry Slime swarm-hunt profile, shared food ownership, sprint cleanup, retained retaliation, and actual horizontal Slime movement toward a distant dropped item through the shared movement owner. Natural pack contention for one item, aquatic navigation, and transitions across all profile families still need focused in-game verification. |
@@ -264,7 +266,7 @@ Largest missing or partial design areas:
| Skeletons as ranged undead | Implemented / partial | `UNDEAD_TOLERANT`, `RetoldSkeletonRangedEvents`. |
| Golems as magical defenders | Partial | Iron/snow golem defender/guard support exists. Stage 2+ Villagers now visibly construct eligible Iron Golems under vanilla's village eligibility rules, spending one emerald, and player-built Iron Golems cost five experience levels on successful animation. Copper/tuff golem behavior remains incomplete or unavailable. |
| Elementals: blaze/breeze/wildfire | Partial | Blaze territory guard; breeze special vanilla; wildfire missing. |
-| Villagers pacifist / communal food / golem creators | Partial / communal food, livestock tending, and golem construction implemented | `VILLAGER_COMMUNAL` gives loaded Villagers persisted hunger. Hungry Villagers first consume their highest-value carried vanilla food. Only an empty personal food supply triggers cached, LOD-aware, budgeted discovery of accessible village chests/barrels; at the supported side they transfer up to 12 food points, consume one item, and retain the remainder. Adult Farmers deposit only food above a 24-point reserve. Shepherds, Leatherworkers, and Butchers use the same bounded dispatcher/storage architecture to retrieve and consume two suitable items, path to their assigned valid adult livestock pair, and relieve both animals' hunger; global satisfaction breeding decides reproduction later. Hunger, danger, sleep, trading, ownership, and higher-priority work win. At Stage 2+, an otherwise vanilla-eligible five-Villager golem decision becomes a persisted, path-backed staged build that spends one emerald, but only Clerics, Librarians, Armorers, Toolsmiths, and Weaponsmiths may build it. Players spend five levels only on successful Survival Iron Golem animation. Entity/container/construction/animal state saves normally, but unloaded-time simulation is deliberately excluded. Seven communal-food, four animal, five golem, and three torch-maintenance tests pass; the latest focused 50-Villager TPS peak is 6.864 ms/tick. Daily stock refresh and all-stage magical/Nitwit-physical torch maintenance also exist; wider society/progression remains incomplete. The Animal Feeder remains animal-only. |
+| Villagers pacifist / communal food / golem creators | Partial / communal food, livestock tending, and golem construction implemented | `VILLAGER_COMMUNAL` gives loaded Villagers persisted hunger. Hungry Villagers first consume their highest-value carried vanilla food. Only an empty personal food supply triggers cached, LOD-aware, budgeted discovery of accessible village chests/barrels; at the supported side they transfer up to 12 food points, consume one item, and retain the remainder. Adult Farmers deposit only food above a 24-point reserve. Shepherds, Leatherworkers, and Butchers use the same bounded dispatcher/storage architecture to retrieve and consume two suitable items, path to their assigned valid adult livestock pair, and relieve both animals' hunger; global satisfaction breeding decides reproduction later. Hunger, danger, sleep, trading, ownership, and higher-priority work win. At Stage 2+, an otherwise vanilla-eligible five-Villager golem decision becomes a persisted, path-backed staged build that spends one emerald, but only Clerics, Librarians, Armorers, Toolsmiths, and Weaponsmiths may build it. Players spend five levels only on successful Survival Iron Golem animation. Entity/container/construction/animal state saves normally, but unloaded-time simulation is deliberately excluded. Seven communal-food, four animal, five golem, and three torch-maintenance tests pass; the staged fixture now trade-locks its test professions and explicitly exercises construction during an allowed activity. The latest focused 50-Villager TPS peak is 6.864 ms/tick. Daily stock refresh and all-stage magical/Nitwit-physical torch maintenance also exist; wider society/progression remains incomplete. The Animal Feeder remains animal-only. |
| Village property ownership and reputation | Implemented / needs natural verification | Generated and Villager-produced storage quantities, Farmer-planted crops, and profession-tended livestock use provenance rather than village-location inference. Player deposits/crops, player-handled livestock, tamed animals, and ambiguous existing-world property remain outside initial ownership. Offspring of two owned parents inherit village ownership; automatic offspring of a player-associated unowned parent inherit player protection instead. Witnessed Survival offenses use vanilla gossip: storage/crop theft is minor, vandalism and direct village-livestock killing are `-50`, and breaking protected storage reaches golem hostility. Monsters, environment, Creative, and Spectator are excluded. Container, crop, and animal focused selectors pass; `/retold village status` reports the bounded loaded standing. Natural livestock routes, witness/trade/golem consequences, multiplayer, dedicated servers, and existing worlds remain unverified. |
| Witches as exiled villagers, raid-only loose ally | Implemented / needs verification | Witches are not full Illager or territory members and remain mutually neutral with Illagers outside raids. Combat alignment requires an active raid, cooperation requires the same raid instance, and Retold-owned assist targets are cleared on raid exit. A deterministic GameTest covers these boundaries; verify natural Stage 3 raid wave participation and healing/support behavior in-game. |
| Four-structure territory warning contract | Implemented / needs verification | Tags cover bastions, Nether fortresses, pillager outposts, and woodland mansions. GameTests cover every configured warning-capable faction member, Nether dimension restriction, Witch exclusion, survival-player observation, creative/spectator exclusion, suppression of premature targets, the minimum final-warning transition, territory target ownership, and immediate retaliation. Actual structure detection, formation movement, sounds/particles, and navigation still need focused in-game checks in all four generated structures. |
@@ -365,7 +367,8 @@ Largest missing or partial design areas:
| Stage progression | `RetoldStageManager`, `RetoldWorldData`, `RetoldEndProgressionEvents`, `RetoldStageRuntimeEvents` |
| Aender | `RetoldAenderAccess`, `AenderChunkGenerator`, `AenderVolatility`, `AenderStabilityData`, `AenderStabilizerEvents` |
| Dragon egg elements | `RetoldEndProgressionEvents`, `RetoldElementType`, `WaterElementItem`, `RetoldRitualEffects` |
-| Recipe knowledge | `RetoldKnownRecipeData`, `RetoldRecipeBookEvents`, `ServerRecipeBookMixin`, `AbstractFurnaceBlockEntityMixin` |
+| Recipe knowledge | `api.recipe.RetoldRecipeKnowledge`, `RetoldKnownRecipeData`, `RetoldRecipeVisibility`, `RetoldRecipeBookEvents`, `ServerRecipeBookMixin`, `AbstractFurnaceBlockEntityMixin` |
+| Compatibility API | `api.recipe.RetoldRecipeKnowledge`, `api.world.RetoldWorldProtection`, `RetoldWorldMutationContext` |
| Villager teaching | `RetoldVillagerTeaching`, `RetoldVillagerTeachingReloadListener`, `data/retold/villager_teaching` |
| Delayed structures | `RetoldDelayedStructureRetrogen`, `RetoldChunkStructureData`, `RetoldDelayedStructureHelper`, delayed structure tags |
| AI | `RetoldBehaviorEntityTickDispatcher`, `RetoldMobProfiles`, `RetoldMobProfileReloadListener`, `data/retold/mob_profiles`, `RetoldAiControl`, `RetoldBehaviorPerf` |
diff --git a/docs/internal/retold_design_risks.md b/docs/internal/retold_design_risks.md
index c1b7afb..9c58779 100644
--- a/docs/internal/retold_design_risks.md
+++ b/docs/internal/retold_design_risks.md
@@ -17,6 +17,9 @@
| Worldgen | Strongholds should be limited to 3. | Not confirmed implemented. |
| Mobs | Survival-only mob removals can drift when Minecraft adds or changes acquisition paths. | Sniffer survival acquisition is removed by overriding warm-ocean-ruin archaeology without its egg. Endermite survival spawning is blocked through the normal spawn-position event and the separate direct Ender Pearl creation hook. GameTests cover both policies while command/Creative entities remain functional, and the developer confirmed the Endermite behavior in-game on 2026-08-02. Recompare the archaeology override and audit all Sniffer/Endermite acquisition paths whenever Minecraft changes them. |
| Mob AI contract | The confirmed design is substantially broader than the current 77-profile implementation. | Implement in dependency order and keep each family marked partial until its target, hunger, home, stage, territory, movement, and performance behavior is tested. Do not infer completion from the presence of a profile. Ground active-food search, Bat 3D search/detours, exact-species Cod school routing, and Villager communal-store routing have isolated path coverage. A synthetic isolated test covers 256 always-ticking managed animals for 200 server ticks and guards AI work budgets/cache use. The latest complete baseline covers 75 tests/375 phases below 50 ms/tick; focused Strider and Nautilus tests cover the two new profiles, but the complete 77-test/385-phase matrix has not been rerun. Natural mixed-hostile populations, multiple players, long sessions, dedicated-server profiler evidence, existing-world behavior, and exhaustive automated per-species path coverage remain unverified. |
+| Modded faction members | A compatibility datapack can give a third-party entity Retold diplomacy, but its class may not support every targeting, retaliation, raid, or territory behavior associated with that identity. | Faction tags do not assign profiles or manufacture missing AI capabilities. Unknown untagged mobs remain untouched, conflicting full faction tags fail closed, full membership suppresses loose Illager alignment, and loaded-mob goals follow tag reloads. The temporary external-pack smoke test covers extension, conflict, precedence, targeting, and retaliation with vanilla stand-ins; test representative real third-party mobs on a dedicated server before advertising a compatibility addon. |
+| Recipe-viewer discovery | A client recipe viewer can spoil progression if it maintains its own knowledge state, filters before the server snapshot arrives, or treats unknown machine recipes as managed without a teaching path. | `RetoldRecipeKnowledge` is the shared server/client authority, its complete per-player snapshot synchronizes on lifecycle changes and learning, and unknown custom types fail open until an integration explicitly registers them. Vanilla and payload-focused tests pass. EMI/JEI adapters, login/reconnect refresh timing, multiplayer isolation, and a real custom machine recipe remain unverified, so do not advertise viewer integration yet. |
+| World-protection integrations | A Retold action can cross a claim boundary, a protection callback can fail, or an Aender/retrogen retry can repeatedly encounter a permanent denial. | Mutation contexts include immutable inclusive bounds; portal and chunk operations expose their full possible area rather than only a center point. All installed rules must allow, exceptions fail closed, denied Aender work remains retryable, and no-rule defaults preserve current behavior. The focused API/forage test passes, but no actual claim adapter, protected portal boundary, denied Aender transition, delayed structure, multiplayer, or dedicated server has been verified. Add bounded backoff or permanent-denial coordination if a concrete adapter shows callback pressure. |
| Hunger-satisfaction breeding | Automatic loaded-world reproduction can overpopulate rich pens, create crowding around ready pairs, or behave unexpectedly after interruption/save-load. Special vanilla species also have nonstandard birth paths that Retold must not replace. | Readiness requires five continuous loaded minutes at `FULL` hunger and resets on hunger, panic, damage, or an active target. Mate scans are cached/budgeted within eight blocks and retry after one minute; successful parents gain 40 hunger and retain vanilla's age cooldown. Retold only arms vanilla mating, preserving Horse/Donkey compatibility, Turtle eggs, Frog pregnancy, genetics, and tame ownership. Four focused tests cover these contracts and state persistence. Naturally verify every species family, dense enclosures, population growth over multiple days, save/reload during readiness and mating, multiplayer feeding, dedicated servers, and existing worlds before tuning time, range, or cost. Unloaded time deliberately does not advance breeding. |
| Loaded starvation balance | Long-lived hungry populations can die, including named/tamed animals, Villagers, and hostile hunger profiles; unsuitable diets or inaccessible food could therefore make an environment harsher than intended. | The developer confirmed one damage point per species-specific hunger interval at 100 hunger for every active hunger profile, with no loaded-world exemption. Focused ordinary/Bat/Villager/Cube coverage and Cow/Bat/Villager TPS checks pass. A separate 40-species survival matrix now proves that every positive-hunger profile can complete at least one production meal in an isolated suitable loaded habitat; it does not establish long-term carrying capacity or realistic-terrain access. Naturally verify timing, repeated food acquisition, recovery, death drops, enclosures, villages, named/tamed mobs, hostile profiles, multiplayer, dedicated servers, long sessions, and existing worlds before tuning metabolism. The future unloaded simulation still protects named/tamed animals from offline death. |
| Herd and school ecology | The new profiles can be mistaken for completion of the broader diet, migration, domestication, and unloaded-ecology contract. | The implemented slice is limited to confirmed land social identities, exact-species fish cohesion, existing cross-fish panic, and exact-species Squid panic. On 2026-08-03, the developer reported that the ordered natural mixed-pen and water-terrain acceptance pass works, including crowding, vertical obstacles, bucket release, and route recovery. Multiplayer, dedicated-server, long-session, and existing-world behavior remain unverified. Fish diet assignments, seagrass/kelp consumption, Squid hunger, player-defined enclosure/range mechanics, natural migration under depletion, and unloaded simulation remain unimplemented or unspecified. |
diff --git a/docs/internal/retold_issues.md b/docs/internal/retold_issues.md
index 3e02e0c..0e107e1 100644
--- a/docs/internal/retold_issues.md
+++ b/docs/internal/retold_issues.md
@@ -20,8 +20,8 @@ When adding an issue, include:
| Aender portal | Ambient particles spawned partly below the horizontal portal plane and were occluded instead of being visible to a player standing beside it. | Developer screenshot and in-game report on 2026-07-22. | Spawn height moved above the rendered plane with upward/outward drift; needs client visual verification before moving to Resolved Issues. |
| Aenderite generation | Aenderite initially appeared not to generate because only about 3.6% of a 9,409-chunk real-sampler probe contained a planned vein. After an initial increase to 8.6%, the developer still could not find it during normal in-game inspection and approved matching diamond-like abundance. | Developer reports on 2026-07-22; the original probe found 341 ore-bearing chunks and 1,134 valid replacements. A full-pipeline `ProtoChunk` GameTest confirms planned Aenderite survives terrain and decoration generation. A read-only scan of the newest local test save (`New World (44)`) found Aenderite in 361 of 1,396 saved chunks whose palettes contain Aender Stone. | Diamond-style tuning now uses mostly 3–4 and 4–8 block veins with rare 8–12 block veins. The 9,409-chunk probe finds 2,917 ore-bearing chunks (31%) and 28,209 placeable blocks, approximately three per sampled chunk. Deterministic tests enforce the frequency, density, size, underside bias, and chunk-border behavior; fresh or legitimately regenerated Aender terrain still needs in-game verification. |
| Slime item pursuit | Slimes recognized and consumed nearby dropped items but did not visibly seek items outside consumption range because ordinary navigation requests do not drive the specialized Cube Mob movement controller. | Developer in-game report on 2026-08-02; the original tests covered appetite and direct consumption but not locomotion. | Fix implemented through a Cube Mob facing-and-hop adapter with owned-heading protection. All 54 GameTests pass, including actual horizontal movement toward a distant item; needs focused in-game verification before moving to Resolved Issues. |
-| Intermittent mob GameTest regressions | Complete-suite or grouped-fixture contention previously missed otherwise focused-green movement requirements. | Historical failures included exact feeder, Bat, Cube, fish-school, and Villager-container paths. The Villager consumer regression was test cross-contamination: neighboring GameTests placed valid food containers inside its real 16-block search radius. Its route fixture uses a real bell POI, retries production behavior across budget windows, clears temporary stores, and is vertically separated while still requiring physical arrival and exact consumption. The broad `retold:*communal_food*` grouping can still contend with that route fixture, so the maintained strategy uses three isolated owning selectors instead: consumer transactions pass 4/4, the exact consumer route passes 1/1, and Farmer supply passes 2/2. The last pre-supply complete run passed 160/160 in 3.504 minutes. The post-personal-stock 50-Villager TPS test passes with a peak of 8.459 ms/tick. | No current failing owning selector. Keep movement assertions under observation without weakening exact arrival, separation, supported destinations, or reachable paths; do not treat an artificial grouped-fixture timeout as permission to weaken behavior. |
-| Villager golem advancement | A player standing near a Villager-built Iron Golem received vanilla's player-summoning “Hired Help” advancement even though the Villager performed the construction. | Developer in-game report on 2026-08-04. Vanilla's carved-pumpkin animation awards the summoned-entity criterion to every player within five blocks without tracking who placed the structure. | Fix implemented with a try/finally-scoped Villager-animation marker and a narrow `SummonedEntityTriggerMixin` that suppresses only Iron Golem criteria during that call. Ordinary player construction remains outside the scope. The expanded focused golem selector passes 5/5; needs developer in-game confirmation before moving to Resolved Issues. |
+| Intermittent mob GameTest regressions | Complete-suite or grouped-fixture contention previously missed otherwise focused-green movement requirements. | Historical failures included exact feeder, Bat, Cube, fish-school, Villager-container, and staged Villager-golem paths. The Villager consumer regression was test cross-contamination: neighboring GameTests placed valid food containers inside its real 16-block search radius. Its route fixture uses a real bell POI, retries production behavior across budget windows, clears temporary stores, and is vertically separated while still requiring physical arrival and exact consumption. The broad `retold:*communal_food*` grouping can still contend with that route fixture, so the maintained strategy uses three isolated owning selectors instead: consumer transactions pass 4/4, the exact consumer route passes 1/1, and Farmer supply passes 2/2. The staged golem fixture assigned an untraded Cleric without a job site, allowing vanilla to reset it to `none`, and inherited whatever scheduled activity was active even though production correctly pauses outside IDLE/MEET/WORK. It now trade-locks test professions and explicitly selects IDLE while exercising staged mechanics. The focused golem selector passes 5/5 again. The last pre-supply complete run passed 160/160 in 3.504 minutes. The post-personal-stock 50-Villager TPS test passes with a peak of 8.459 ms/tick. | Keep movement assertions under observation without weakening exact arrival, separation, supported destinations, or reachable paths; do not treat an artificial grouped-fixture timeout as permission to weaken behavior. |
+| Villager golem advancement | A player standing near a Villager-built Iron Golem received vanilla's player-summoning “Hired Help” advancement even though the Villager performed the construction. | Developer in-game report on 2026-08-04. Vanilla's carved-pumpkin animation awards the summoned-entity criterion to every player within five blocks without tracking who placed the structure. | Fix implemented with a try/finally-scoped Villager-animation marker and a narrow `SummonedEntityTriggerMixin` that suppresses only Iron Golem criteria during that call. Ordinary player construction remains outside the scope. The expanded focused golem selector passes 5/5; developer in-game confirmation is still needed before moving this advancement issue to Resolved Issues. |
| Nitwit torch-relighting tool visual | A Nitwit's temporary Flint and Steel appeared for only one tick instead of remaining visible through the close relighting interaction. | Developer in-game report on 2026-08-04. Villager systems were dispatched every 20 ticks, while vanilla could clear the visual hand stack on the next entity tick. | Active Nitwit tool use now receives lightweight continuous dispatcher updates and reasserts the fake hand stack until the one-second interaction completes; idle discovery and routing remain on the normal cadence. The strengthened fake-tool regression passes within the 3/3 relighting selector and the Villager TPS test peaks at 7.102 ms/tick. Needs developer in-game visual confirmation before moving to Resolved Issues. |
| Hunger-profile survival integration | Registry-wide survival testing found hungry profiles whose tests supplied food they would not reliably acquire in their actual spawn habitats. The expanded habitat matrix also exposed deterministic shared block-search starvation: under 41 concurrent cases, late environmental browsers could miss every search window even though total work stayed within budget, then generic food search carried them away from the source before their turn. A cactus-based Camel fixture additionally proved unsafe because collision damage correctly triggered higher-priority fleeing. | The audit compared all 40 positive-hunger profiles with patched 26.2 spawn-biome definitions and production acquisition owners. Reproduction used `retold:hunger_survival_*`; focused cases passed alone while the grouped matrix repeatedly deferred environmental foragers. | Every profile now has a representative habitat route: safe renewable dead-bush scrub replaces deliberate cactus feeding, and the added badlands, alpine, Mushroom-Field, cave, Nether, caravan, and Cube-prey paths complement existing forage, hunt, aquatic, lava, and village-storage behavior. A weak fair-claim queue reserves capacity within the unchanged eight-start block-search cap so a live deferred claimant eventually runs; denied forage claimants hold position rather than beginning a random search until admitted. Two final different-order focused matrix runs pass 41/41, and the five affected/representative TPS tests pass all 25 phases below 50 ms/tick with a 10.061 ms/tick peak. Needs representative natural-world and long-term survival verification before moving to Resolved Issues. |
| Feeding during urgent flight | Sheep could visibly continue vanilla grass eating while Retold was already making them run from Wolves, because the long-running vanilla `EatBlockGoal` did not participate in Retold ownership. Species-specific meal callbacks also relied on their earlier dispatcher checks instead of rechecking priority at the final transaction. | Developer in-game report on 2026-08-05. The exact regression starts vanilla Sheep grazing, gives the Sheep `FLEE` ownership, and probes ordinary forage and dropped Wheat during the same flight. | Fix implemented with a narrow vanilla goal guard plus a shared transaction-time meal check across ordinary and specialized feeding paths. The exact unthrottled GameTest passes 1/1: grazing stops, grass/Wheat/hunger remain unchanged during flight, and one-item feeding resumes after flight ends. Needs developer in-game confirmation with Sheep naturally fleeing Wolves before moving to Resolved Issues. |
diff --git a/docs/internal/retold_mob_ai_system.md b/docs/internal/retold_mob_ai_system.md
index eac8214..88c696f 100644
--- a/docs/internal/retold_mob_ai_system.md
+++ b/docs/internal/retold_mob_ai_system.md
@@ -125,12 +125,23 @@ Technical owners:
Main faction design:
+- Static faction classification is data-driven through one additive `retold:factions/*`
+ entity-type tag per fixed Retold faction. `minecraft:illager` and `minecraft:undead` are composed
+ where their meanings match; Ghasts remain an explicit Retold Undead addition.
+- An entity type in multiple full faction tags fails closed as unfactioned and logs an error. Full
+ membership takes precedence over `retold:alliances/illager_loose_allies` with a warning.
+- Classification is cached by entity type and invalidated after server tag reload. Faction target
+ and retaliation goals are added or removed for loaded mobs as their effective membership changes.
+- Faction membership is independent from mob profiles: it supplies diplomacy, assist, retaliation,
+ and territory identity without assigning daily-life behavior to an unknown third-party mob.
- Nether Remnants: piglins, piglin brutes, blazes
- Illagers: pillagers, vindicators, evokers, illusioners, ravagers, vexes
- Witch: permanent loose-ally identity, active Illager combat alignment only during a raid, and
cooperation only with members of that same raid; never a full faction or territory member
- Village defenders: iron golems, snow golems, dynamically defending tamed wolves
-- Undead: not one cozy social faction; zombies horde, skeletons tolerate undead, wither skeletons guard fortress
+- Undead: not one cozy social faction; standard `minecraft:undead` types plus Ghasts share diplomacy,
+ zombies horde, skeletons tolerate undead, and Wither Skeletons guard fortresses. Tamed undead
+ mounts do not inherit the generic hostile identity while tamed.
- Ocean Monument: guardians and elder guardians defend monument purpose
### State
@@ -552,8 +563,10 @@ while remaining invalid prey. This is a one-meal viability matrix, not evidence
balance, every generated terrain arrangement, unloaded simulation, multiplayer, or existing worlds.
Spawn-habitat fallbacks fill the contexts where an ordinary diet is not naturally placed nearby.
-Camels and desert Rabbits browse dead-bush scrub while cactus remains a hazard; Goats non-destructively scrape stone, snow, packed ice, or
-gravel; Mooshrooms non-destructively graze mycelium; and Armadillos find abstract grubs in red sand
+Camels and desert Rabbits browse blocks in `retold:desert_browse_blocks`, with Dead Bush as the
+default while cactus remains a hazard. Goats non-destructively scrape blocks in
+`retold:goat_scrape_blocks`, defaulting to stone, snow, packed ice, and gravel; Mooshrooms
+non-destructively graze `retold:mooshroom_grazing_blocks`, defaulting to mycelium; and Armadillos find abstract grubs in red sand
and terracotta as well as ordinary soil. Piglins can consume red/brown mushrooms or Crimson Fungus.
Cats admit Frogs as wetland prey, Ocelots retain jungle Chicken prey, and Spiders/Cave Spiders admit Bats. A hungry nighttime
Bat that finds neither a dropped Spider Eye nor physical prey catches abstract cave insects, and a
@@ -564,6 +577,13 @@ target exists. Habitat forage is renewable and non-destructive, remains availabl
Renewable habitat forage uses the existing six-block horizontal scan volume so a staggered first
food tick does not lose a nearby source after ordinary vanilla wandering; destructive forage keeps
the narrower four-block scoring radius.
+Ordinary crop, flower, grazer-plant, small-passive-plant, Turtle, Hoglin, Piglin, and Strider forage
+eligibility is exposed through Retold-owned block tags. Meat, fish, berry, grazer, small-passive,
+flower, Nether-fungus, Bat, and feline-scavenge dropped-food families are exposed through item
+tags, nesting exact-match vanilla/Common tags where available. Existing registry-path checks remain
+as compatibility fallbacks. Tagged ordinary forage is still destroyed and requires `mobGriefing`;
+tagging an item or block changes classification only and does not bypass AI ownership, bounded
+searches, feeding timing, or hunger rules.
Living-death integration credits valid ordinary predator and wild Nautilus prey through
`RetoldControlledHuntingEvents` and credits vanilla as well as Retold lethal Frog/Axolotl attacks
diff --git a/docs/internal/retold_mod_system.md b/docs/internal/retold_mod_system.md
index 227f733..aeebcfa 100644
--- a/docs/internal/retold_mod_system.md
+++ b/docs/internal/retold_mod_system.md
@@ -145,15 +145,17 @@ to every pre-Steel material while allowing Steel and later tiers to harvest them
registered through `RetoldLootModifiers`, gives
every `minecraft:leaves` block a supplemental 20% roll for 1–2 Sticks, increasing five percentage
points per Fortune level. It also normalizes Dead Bushes to 2–4 Sticks and gives tagged living
-bushes a 10% one-Stick roll, while excluding shears and Silk Touch. `RetoldBlocks` registers the
+bushes a 10% one-Stick roll, while excluding Silk Touch and tools in the Retold-owned
+`leaf_preserving_tools` item tag. `RetoldBlocks` registers the
Flint Multi-tool, Flint Spear, Steel Spear, and provisional Steel tool/armor materials, while block/item tags own mining, repair,
enchantment-family, and equipment boundaries. Vanilla placed-feature overrides reduce ordinary
and Dripstone Copper from sixteen to six attempts per chunk while retaining vanilla vein sizes and
height distribution; only newly generated chunks receive the reduced distribution.
`CampfirePlacementMixin` is a narrow initial-state hook that delegates to
`RetoldCampfireProgressionEvents`, making Campfires unlit on both logical sides before placement can
-render. The event class owns consumable bare-Flint ignition, while vanilla Flint and Steel remains
-the durable alternative. Recipes remove Coal from
+render. The event class owns ignition by items in the Retold-owned
+`campfire_consumable_igniters` tag, consuming one in Survival; bare Flint is the default, while
+vanilla Flint and Steel remains the durable alternative. Recipes remove Coal from
the three-Stick/three-Log Campfire, fire Clay Balls into Bricks through campfire cooking, repurpose
the vanilla Smoker as the eight-Brick Brick Furnace, add its Copper/Charcoal processing, blast Iron
Ingots directly into Steel, and craft the full standard Steel tool and armor sets. These rules are
@@ -696,8 +698,11 @@ Retold tracks recipe knowledge separately from vanilla automatic unlock assumpti
Technical owners:
+- `api.recipe.RetoldRecipeKnowledge`
- `RetoldKnownRecipeData`
- `RetoldRecipeBookEvents`
+- `RetoldRecipeVisibility`
+- `RetoldRecipeKnowledgeSyncPayload`
- `RetoldRecipeUnlockContext`
- `RetoldRecipeResultHelper`
- `RetoldCookingRecipeSiblingHelper`
@@ -715,12 +720,38 @@ Behavior:
- Stonecutting and smithing are tracked by result where direct input matching is not enough.
- Cooking siblings can be unlocked together.
- Internal recipe unlocks are wrapped with `RetoldRecipeUnlockContext`.
+- The seven vanilla recipe types Retold already discovers are managed by one visibility authority.
+- Unknown custom recipe types fail open unless an integration explicitly registers that type.
+- The complete known-id snapshot is synchronized to the client on login, dimension change,
+ respawn, and successful learning so client recipe viewers can use the server authority.
- Vanilla recipe book behavior is intercepted through mixins.
- Many vanilla recipe advancements are included as data resources with reward/visibility changes.
Design rule:
-Use `RetoldRecipeBookEvents.markKnownAndUnlockRecipe` or related helpers when teaching/unlocking recipes. Do not directly add recipe book entries from feature code.
+Feature code inside Retold should use `RetoldRecipeBookEvents.markKnownAndUnlockRecipe` or related
+helpers when teaching/unlocking recipes. External integrations use the stable
+`RetoldRecipeKnowledge` facade. Neither should directly add recipe-book entries or maintain a
+parallel discovery store.
+
+## World Mutation Protection
+
+Retold-owned world mutations use the default-allow `api.world.RetoldWorldProtection` facade.
+Protection integrations install uniquely identified `RetoldWorldProtectionRule` instances and
+retain their removable registration handles. Every installed rule must allow the immutable
+context. Unexpected rule failures are logged and deny only the checked mutation.
+
+The context exposes the server level, representative position, immutable inclusive affected bounds,
+mutation type, optional responsible entity, and optional subject id. Single-block actions use exact
+one-block bounds; portal and chunk operations expose their complete possible area. Current routed
+owners are destructive forage and bamboo, weak barriers, Spider webs, Villager torch maintenance,
+Gale Core breaking, Aender stale-chunk replacement, Aender portal creation, and delayed
+structure retrogen. `RetoldMobGriefing` continues to invoke NeoForge's normal entity-griefing hook
+first, then the position-aware Retold rule immediately before destructive or placement actions.
+
+Do not call a claim mod from gameplay classes. Add an optional adapter that translates a
+`RetoldWorldMutationContext` into that mod's permission query. No adapter may be required for Retold
+startup, and no registered rules must retain the exact pre-API behavior.
## Villager Teaching
@@ -998,6 +1029,10 @@ High-level systems:
- profile-based mob life behavior
- faction relationships, including separate permanent identity, active combat alignment, and
same-context cooperation for conditional allies such as raiding witches
+- tag-backed faction classification through additive `retold:factions/*` entity-type tags, with
+ fail-closed conflict handling, reload-aware cached membership, and a separate
+ `retold:alliances/illager_loose_allies` extension point; faction identity remains independent of
+ datapack mob profiles
- separate neutral Silverfish and Endermite identities, with same-entity-type enforcement in the
small-arthropod swarm owner so their shared profile cannot create cross-species coordination
- owned Polar Bear cub defense with a cancellable standing/sound warning before proactive attack
@@ -1020,6 +1055,16 @@ High-level systems:
- invalid player target cleanup
- AI performance scheduling, caches, LOD, and budgets
- hunger-driven weak-barrier breaching through a data tag, shared griefing policy, and AI ownership
+- Retold-owned extension tags for Armadillo grub/scrub terrain, Panda bamboo, Turtle beaches,
+ Nether-remnant/Ocean-Monument guard anchors, consumable Campfire igniters, and tools that
+ preserve leaves and woody bushes from supplemental Stick drops
+- Retold-owned renewable forage tags for Camel/Rabbit desert browse, Goat scraping, and Mooshroom
+ grazing; these retain bounded searches, per-mob cooldowns, and non-destructive behavior
+- tag-driven ordinary forage crops/flowers/plants and dropped-food families, with vanilla/Common
+ tag composition where memberships preserve Retold's current defaults; ordinary forage remains
+ destructive and `mobGriefing`-gated
+- tag-driven Spider-lair web counting and Illager village-signal blocks; Spiders still place the
+ exact vanilla Cobweb block
Territory escalation is an explicit state machine owned by `RetoldTerritoryStateMachine`:
@@ -1149,6 +1194,8 @@ Behavior:
- Blocked hits apply feedback, knockback/bounce, and mining fatigue.
- Elder guardians guarantee a `water_element` drop if one is not already dropping.
- Guardians pressure players mining protected monument blocks.
+- Protected monument materials are selected through `retold:ocean_monument_protected_blocks`;
+ compatibility packs may append equivalent blocks without patching Guardian code.
- Guardian beam pressure can block mining and alert nearby guardians.
- Ordinary Guardians are not Axolotl prey. A Guardian attack may create a retaliation-owned target
for the victim and faction-assist targets for nearby witnessing Axolotls; these assignments use
@@ -1405,15 +1452,23 @@ bash ./gradlew compileJava
Use this checklist:
1. Identify the owning subsystem.
-2. Add registries in `registry` if new blocks, items, entities, game rules, or tags are needed.
-3. Add data files if behavior should be pack-tunable.
-4. Add saved data only when state must persist.
-5. Add network payload only when client/server state must sync.
-6. Add event registration in `Retold` only when needed.
-7. Prefer system classes over mixin logic.
-8. Keep mixins small and named after the vanilla hook they touch.
-9. Add debug commands or output for stateful systems.
-10. Compile and test the relevant progression path.
+2. Identify compatibility boundaries before hard-coding item, block, entity, recipe, structure, or
+ world-mutation identities. Prefer standard tags where their complete meaning matches, otherwise
+ use a Retold-owned semantic tag, data definition, or small stable hook.
+3. When materially changing an existing subsystem, audit the compatibility assumptions on the
+ touched path. Improve them in the same focused change when safe and testable; document and defer
+ unresolved cases instead of broadening the change into an unrelated rewrite.
+4. Preserve Retold's standalone defaults and add focused regression coverage when introducing an
+ extension point. Unknown third-party content must fail safely rather than being implicitly owned.
+5. Add registries in `registry` if new blocks, items, entities, game rules, or tags are needed.
+6. Add data files if behavior should be pack-tunable.
+7. Add saved data only when state must persist.
+8. Add network payload only when client/server state must sync.
+9. Add event registration in `Retold` only when needed.
+10. Prefer system classes over mixin logic.
+11. Keep mixins small and named after the vanilla hook they touch.
+12. Add debug commands or output for stateful systems.
+13. Compile and test the relevant progression path.
Do not:
diff --git a/docs/internal/retold_roadmap.md b/docs/internal/retold_roadmap.md
index bee9de7..a24d9cb 100644
--- a/docs/internal/retold_roadmap.md
+++ b/docs/internal/retold_roadmap.md
@@ -69,6 +69,14 @@ Retold should remain usable as a standalone overhaul, but its major systems shou
General rule:
+- design every new system with mod and datapack compatibility in mind from the beginning; identify
+ standard tags, Retold-owned semantic data/tags, optional integration boundaries, and safe behavior
+ for unknown third-party content before hard-coding vanilla identities
+- when materially changing an existing system, audit and improve the compatibility of the touched
+ surface where it is safe and testable; do not launch unrelated broad rewrites, and record unsafe or
+ unresolved extension points for later work
+- compatibility improvements must preserve Retold's standalone defaults unless the developer has
+ explicitly approved a gameplay change, with regression coverage for the unchanged defaults
- prefer data, tags, stable public hooks, and small optional adapters over hard-coded checks for individual mods
- distinguish **compatibility** (both mods work together), **integration** (Retold understands the other mod's systems), and **balance support** (the other mod preserves Retold's intended progression); broad compatibility is desirable, integration should be selective, and balance support should not be promised by default
- avoid direct dependencies unless an integration genuinely cannot be implemented safely as optional support
@@ -76,21 +84,32 @@ General rule:
### High-priority compatibility work
-- [ ] Make faction membership data-driven instead of relying only on hard-coded vanilla entity IDs in `RetoldFactionMembers`.
+- [x] Make faction membership data-driven instead of relying only on hard-coded vanilla entity IDs in `RetoldFactionMembers`.
- Allow exact entity IDs and/or entity tags to opt into Retold factions.
- Preserve Retold's built-in vanilla defaults.
- Make it possible for a datapack or compatibility addon to classify a modded mob as Undead, Illager, Nether Remnant, Village Defender, Ender, etc. without Java patches in Retold.
- Keep conditional relations such as Witch raid cooperation expressible without turning every special case into a generic faction member.
-
-- [ ] Audit Retold for places where standard Minecraft/NeoForge common tags should be used instead of exact vanilla item/block checks.
+ - Implemented with one additive `retold:factions/*` entity-type tag per fixed Retold faction and
+ `retold:alliances/illager_loose_allies` for conditional Witch-style alignment. Conflicting full
+ memberships fail closed, a full faction suppresses a loose alliance, and reloads refresh cached
+ classification plus goals for loaded mobs. Defaults preserve the former exact members while
+ composing `minecraft:illager` and `minecraft:undead`; tamed undead mounts suppress generic
+ hostile Undead identity at the entity level.
+
+- [x] Audit Retold for places where standard Minecraft/NeoForge common tags should be used instead of exact vanilla item/block checks.
- Prefer common material tags where the gameplay meaning is genuinely "any valid material of this type".
- Keep Retold-specific semantic tags for concepts owned by Retold.
- Add extension tags where useful for modpack authors, such as valid torch igniters, weak mob barriers, portal-related materials, or other future Retold systems.
+ - Behavior-preserving audit completed for environmental mob resources, ordinary forage and food families, Spider-lair web counting, Illager village signals, Nether-remnant and Ocean-Monument guard anchors, protected monument blocks, consumable Campfire igniters, and leaf-preserving tools. Standard vanilla/NeoForge tags are nested where their defaults match exactly. Fixed progression, paired mapping, portal/structure, unique loot/replacement, and currently unverified Sniffer checks remain exact by design.
-- [ ] Add a stable recipe-visibility/knowledge hook shared by all recipe UIs.
+- [x] Add a stable recipe-visibility/knowledge hook shared by all recipe UIs.
- Vanilla recipe-book behavior, EMI, JEI, and future viewers should be able to ask the same Retold authority whether a recipe is known/visible to a player.
- Unknown Retold recipes must not become spoilers merely because a recipe-viewer mod is installed.
- Consider separate states where an item/output is known but its exact recipe is still undiscovered.
+ - `RetoldRecipeKnowledge` now exposes the authoritative per-player query, teaching operation,
+ immutable known-id snapshot, and opt-in custom recipe-type registration. The client receives the
+ same knowledge snapshot used by vanilla; separate output-only discovery remains only a future
+ design option.
- [ ] Add optional EMI compatibility.
- Hide or filter undiscovered recipes according to Retold recipe knowledge.
@@ -100,11 +119,14 @@ General rule:
- Match the same recipe-discovery rules as vanilla/EMI rather than creating a separate knowledge model.
- Do not require JEI for normal Retold operation.
-- [ ] Create a generic Retold world-protection permission layer before adding claim-mod-specific adapters.
+- [x] Create a generic Retold world-protection permission layer before adding claim-mod-specific adapters.
- Centralize checks such as `canMobBreak`, `canWorldModify`, `canStructureGenerate`, and `canPortalCreate` (exact API names TBD).
- Route Retold-owned world mutation through this layer where practical: Aender chunk replacement/regeneration, counterpart portal construction, retrogen/delayed structures, Gale Core block breaking, and future environmental transformations.
- Default behavior without a protection integration should preserve current Retold behavior.
- Later adapters can map this layer to popular claims/protection systems without scattering mod checks across gameplay code.
+ - `RetoldWorldProtection` is default-allow with uniquely identified, removable deny rules. It now
+ gates position-aware Retold mob breaking/placement, Gale Core damage, Aender regeneration and
+ portal creation, and delayed structure retrogen. Concrete claim adapters remain separate work.
### Data-driven mob compatibility
@@ -116,15 +138,17 @@ General rule:
- Consider Retold tags/data for AI-managed, AI-excluded, territory-excluded, or faction-excluded entities if those distinctions are needed in practice.
- A mod with sophisticated custom AI must be able to coexist without Retold overriding behavior it does not own.
-- [ ] Make faction + profile composition work cleanly for third-party mobs.
+- [x] Make faction + profile composition work cleanly for third-party mobs.
- A compatibility pack should be able to say, for example, that a modded creature uses a predator/grazer profile while separately belonging to an existing Retold faction.
- Preserve the design rule that profile describes daily life while faction describes diplomacy/relationships.
+ - Faction tags now classify independently of the existing datapack profile registry; adding only
+ a faction tag never opts an unknown entity into Retold's managed daily-life AI.
### Recipe and machine compatibility
-- [ ] Generalize recipe-discovery handling so unknown third-party recipe types fail open safely instead of being unintentionally blocked or spoiled.
+- [x] Generalize recipe-discovery handling so unknown third-party recipe types fail open safely instead of being unintentionally blocked or spoiled.
-- [ ] Provide an extension point for integrations to register additional recipe types with Retold's discovery system.
+- [x] Provide an extension point for integrations to register additional recipe types with Retold's discovery system.
- This should support modded processing systems such as crushers, mixers, presses, sawmills, alloying, etc. without Retold knowing every machine mod directly.
- Pack authors should be able to decide whether a third-party recipe type participates in Retold discovery.
@@ -134,24 +158,29 @@ General rule:
### Public Retold integration API
-- [ ] Design a deliberately small, stable public API package, tentatively `cz.xefensor.retold.api`.
+- [x] Design a deliberately small, stable public API package, `cz.xefensor.retold.api`.
+ - The initial supported surface intentionally contains only recipe discovery and world mutation
+ protection. Stage, faction, profile, and Aender-specific contracts remain candidates rather than
+ exposing their mutable implementation classes prematurely.
Potential API surfaces to evaluate:
- [ ] World stage read access and safe stage transition hooks (`RetoldStages` or equivalent).
- [ ] Faction registration/query hooks (`RetoldFactions`).
- [ ] Mob-profile integration/query hooks (`RetoldMobProfiles`).
-- [ ] Recipe knowledge and visibility (`RetoldRecipeKnowledge`).
-- [ ] World-protection/world-mutation permission hooks (`RetoldWorldProtection`).
+- [x] Recipe knowledge and visibility (`RetoldRecipeKnowledge`).
+- [x] World-protection/world-mutation permission hooks (`RetoldWorldProtection`).
- [ ] Aender queries/events that third-party integrations may legitimately need (`RetoldAender`).
API rules:
- [ ] Do not expose mutable implementation storage when a manager already owns side effects.
- Example: external code should never directly edit Retold world-stage saved data; stage changes must continue through the stage manager/official API so synchronization and transition side effects occur.
-- [ ] Prefer events/queries over exposing internal classes.
-- [ ] Document API stability expectations before declaring interfaces public.
-- [ ] Keep optional integrations isolated so a missing third-party mod never causes classloading failures.
+- [x] Prefer events/queries over exposing internal classes.
+- [x] Document API stability expectations before declaring interfaces public.
+- [x] Keep optional integrations isolated so a missing third-party mod never causes classloading failures.
+ - The core API contains no EMI, JEI, or claim-mod classes; concrete adapters will remain optional,
+ separately loaded integration code.
### UI/information integrations
diff --git a/docs/internal/testing_strategy.md b/docs/internal/testing_strategy.md
index 7df5561..b4236de 100644
--- a/docs/internal/testing_strategy.md
+++ b/docs/internal/testing_strategy.md
@@ -71,6 +71,29 @@ Generic push, pull-request, and release workflows run `./gradlew build` but do n
automatically because the correct exact test depends on the changed behavior. The developer or
implementer records the exact GameTest command and result for the change before handoff.
+For faction classification changes, run
+`retold:faction_tags_preserve_defaults_and_standard_undead` for the registry contract and
+`retold:faction_tags_drive_targeting_and_retaliation` for live relationships, target ownership,
+retaliation, and loaded-mob goal removal. Add the exact existing faction behavior affected, such as
+the Silverfish/Endermite, Witch raid-alliance, or territory-member test. When changing tag extension
+or conflict behavior, also run those two faction tests once with a temporary external datapack that
+adds a member and exercises the relevant ambiguity rule. Add one factioned and one unfactioned mob
+TPS selector when repeated classification or loaded-goal maintenance changes.
+
+For recipe-discovery compatibility changes, run
+`retold:recipe_visibility_uses_shared_knowledge_authority` and
+`retold:unknown_recipe_types_fail_open_for_viewers`, then the exact existing crafting, cooking,
+smithing, stonecutting, or Villager-teaching path changed. A concrete viewer adapter also requires
+an in-client login/learning/reconnect pass with that viewer present; a server-only GameTest cannot
+prove its dynamic UI refresh.
+
+For generic world-protection changes, run
+`retold:world_protection_rules_preserve_defaults_and_block_mutations` plus one focused existing test
+for every routed owner changed. A concrete claim adapter requires dedicated-server and multiplayer
+checks for allow/deny, claim boundaries, owner/projectile attribution, portals, delayed structures,
+and Aender retry behavior. Do not run the complete worldgen or mob suite unless the shared layer or
+mutation routing changed broadly enough to justify it.
+
The Villager consumer transaction, consumer route, and Farmer supply tests intentionally use
separate isolated GameTest environments. Run only the exact route or transaction test changed.
@@ -157,7 +180,10 @@ For Villager golem construction, select the exact profession, eligibility, curre
structure-animation, or player-placement test changed. Add
`retold:mob_tps_villager` only
when the repeated Villager dispatcher, scan, budget, ownership, or navigation path changes; the
-player pumpkin-placement hook alone does not justify a TPS rerun.
+player pumpkin-placement hook alone does not justify a TPS rerun. Long-running construction
+fixtures must trade-lock manually assigned professions and select an allowed activity while testing
+staged mechanics; otherwise vanilla may legitimately remove an unclaimed profession or Retold may
+correctly pause for the inherited schedule.
For Villager torch maintenance, select the exact relighting test for magical casting, Nitwit
close-range fake-tool use, stage, village, range, priority, inventory conservation, or wall-state
diff --git a/docs/internal/tool_armor_ore_progression.md b/docs/internal/tool_armor_ore_progression.md
index 42c9c7c..0bb0847 100644
--- a/docs/internal/tool_armor_ore_progression.md
+++ b/docs/internal/tool_armor_ore_progression.md
@@ -73,8 +73,10 @@ Stick Stick
Log Log Log
```
-Newly placed Campfires begin unlit. A bare Flint lights an unlit, non-waterlogged Campfire and is
-consumed in Survival; Flint and Steel also works through its ordinary durability-based behavior.
+Newly placed Campfires begin unlit. An item in `retold:campfire_consumable_igniters` lights an
+unlit, non-waterlogged Campfire and is consumed in Survival; bare Flint is the default. Flint and
+Steel also works through its ordinary durability-based behavior and deliberately is not in the
+consumable tag.
Campfire cooking turns one Clay Ball into one Brick in 600 ticks. Eight fired Bricks in a ring then
craft the Brick Furnace:
@@ -148,8 +150,9 @@ The player begins by gathering materials that do not require tools:
All blocks in the standard `minecraft:leaves` tag receive a supplemental 20% chance to drop 1–2
Sticks. Fortune adds five percentage points per level (25%/30%/35% for Fortune I/II/III). This is
independent of the leaf's ordinary loot-table Stick roll, so the vanilla drop remains possible too.
-Shears and Silk Touch do not receive the supplemental drop and continue harvesting the leaf block
-normally. The values are provisional until the natural opening loop is playtested.
+Tools in `retold:leaf_preserving_tools` and tools with Silk Touch do not receive the supplemental
+drop and continue harvesting the leaf block normally; Shears are the tag's default. The values are
+provisional until the natural opening loop is playtested.
The first essential tool is the **Flint Multi-tool**.
diff --git a/src/main/java/cz/xefensor/retold/aender/generation/AenderLoadedChunkReplacement.java b/src/main/java/cz/xefensor/retold/aender/generation/AenderLoadedChunkReplacement.java
index 41dad3e..d06f637 100644
--- a/src/main/java/cz/xefensor/retold/aender/generation/AenderLoadedChunkReplacement.java
+++ b/src/main/java/cz/xefensor/retold/aender/generation/AenderLoadedChunkReplacement.java
@@ -1,5 +1,6 @@
package cz.xefensor.retold.aender.generation;
+import cz.xefensor.retold.api.world.RetoldWorldProtection;
import net.minecraft.core.BlockPos;
import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
import net.minecraft.server.level.ServerLevel;
@@ -21,19 +22,36 @@ public final class AenderLoadedChunkReplacement {
private AenderLoadedChunkReplacement() {
}
- public static void regenerate(ServerLevel level, ChunkAccess chunk) {
+ public static boolean regenerate(ServerLevel level, ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
+
+ if (!RetoldWorldProtection.canRegenerateAenderChunk(level, chunkPos)) {
+ return false;
+ }
+
List entities = entitiesInChunk(level, chunkPos);
AenderChunkGenerator.regenerateLoadedChunk(level, chunk);
reconcileEntitiesAfterRegeneration(level, chunkPos, entities);
resend(level, chunk);
+ return true;
}
- public static void blankForProgressiveRegeneration(ServerLevel level, ChunkAccess chunk) {
+ public static boolean blankForProgressiveRegeneration(
+ ServerLevel level,
+ ChunkAccess chunk
+ ) {
+ if (!RetoldWorldProtection.canRegenerateAenderChunk(
+ level,
+ chunk.getPos()
+ )) {
+ return false;
+ }
+
discardEntitiesInChunk(level, chunk.getPos());
AenderChunkSectionEditor.clear(chunk);
AenderChunkSectionEditor.primeFreshHeightmaps(chunk);
AenderVolatility.forgetGeneratedMark(chunk);
+ return true;
}
private static List entitiesInChunk(ServerLevel level, ChunkPos chunkPos) {
diff --git a/src/main/java/cz/xefensor/retold/aender/portal/AenderPortalLogic.java b/src/main/java/cz/xefensor/retold/aender/portal/AenderPortalLogic.java
index 3ec78fc..dfcdaeb 100644
--- a/src/main/java/cz/xefensor/retold/aender/portal/AenderPortalLogic.java
+++ b/src/main/java/cz/xefensor/retold/aender/portal/AenderPortalLogic.java
@@ -1,6 +1,8 @@
package cz.xefensor.retold.aender.portal;
import cz.xefensor.retold.Retold;
+import cz.xefensor.retold.api.world.RetoldWorldMutationBounds;
+import cz.xefensor.retold.api.world.RetoldWorldProtection;
import cz.xefensor.retold.aender.RetoldAenderDimensions;
import cz.xefensor.retold.aender.generation.AenderVolatility;
import cz.xefensor.retold.aender.stability.AenderRealityTickEvents;
@@ -44,6 +46,10 @@ private AenderPortalLogic() {
}
public static void activatePortal(ServerLevel level, AenderPortalShape shape) {
+ if (!isPortalCreationAllowed(level, shape.minCorner())) {
+ return;
+ }
+
shape.createPortalBlocks(level);
AenderPortalData.get(level).register(level, shape);
@@ -99,11 +105,24 @@ public static void activatePortal(ServerLevel level, AenderPortalShape shape) {
* bounded fallback remains for commands, other portal integrations,
* and non-player entities that bypass the charging state machine.
*/
- AenderRealityTickEvents.prepareArrivalCore(destinationLevel, approximateExit);
+ if (!AenderRealityTickEvents.prepareArrivalCore(
+ destinationLevel,
+ approximateExit
+ )) {
+ return null;
+ }
}
AenderPortalShape exitPortal = findClosestPortal(destinationLevel, approximateExit)
- .orElseGet(() -> createExitPortal(destinationLevel, approximateExit));
+ .orElse(null);
+
+ if (exitPortal == null) {
+ exitPortal = createExitPortal(destinationLevel, approximateExit);
+ }
+
+ if (exitPortal == null) {
+ return null;
+ }
if (enteringAender) {
AenderPortalWarmup.finish(entity);
@@ -284,8 +303,16 @@ private static boolean isAenderChunkCurrent(ServerLevel level, ChunkAccess chunk
return true;
}
- static AenderPortalShape createExitPortal(ServerLevel level, BlockPos approximateExit) {
+ static @Nullable AenderPortalShape createExitPortal(
+ ServerLevel level,
+ BlockPos approximateExit
+ ) {
BlockPos origin = findExitOrigin(level, approximateExit);
+
+ if (origin == null) {
+ return null;
+ }
+
AenderPortalShape shape = new AenderPortalShape(origin, TARGET_PORTAL_SIZE, TARGET_PORTAL_SIZE);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
BlockState frame = RetoldBlocks.DEV_AENDER_PORTAL_FRAME.get().defaultBlockState();
@@ -322,7 +349,7 @@ static AenderPortalShape createExitPortal(ServerLevel level, BlockPos approximat
return shape;
}
- private static BlockPos findExitOrigin(ServerLevel level, BlockPos approximateExit) {
+ private static @Nullable BlockPos findExitOrigin(ServerLevel level, BlockPos approximateExit) {
BlockPos.MutableBlockPos candidate = new BlockPos.MutableBlockPos();
boolean destinationIsAender = level.dimension() == RetoldAenderDimensions.AENDER;
@@ -345,7 +372,8 @@ private static BlockPos findExitOrigin(ServerLevel level, BlockPos approximateEx
int y = preferredPortalY(level, surfaceY);
candidate.set(column.getX() - 1, y, column.getZ() - 1);
- if (canCreatePortal(level, candidate)) {
+ if (canCreatePortal(level, candidate)
+ && isPortalCreationAllowed(level, candidate)) {
return candidate.immutable();
}
}
@@ -375,7 +403,8 @@ private static BlockPos findExitOrigin(ServerLevel level, BlockPos approximateEx
? Mth.clamp(approximateExit.getZ() - 1, minOriginZ, maxOriginZ)
: approximateExit.getZ() - 1;
- return new BlockPos(fallbackX, fallbackY, fallbackZ);
+ BlockPos fallback = new BlockPos(fallbackX, fallbackY, fallbackZ);
+ return isPortalCreationAllowed(level, fallback) ? fallback : null;
}
private static int preferredPortalY(ServerLevel level, int surfaceY) {
@@ -415,6 +444,23 @@ private static boolean canCreatePortal(ServerLevel level, BlockPos origin) {
return true;
}
+ private static boolean isPortalCreationAllowed(
+ ServerLevel level,
+ BlockPos origin
+ ) {
+ return RetoldWorldProtection.canCreatePortal(
+ level,
+ new RetoldWorldMutationBounds(
+ origin.getX() - 1,
+ origin.getY() - 1,
+ origin.getZ() - 1,
+ origin.getX() + TARGET_PORTAL_SIZE,
+ origin.getY() + 3,
+ origin.getZ() + TARGET_PORTAL_SIZE
+ )
+ );
+ }
+
private static TeleportTransition createTransition(
ServerLevel currentLevel,
ServerLevel destinationLevel,
diff --git a/src/main/java/cz/xefensor/retold/aender/stability/AenderRealityTickEvents.java b/src/main/java/cz/xefensor/retold/aender/stability/AenderRealityTickEvents.java
index b6dbeec..eac7127 100644
--- a/src/main/java/cz/xefensor/retold/aender/stability/AenderRealityTickEvents.java
+++ b/src/main/java/cz/xefensor/retold/aender/stability/AenderRealityTickEvents.java
@@ -153,9 +153,9 @@ public static void beginRealityTransition(ServerLevel level, List prev
* radius prevents configured view distance from turning a teleport into an
* unbounded main-thread generation stall.
*/
- public static void prepareArrivalCore(ServerLevel level, BlockPos center) {
+ public static boolean prepareArrivalCore(ServerLevel level, BlockPos center) {
if (level.dimension() != RetoldAenderDimensions.AENDER) {
- return;
+ return false;
}
int centerChunkX = center.getX() >> 4;
@@ -165,6 +165,7 @@ public static void prepareArrivalCore(ServerLevel level, BlockPos center) {
int regenerated = 0;
long startedAtNanos = System.nanoTime();
AenderStabilityData stability = AenderStabilityData.get(level);
+ boolean allReady = true;
// Work from the center out so the essential arrival chunks are prepared first.
for (int ring = 0; ring <= radius; ring++) {
@@ -190,8 +191,11 @@ public static void prepareArrivalCore(ServerLevel level, BlockPos center) {
AenderVolatility.retainForChunk(chunk);
if (AenderVolatility.needsRegeneration(chunk)) {
- AenderLoadedChunkReplacement.regenerate(level, chunk);
- regenerated++;
+ if (AenderLoadedChunkReplacement.regenerate(level, chunk)) {
+ regenerated++;
+ } else {
+ allReady = false;
+ }
}
}
}
@@ -207,6 +211,8 @@ public static void prepareArrivalCore(ServerLevel level, BlockPos center) {
regenerated
);
}
+
+ return allReady;
}
/**
@@ -234,7 +240,7 @@ public static boolean prepareLoadedArrivalChunk(ServerLevel level, int chunkX, i
AenderVolatility.retainForChunk(chunk);
if (shouldRegenerate(stability, chunk)) {
- AenderLoadedChunkReplacement.regenerate(level, chunk);
+ return AenderLoadedChunkReplacement.regenerate(level, chunk);
}
return true;
@@ -266,16 +272,23 @@ public static boolean prepareArrivalVisibilityChunk(ServerLevel level, int chunk
AenderVolatility.retainForChunk(chunk);
if (AenderVolatility.needsRegeneration(chunk)) {
- blankLoadedStaleChunk(level, chunk);
+ return blankLoadedStaleChunk(level, chunk);
}
return true;
}
- public static void blankLoadedStaleChunk(ServerLevel level, ChunkAccess chunk) {
- AenderLoadedChunkReplacement.blankForProgressiveRegeneration(level, chunk);
+ public static boolean blankLoadedStaleChunk(ServerLevel level, ChunkAccess chunk) {
+ if (!AenderLoadedChunkReplacement.blankForProgressiveRegeneration(
+ level,
+ chunk
+ )) {
+ return false;
+ }
+
BLANK_QUEUED.remove(pack(chunk.getPos().x(), chunk.getPos().z()));
enqueueIfNeeded(level, chunk);
+ return true;
}
/**
@@ -455,7 +468,15 @@ private static int processBlankQueue(ServerLevel level) {
continue;
}
- AenderLoadedChunkReplacement.blankForProgressiveRegeneration(level, chunk);
+ if (!AenderLoadedChunkReplacement.blankForProgressiveRegeneration(
+ level,
+ chunk
+ )) {
+ enqueueBlank(chunk.getPos().x(), chunk.getPos().z());
+ nextBlankGameTime = gameTime + OVERLOADED_RETRY_TICKS;
+ break;
+ }
+
blanked++;
}
@@ -512,7 +533,12 @@ private static void processRegenQueue(ServerLevel level) {
}
long chunkStartedAt = System.nanoTime();
- AenderLoadedChunkReplacement.regenerate(level, chunk);
+ if (!AenderLoadedChunkReplacement.regenerate(level, chunk)) {
+ enqueue(chunkX, chunkZ);
+ nextRegenGameTime = gameTime + OVERLOADED_RETRY_TICKS;
+ break;
+ }
+
long chunkNanos = System.nanoTime() - chunkStartedAt;
estimatedRegenNanos = (estimatedRegenNanos * 3L + chunkNanos) / 4L;
regenerated++;
diff --git a/src/main/java/cz/xefensor/retold/api/recipe/RetoldRecipeKnowledge.java b/src/main/java/cz/xefensor/retold/api/recipe/RetoldRecipeKnowledge.java
new file mode 100644
index 0000000..1fd7d48
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/api/recipe/RetoldRecipeKnowledge.java
@@ -0,0 +1,87 @@
+package cz.xefensor.retold.api.recipe;
+
+import cz.xefensor.retold.recipe.RetoldKnownRecipeData;
+import cz.xefensor.retold.recipe.RetoldRecipeBookEvents;
+import cz.xefensor.retold.recipe.RetoldRecipeVisibility;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.item.crafting.Recipe;
+import net.minecraft.world.item.crafting.RecipeHolder;
+import net.minecraft.world.item.crafting.RecipeType;
+
+import java.util.Set;
+
+/**
+ * Stable integration surface for Retold's per-player recipe-discovery rules.
+ *
+ * Unknown recipe types are visible by default. An optional integration can
+ * register a custom machine recipe type when it has a reliable way to teach
+ * those recipes through {@link #teachAndUnlock(ServerPlayer, RecipeHolder)}.
+ */
+public final class RetoldRecipeKnowledge {
+ private RetoldRecipeKnowledge() {
+ }
+
+ public static boolean isDiscoveryManaged(RecipeHolder> recipe) {
+ return RetoldRecipeVisibility.isDiscoveryManaged(recipe);
+ }
+
+ public static boolean isDiscoveryManaged(RecipeType> recipeType) {
+ return RetoldRecipeVisibility.isDiscoveryManaged(recipeType);
+ }
+
+ public static boolean isKnown(
+ ServerPlayer player,
+ ResourceKey> recipeId
+ ) {
+ return RetoldRecipeVisibility.isKnown(player, recipeId);
+ }
+
+ public static boolean isVisibleTo(Player player, RecipeHolder> recipe) {
+ return RetoldRecipeVisibility.isVisibleTo(player, recipe);
+ }
+
+ public static boolean isVisibleTo(
+ Player player,
+ ResourceKey> recipeId,
+ RecipeType> recipeType
+ ) {
+ return RetoldRecipeVisibility.isVisibleTo(player, recipeId, recipeType);
+ }
+
+ public static Set>> knownRecipes(ServerPlayer player) {
+ if (player == null) {
+ return Set.of();
+ }
+
+ return RetoldKnownRecipeData.get(player.level()).knownRecipes(player);
+ }
+
+ public static void teachAndUnlock(
+ ServerPlayer player,
+ RecipeHolder> recipe
+ ) {
+ if (player == null || recipe == null) {
+ return;
+ }
+
+ RetoldRecipeBookEvents.markKnownAndUnlockRecipe(player, recipe);
+ }
+
+ public static DiscoveryRegistration registerDiscoveryManagedType(
+ RecipeType> recipeType
+ ) {
+ if (recipeType == null) {
+ throw new IllegalArgumentException("recipeType must not be null");
+ }
+
+ return RetoldRecipeVisibility.registerDiscoveryManagedType(recipeType);
+ }
+
+ @FunctionalInterface
+ public interface DiscoveryRegistration extends AutoCloseable {
+ @Override
+ void close();
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/api/world/RetoldWorldMutationBounds.java b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldMutationBounds.java
new file mode 100644
index 0000000..3b9d1df
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldMutationBounds.java
@@ -0,0 +1,44 @@
+package cz.xefensor.retold.api.world;
+
+import net.minecraft.core.BlockPos;
+
+/** Inclusive block bounds for the area a Retold mutation may affect. */
+public record RetoldWorldMutationBounds(
+ int minX,
+ int minY,
+ int minZ,
+ int maxX,
+ int maxY,
+ int maxZ
+) {
+ public RetoldWorldMutationBounds {
+ if (maxX < minX || maxY < minY || maxZ < minZ) {
+ throw new IllegalArgumentException("World mutation bounds must not be inverted");
+ }
+ }
+
+ public static RetoldWorldMutationBounds single(BlockPos pos) {
+ if (pos == null) {
+ throw new IllegalArgumentException("pos must not be null");
+ }
+
+ return new RetoldWorldMutationBounds(
+ pos.getX(),
+ pos.getY(),
+ pos.getZ(),
+ pos.getX(),
+ pos.getY(),
+ pos.getZ()
+ );
+ }
+
+ public boolean contains(BlockPos pos) {
+ return pos != null
+ && pos.getX() >= minX
+ && pos.getX() <= maxX
+ && pos.getY() >= minY
+ && pos.getY() <= maxY
+ && pos.getZ() >= minZ
+ && pos.getZ() <= maxZ;
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/api/world/RetoldWorldMutationContext.java b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldMutationContext.java
new file mode 100644
index 0000000..e7e87ea
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldMutationContext.java
@@ -0,0 +1,32 @@
+package cz.xefensor.retold.api.world;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.resources.Identifier;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import org.jspecify.annotations.Nullable;
+
+import java.util.Objects;
+
+/** Immutable description of a Retold-owned world mutation before it occurs. */
+public record RetoldWorldMutationContext(
+ ServerLevel level,
+ BlockPos pos,
+ RetoldWorldMutationBounds bounds,
+ RetoldWorldMutationType type,
+ @Nullable Entity actor,
+ @Nullable Identifier subjectId
+) {
+ public RetoldWorldMutationContext {
+ Objects.requireNonNull(level, "level");
+ pos = Objects.requireNonNull(pos, "pos").immutable();
+ Objects.requireNonNull(bounds, "bounds");
+ Objects.requireNonNull(type, "type");
+
+ if (!bounds.contains(pos)) {
+ throw new IllegalArgumentException(
+ "Representative mutation position must be inside its bounds"
+ );
+ }
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/api/world/RetoldWorldMutationType.java b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldMutationType.java
new file mode 100644
index 0000000..73e101f
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldMutationType.java
@@ -0,0 +1,12 @@
+package cz.xefensor.retold.api.world;
+
+/** Categories exposed to optional claim and world-protection integrations. */
+public enum RetoldWorldMutationType {
+ MOB_BREAK,
+ MOB_PLACE,
+ ENTITY_BREAK,
+ PORTAL_CREATE,
+ STRUCTURE_RETROGEN,
+ AENDER_CHUNK_REGENERATE,
+ WORLD_MODIFY
+}
diff --git a/src/main/java/cz/xefensor/retold/api/world/RetoldWorldProtection.java b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldProtection.java
new file mode 100644
index 0000000..dbc24f0
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldProtection.java
@@ -0,0 +1,271 @@
+package cz.xefensor.retold.api.world;
+
+import cz.xefensor.retold.Retold;
+import net.minecraft.core.BlockPos;
+import net.minecraft.resources.Identifier;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.ChunkPos;
+import org.jspecify.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Stable, default-allow permission layer for Retold-owned world mutations.
+ *
+ * Integrations register one rule under their own identifier. A mutation is
+ * allowed only when every installed rule allows it. No registered rules means
+ * the exact standalone Retold behavior is preserved.
+ */
+public final class RetoldWorldProtection {
+ private static final Object RULE_LOCK = new Object();
+ private static final Map RULES =
+ new LinkedHashMap<>();
+ private static volatile List ruleSnapshot = List.of();
+
+ private RetoldWorldProtection() {
+ }
+
+ public static Registration register(
+ Identifier integrationId,
+ RetoldWorldProtectionRule rule
+ ) {
+ Objects.requireNonNull(integrationId, "integrationId");
+ Objects.requireNonNull(rule, "rule");
+
+ synchronized (RULE_LOCK) {
+ if (RULES.putIfAbsent(integrationId, rule) != null) {
+ throw new IllegalStateException(
+ "A Retold world-protection rule is already registered as "
+ + integrationId
+ );
+ }
+
+ refreshSnapshot();
+ }
+
+ return new RuleRegistration(integrationId, rule);
+ }
+
+ public static boolean canModify(RetoldWorldMutationContext context) {
+ Objects.requireNonNull(context, "context");
+
+ for (RegisteredRule registered : ruleSnapshot) {
+ try {
+ if (!registered.rule().allows(context)) {
+ return false;
+ }
+ } catch (RuntimeException exception) {
+ Retold.LOGGER.error(
+ "World-protection rule {} failed while checking {} at {} in {}. Denying the mutation.",
+ registered.id(),
+ context.type(),
+ context.pos(),
+ context.level().dimension().identifier(),
+ exception
+ );
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static boolean canMobBreak(
+ ServerLevel level,
+ BlockPos pos,
+ Entity actor
+ ) {
+ return canModify(new RetoldWorldMutationContext(
+ level,
+ pos,
+ RetoldWorldMutationBounds.single(pos),
+ RetoldWorldMutationType.MOB_BREAK,
+ actor,
+ null
+ ));
+ }
+
+ public static boolean canMobPlace(
+ ServerLevel level,
+ BlockPos pos,
+ Entity actor
+ ) {
+ return canModify(new RetoldWorldMutationContext(
+ level,
+ pos,
+ RetoldWorldMutationBounds.single(pos),
+ RetoldWorldMutationType.MOB_PLACE,
+ actor,
+ null
+ ));
+ }
+
+ public static boolean canEntityBreak(
+ ServerLevel level,
+ BlockPos pos,
+ Entity actor
+ ) {
+ return canModify(new RetoldWorldMutationContext(
+ level,
+ pos,
+ RetoldWorldMutationBounds.single(pos),
+ RetoldWorldMutationType.ENTITY_BREAK,
+ actor,
+ null
+ ));
+ }
+
+ public static boolean canCreatePortal(ServerLevel level, BlockPos pos) {
+ return canCreatePortal(level, RetoldWorldMutationBounds.single(pos));
+ }
+
+ public static boolean canCreatePortal(
+ ServerLevel level,
+ RetoldWorldMutationBounds bounds
+ ) {
+ BlockPos pos = center(bounds);
+ return canModify(new RetoldWorldMutationContext(
+ level,
+ pos,
+ bounds,
+ RetoldWorldMutationType.PORTAL_CREATE,
+ null,
+ Identifier.fromNamespaceAndPath(Retold.MODID, "aender_portal")
+ ));
+ }
+
+ public static boolean canRetrogenStructure(
+ ServerLevel level,
+ ChunkPos chunkPos,
+ Identifier structureId
+ ) {
+ RetoldWorldMutationBounds bounds = chunkBounds(level, chunkPos);
+ return canModify(new RetoldWorldMutationContext(
+ level,
+ center(bounds),
+ bounds,
+ RetoldWorldMutationType.STRUCTURE_RETROGEN,
+ null,
+ structureId
+ ));
+ }
+
+ public static boolean canRegenerateAenderChunk(
+ ServerLevel level,
+ ChunkPos chunkPos
+ ) {
+ RetoldWorldMutationBounds bounds = chunkBounds(level, chunkPos);
+ return canModify(new RetoldWorldMutationContext(
+ level,
+ center(bounds),
+ bounds,
+ RetoldWorldMutationType.AENDER_CHUNK_REGENERATE,
+ null,
+ null
+ ));
+ }
+
+ public static boolean canWorldModify(
+ ServerLevel level,
+ BlockPos pos,
+ @Nullable Identifier subjectId
+ ) {
+ return canModify(new RetoldWorldMutationContext(
+ level,
+ pos,
+ RetoldWorldMutationBounds.single(pos),
+ RetoldWorldMutationType.WORLD_MODIFY,
+ null,
+ subjectId
+ ));
+ }
+
+ private static RetoldWorldMutationBounds chunkBounds(
+ ServerLevel level,
+ ChunkPos chunkPos
+ ) {
+ return new RetoldWorldMutationBounds(
+ chunkPos.getMinBlockX(),
+ level.getMinY(),
+ chunkPos.getMinBlockZ(),
+ chunkPos.getMaxBlockX(),
+ level.getMaxY() - 1,
+ chunkPos.getMaxBlockZ()
+ );
+ }
+
+ private static BlockPos center(RetoldWorldMutationBounds bounds) {
+ return new BlockPos(
+ bounds.minX() + (bounds.maxX() - bounds.minX()) / 2,
+ bounds.minY() + (bounds.maxY() - bounds.minY()) / 2,
+ bounds.minZ() + (bounds.maxZ() - bounds.minZ()) / 2
+ );
+ }
+
+ private static void refreshSnapshot() {
+ List snapshot = new ArrayList<>();
+
+ for (Map.Entry entry : RULES.entrySet()) {
+ snapshot.add(new RegisteredRule(entry.getKey(), entry.getValue()));
+ }
+
+ ruleSnapshot = List.copyOf(snapshot);
+ }
+
+ private static void unregister(
+ Identifier integrationId,
+ RetoldWorldProtectionRule rule
+ ) {
+ synchronized (RULE_LOCK) {
+ if (RULES.get(integrationId) == rule) {
+ RULES.remove(integrationId);
+ refreshSnapshot();
+ }
+ }
+ }
+
+ public interface Registration extends AutoCloseable {
+ Identifier integrationId();
+
+ @Override
+ void close();
+ }
+
+ private record RegisteredRule(
+ Identifier id,
+ RetoldWorldProtectionRule rule
+ ) {
+ }
+
+ private static final class RuleRegistration implements Registration {
+ private final Identifier integrationId;
+ private final RetoldWorldProtectionRule rule;
+ private final AtomicBoolean closed = new AtomicBoolean();
+
+ private RuleRegistration(
+ Identifier integrationId,
+ RetoldWorldProtectionRule rule
+ ) {
+ this.integrationId = integrationId;
+ this.rule = rule;
+ }
+
+ @Override
+ public Identifier integrationId() {
+ return integrationId;
+ }
+
+ @Override
+ public void close() {
+ if (closed.compareAndSet(false, true)) {
+ unregister(integrationId, rule);
+ }
+ }
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/api/world/RetoldWorldProtectionRule.java b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldProtectionRule.java
new file mode 100644
index 0000000..63eb518
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/api/world/RetoldWorldProtectionRule.java
@@ -0,0 +1,10 @@
+package cz.xefensor.retold.api.world;
+
+/**
+ * A deny-capable rule installed by a claims or world-protection integration.
+ * Every registered rule must allow a mutation before Retold performs it.
+ */
+@FunctionalInterface
+public interface RetoldWorldProtectionRule {
+ boolean allows(RetoldWorldMutationContext context);
+}
diff --git a/src/main/java/cz/xefensor/retold/behavior/breeding/RetoldAnimalBreeding.java b/src/main/java/cz/xefensor/retold/behavior/breeding/RetoldAnimalBreeding.java
index b1524e1..b347069 100644
--- a/src/main/java/cz/xefensor/retold/behavior/breeding/RetoldAnimalBreeding.java
+++ b/src/main/java/cz/xefensor/retold/behavior/breeding/RetoldAnimalBreeding.java
@@ -149,7 +149,7 @@ public static boolean feed(
? 20
: RetoldMobRules.foodRelief(
animal,
- RetoldMobRules.getItemPath(food)
+ food
);
state.addHunger(-Math.max(1, relief));
diff --git a/src/main/java/cz/xefensor/retold/behavior/core/RetoldMobGriefing.java b/src/main/java/cz/xefensor/retold/behavior/core/RetoldMobGriefing.java
index d9f73cd..0ef75cd 100644
--- a/src/main/java/cz/xefensor/retold/behavior/core/RetoldMobGriefing.java
+++ b/src/main/java/cz/xefensor/retold/behavior/core/RetoldMobGriefing.java
@@ -1,5 +1,7 @@
package cz.xefensor.retold.behavior.core;
+import cz.xefensor.retold.api.world.RetoldWorldProtection;
+import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.projectile.Projectile;
@@ -24,6 +26,34 @@ public static boolean canModifyBlocks(
);
}
+ public static boolean canBreakBlock(
+ ServerLevel level,
+ Entity actor,
+ BlockPos pos
+ ) {
+ return canModifyBlocks(level, actor)
+ && pos != null
+ && RetoldWorldProtection.canMobBreak(
+ level,
+ pos,
+ responsibleActor(actor)
+ );
+ }
+
+ public static boolean canPlaceBlock(
+ ServerLevel level,
+ Entity actor,
+ BlockPos pos
+ ) {
+ return canModifyBlocks(level, actor)
+ && pos != null
+ && RetoldWorldProtection.canMobPlace(
+ level,
+ pos,
+ responsibleActor(actor)
+ );
+ }
+
public static boolean isEnabled(ServerLevel level) {
return level != null
&& level.getGameRules().get(GameRules.MOB_GRIEFING);
diff --git a/src/main/java/cz/xefensor/retold/behavior/food/RetoldAnimalFeederBehavior.java b/src/main/java/cz/xefensor/retold/behavior/food/RetoldAnimalFeederBehavior.java
index a9774d8..b2182b0 100644
--- a/src/main/java/cz/xefensor/retold/behavior/food/RetoldAnimalFeederBehavior.java
+++ b/src/main/java/cz/xefensor/retold/behavior/food/RetoldAnimalFeederBehavior.java
@@ -161,7 +161,7 @@ public static boolean tryConsume(
state.addHunger(-RetoldMobRules.foodRelief(
mob,
- RetoldMobRules.getItemPath(consumed)
+ consumed
));
state.markFed(gameTime);
RetoldFeedingAnimations.play(mob);
diff --git a/src/main/java/cz/xefensor/retold/behavior/food/RetoldFoodBehaviorEvents.java b/src/main/java/cz/xefensor/retold/behavior/food/RetoldFoodBehaviorEvents.java
index 9cc76f0..4c27e57 100644
--- a/src/main/java/cz/xefensor/retold/behavior/food/RetoldFoodBehaviorEvents.java
+++ b/src/main/java/cz/xefensor/retold/behavior/food/RetoldFoodBehaviorEvents.java
@@ -627,12 +627,10 @@ private static boolean tryConsumeDroppedFood(
return false;
}
- String itemPath = RetoldMobRules.getItemPath(stack);
-
state.addHunger(
-RetoldMobRules.foodRelief(
mob,
- itemPath
+ stack
)
);
@@ -799,7 +797,7 @@ private static boolean tryConsumeForageBlock(
RetoldMobRules.isRenewableEnvironmentalForage(mob, blockState);
if (!renewableEnvironmentalForage
- && !RetoldMobGriefing.canModifyBlocks(level, mob)) {
+ && !RetoldMobGriefing.canBreakBlock(level, mob, foragePos)) {
return false;
}
@@ -818,7 +816,7 @@ private static boolean tryConsumeForageBlock(
state.addHunger(
-RetoldMobRules.forageRelief(
mob,
- blockPath
+ blockState
)
);
diff --git a/src/main/java/cz/xefensor/retold/behavior/food/RetoldHeldFoodConsumptionEvents.java b/src/main/java/cz/xefensor/retold/behavior/food/RetoldHeldFoodConsumptionEvents.java
index 708159d..00c63d3 100644
--- a/src/main/java/cz/xefensor/retold/behavior/food/RetoldHeldFoodConsumptionEvents.java
+++ b/src/main/java/cz/xefensor/retold/behavior/food/RetoldHeldFoodConsumptionEvents.java
@@ -163,7 +163,7 @@ private static void consumeOneHeldFood(
);
int relief = RetoldMobRules.foodRelief(
mob,
- RetoldMobRules.getItemPath(stack)
+ stack
);
if (!RetoldAiControl.tryClaim(
diff --git a/src/main/java/cz/xefensor/retold/behavior/food/RetoldWeakBarrierBehavior.java b/src/main/java/cz/xefensor/retold/behavior/food/RetoldWeakBarrierBehavior.java
index b696dcb..e27a6c2 100644
--- a/src/main/java/cz/xefensor/retold/behavior/food/RetoldWeakBarrierBehavior.java
+++ b/src/main/java/cz/xefensor/retold/behavior/food/RetoldWeakBarrierBehavior.java
@@ -287,7 +287,7 @@ private static void finishBreak(
) {
clearProgress(level, mob, breach);
- if (!RetoldMobGriefing.canModifyBlocks(level, mob)
+ if (!RetoldMobGriefing.canBreakBlock(level, mob, target)
|| !RetoldWeakBarriers.isBreakable(level.getBlockState(target))) {
breach.target = null;
return;
diff --git a/src/main/java/cz/xefensor/retold/behavior/performance/RetoldBlockTargetSearch.java b/src/main/java/cz/xefensor/retold/behavior/performance/RetoldBlockTargetSearch.java
index 52684c8..7423729 100644
--- a/src/main/java/cz/xefensor/retold/behavior/performance/RetoldBlockTargetSearch.java
+++ b/src/main/java/cz/xefensor/retold/behavior/performance/RetoldBlockTargetSearch.java
@@ -1,6 +1,7 @@
package cz.xefensor.retold.behavior.performance;
import cz.xefensor.retold.behavior.profiles.RetoldMobRules;
+import cz.xefensor.retold.registry.RetoldTags;
import cz.xefensor.retold.behavior.core.RetoldWeakBarriers;
import net.minecraft.core.BlockPos;
@@ -669,8 +670,7 @@ private static boolean isSand(
) {
BlockState state = level.getBlockState(pos);
- return state.is(Blocks.SAND)
- || state.is(Blocks.RED_SAND);
+ return state.is(RetoldTags.TURTLE_BEACH_BLOCKS);
}
private static boolean isWater(
@@ -784,8 +784,7 @@ private static boolean isBamboo(
) {
BlockState state = level.getBlockState(pos);
- return state.is(Blocks.BAMBOO)
- || state.is(Blocks.BAMBOO_SAPLING);
+ return state.is(RetoldTags.PANDA_BAMBOO_BLOCKS);
}
private static boolean isScrubRange(
@@ -798,14 +797,7 @@ private static boolean isScrubRange(
BlockState state = level.getBlockState(pos);
- return state.is(Blocks.GRASS_BLOCK)
- || state.is(Blocks.DIRT)
- || state.is(Blocks.COARSE_DIRT)
- || state.is(Blocks.ROOTED_DIRT)
- || state.is(Blocks.PODZOL)
- || state.is(Blocks.SAND)
- || state.is(Blocks.RED_SAND)
- || state.is(Blocks.TERRACOTTA);
+ return state.is(RetoldTags.ARMADILLO_SCRUB_RANGE_BLOCKS);
}
private static boolean isArmadilloGrubSoil(
diff --git a/src/main/java/cz/xefensor/retold/behavior/profiles/RetoldMobRules.java b/src/main/java/cz/xefensor/retold/behavior/profiles/RetoldMobRules.java
index 626bbe7..c32fb22 100644
--- a/src/main/java/cz/xefensor/retold/behavior/profiles/RetoldMobRules.java
+++ b/src/main/java/cz/xefensor/retold/behavior/profiles/RetoldMobRules.java
@@ -1,11 +1,13 @@
package cz.xefensor.retold.behavior.profiles;
+import cz.xefensor.retold.registry.RetoldTags;
import cz.xefensor.retold.behavior.control.RetoldAiControl;
import cz.xefensor.retold.behavior.control.RetoldAiControlMode;
import cz.xefensor.retold.behavior.performance.RetoldAiTickContext;
import cz.xefensor.retold.behavior.core.RetoldBehaviorCoordinator;
import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.tags.ItemTags;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
@@ -689,6 +691,41 @@ public static int foodRelief(
return 20;
}
+ public static int foodRelief(
+ Mob mob,
+ ItemStack stack
+ ) {
+ String mobPath = getEntityTypePath(mob.getType());
+
+ if (isPredator(mobPath)) {
+ return isMeatItem(stack) || isFishItem(stack) ? 28 : 18;
+ }
+
+ if (isGrazer(mobPath)) {
+ String itemPath = getItemPath(stack);
+ return itemPath.equals("hay_block") || itemPath.equals("wheat")
+ ? 28
+ : 20;
+ }
+
+ if (isSmallPassive(mobPath)) {
+ return 20;
+ }
+
+ if (isPandaBamboo(mob)) {
+ return (stack.is(ItemTags.PANDA_FOOD)
+ || getItemPath(stack).equals("bamboo"))
+ ? 28
+ : 16;
+ }
+
+ if (isSlime(mobPath)) {
+ return 18;
+ }
+
+ return 20;
+ }
+
public static int preyRelief(
Mob hunter,
LivingEntity prey
@@ -710,12 +747,7 @@ public static int forageRelief(
String mobPath = getEntityTypePath(mob.getType());
if (isRenewableEnvironmentalForage(mob, blockPath)) {
- return switch (mobPath) {
- case "goat" -> 12;
- case "mooshroom" -> 20;
- case "camel", "rabbit" -> 16;
- default -> 16;
- };
+ return renewableEnvironmentalForageRelief(mob);
}
if (isGrazer(mobPath)) {
@@ -749,6 +781,29 @@ public static int forageRelief(
return 16;
}
+ public static int forageRelief(
+ PathfinderMob mob,
+ BlockState state
+ ) {
+ if (isRenewableEnvironmentalForage(mob, state)) {
+ return renewableEnvironmentalForageRelief(mob);
+ }
+
+ String mobPath = getEntityTypePath(mob.getType());
+ String blockPath = getBlockPath(state);
+
+ if (isGrazer(mobPath) && state.is(RetoldTags.FORAGE_CROPS)) {
+ return 24;
+ }
+
+ if (isSmallPassive(mobPath)
+ && state.is(RetoldTags.FORAGE_CROPS)) {
+ return 22;
+ }
+
+ return forageRelief(mob, blockPath);
+ }
+
public static boolean canEatDroppedItem(
Mob mob,
ItemStack stack
@@ -761,52 +816,58 @@ public static boolean canEatDroppedItem(
String itemPath = getItemPath(stack);
if (mobPath.equals("armadillo")) {
- return itemPath.equals("spider_eye");
+ return stack.is(ItemTags.ARMADILLO_FOOD)
+ || itemPath.equals("spider_eye");
}
if (mobPath.equals("turtle")) {
- return itemPath.equals("seagrass");
+ return stack.is(ItemTags.TURTLE_FOOD)
+ || itemPath.equals("seagrass");
}
if (mobPath.equals("nautilus")) {
- return isFishItem(itemPath);
+ return isFishItem(stack);
}
if (mobPath.equals("bat")) {
- return itemPath.equals("spider_eye");
+ return stack.is(RetoldTags.BAT_FOODS)
+ || itemPath.equals("spider_eye");
}
if (isPredator(mobPath)) {
- return isMeatItem(itemPath)
- || isFishItem(itemPath)
- || (mobPath.equals("fox") && isBerryItem(itemPath))
- || ((mobPath.equals("cat") || mobPath.equals("ocelot")) && itemPath.equals("phantom_membrane"));
+ return isMeatItem(stack)
+ || isFishItem(stack)
+ || (mobPath.equals("fox") && isBerryItem(stack))
+ || ((mobPath.equals("cat") || mobPath.equals("ocelot"))
+ && (stack.is(RetoldTags.FELINE_SCAVENGE_FOODS)
+ || itemPath.equals("phantom_membrane")));
}
if (isGrazer(mobPath)) {
- return isGrazerFoodItem(itemPath);
+ return isGrazerFoodItem(stack);
}
if (isSmallPassive(mobPath)) {
- return isSmallPassiveFoodItem(itemPath);
+ return isSmallPassiveFoodItem(stack);
}
if (mobPath.equals("bee")) {
- return isFlower(itemPath);
+ return isFlowerFoodItem(stack);
}
if (isPandaBamboo(mob)) {
- return itemPath.equals("bamboo");
+ return stack.is(ItemTags.PANDA_FOOD)
+ || itemPath.equals("bamboo");
}
if (isNetherHungry(mobPath)) {
- return isMeatItem(itemPath)
- || isNetherFungusItem(itemPath);
+ return isMeatItem(stack)
+ || isNetherFungusItem(stack);
}
if (isUndeadHungry(mobPath)) {
return itemPath.equals("rotten_flesh")
- || isMeatItem(itemPath);
+ || isMeatItem(stack);
}
if (isSlime(mobPath)) {
@@ -814,7 +875,7 @@ public static boolean canEatDroppedItem(
}
if (mobPath.equals("guardian") || mobPath.equals("elder_guardian")) {
- return isFishItem(itemPath);
+ return isFishItem(stack);
}
return false;
@@ -843,13 +904,18 @@ public static boolean isAnimalFeederFood(ItemStack stack) {
String itemPath = getItemPath(stack);
- return isMeatItem(itemPath)
- || isFishItem(itemPath)
- || isBerryItem(itemPath)
- || isGrazerFoodItem(itemPath)
- || isSmallPassiveFoodItem(itemPath)
- || isFlower(itemPath)
- || isNetherFungusItem(itemPath)
+ return isMeatItem(stack)
+ || isFishItem(stack)
+ || isBerryItem(stack)
+ || isGrazerFoodItem(stack)
+ || isSmallPassiveFoodItem(stack)
+ || isFlowerFoodItem(stack)
+ || isNetherFungusItem(stack)
+ || stack.is(ItemTags.TURTLE_FOOD)
+ || stack.is(ItemTags.ARMADILLO_FOOD)
+ || stack.is(ItemTags.PANDA_FOOD)
+ || stack.is(RetoldTags.BAT_FOODS)
+ || stack.is(RetoldTags.FELINE_SCAVENGE_FOODS)
|| itemPath.equals("seagrass")
|| itemPath.equals("spider_eye")
|| itemPath.equals("bamboo")
@@ -867,16 +933,20 @@ public static boolean canForageBlock(
String mobPath = getEntityTypePath(mob.getType());
String blockPath = getBlockPath(state);
- if (isRenewableEnvironmentalForage(mob, blockPath)) {
+ if (isRenewableEnvironmentalForage(mob, state)) {
return true;
}
if (mobPath.equals("turtle")) {
- return blockPath.equals("seagrass");
+ return state.is(RetoldTags.TURTLE_FORAGE_BLOCKS)
+ || blockPath.equals("seagrass");
}
if (isGrazer(mobPath)) {
- return blockPath.equals("grass_block")
+ return state.is(RetoldTags.GRAZER_FORAGE_PLANTS)
+ || state.is(RetoldTags.FORAGE_CROPS)
+ || state.is(RetoldTags.FORAGE_FLOWERS)
+ || blockPath.equals("grass_block")
|| blockPath.equals("short_grass")
|| blockPath.equals("tall_grass")
|| blockPath.equals("fern")
@@ -886,7 +956,10 @@ public static boolean canForageBlock(
}
if (isSmallPassive(mobPath)) {
- return blockPath.equals("short_grass")
+ return state.is(RetoldTags.SMALL_PASSIVE_FORAGE_PLANTS)
+ || state.is(RetoldTags.FORAGE_CROPS)
+ || state.is(RetoldTags.FORAGE_FLOWERS)
+ || blockPath.equals("short_grass")
|| blockPath.equals("tall_grass")
|| isCropBlock(blockPath)
|| isFlower(blockPath);
@@ -897,17 +970,20 @@ public static boolean canForageBlock(
}
if (mobPath.equals("hoglin")) {
- return blockPath.equals("crimson_fungus");
+ return state.is(RetoldTags.HOGLIN_FORAGE_BLOCKS)
+ || blockPath.equals("crimson_fungus");
}
if (mobPath.equals("piglin")) {
- return blockPath.equals("crimson_fungus")
+ return state.is(RetoldTags.PIGLIN_FORAGE_BLOCKS)
+ || blockPath.equals("crimson_fungus")
|| blockPath.equals("red_mushroom")
|| blockPath.equals("brown_mushroom");
}
if (mobPath.equals("strider")) {
- return blockPath.equals("warped_fungus");
+ return state.is(RetoldTags.STRIDER_FORAGE_BLOCKS)
+ || blockPath.equals("warped_fungus");
}
return false;
@@ -917,12 +993,28 @@ public static boolean isRenewableEnvironmentalForage(
PathfinderMob mob,
BlockState state
) {
- return mob != null
- && state != null
- && isRenewableEnvironmentalForage(
- mob,
- getBlockPath(state)
- );
+ if (mob == null || state == null) {
+ return false;
+ }
+
+ String mobPath = getEntityTypePath(mob.getType());
+
+ if ((mobPath.equals("camel") || mobPath.equals("rabbit"))
+ && state.is(RetoldTags.DESERT_BROWSE_BLOCKS)) {
+ return true;
+ }
+
+ if (mobPath.equals("mooshroom")
+ && state.is(RetoldTags.MOOSHROOM_GRAZING_BLOCKS)) {
+ return true;
+ }
+
+ if (mobPath.equals("goat")
+ && state.is(RetoldTags.GOAT_SCRAPE_BLOCKS)) {
+ return true;
+ }
+
+ return isRenewableEnvironmentalForage(mob, getBlockPath(state));
}
public static boolean usesRenewableEnvironmentalForage(PathfinderMob mob) {
@@ -961,6 +1053,17 @@ private static boolean isRenewableEnvironmentalForage(
return false;
}
+ private static int renewableEnvironmentalForageRelief(
+ PathfinderMob mob
+ ) {
+ return switch (getEntityTypePath(mob.getType())) {
+ case "goat" -> 12;
+ case "mooshroom" -> 20;
+ case "camel", "rabbit" -> 16;
+ default -> 16;
+ };
+ }
+
public static boolean canDigForGrubs(BlockState state) {
if (state == null) {
return false;
@@ -968,7 +1071,8 @@ public static boolean canDigForGrubs(BlockState state) {
String blockPath = getBlockPath(state);
- return blockPath.equals("grass_block")
+ return state.is(RetoldTags.ARMADILLO_GRUB_SOILS)
+ || blockPath.equals("grass_block")
|| blockPath.equals("dirt")
|| blockPath.equals("coarse_dirt")
|| blockPath.equals("rooted_dirt")
@@ -982,9 +1086,10 @@ public static boolean canDigForGrubs(BlockState state) {
public static boolean isFlowerBlock(BlockState state) {
return state != null
- && isFlower(
+ && (state.is(RetoldTags.FORAGE_FLOWERS)
+ || isFlower(
getBlockPath(state)
- );
+ ));
}
public static boolean canHuntPrey(
@@ -1148,6 +1253,11 @@ private static boolean isMeatItem(String itemPath) {
|| itemPath.equals("rotten_flesh");
}
+ private static boolean isMeatItem(ItemStack stack) {
+ return stack.is(RetoldTags.MEAT_FOODS)
+ || isMeatItem(getItemPath(stack));
+ }
+
private static boolean isFishItem(String itemPath) {
return itemPath.equals("cod")
|| itemPath.equals("cooked_cod")
@@ -1157,11 +1267,21 @@ private static boolean isFishItem(String itemPath) {
|| itemPath.equals("pufferfish");
}
+ private static boolean isFishItem(ItemStack stack) {
+ return stack.is(RetoldTags.FISH_FOODS)
+ || isFishItem(getItemPath(stack));
+ }
+
private static boolean isBerryItem(String itemPath) {
return itemPath.equals("sweet_berries")
|| itemPath.equals("glow_berries");
}
+ private static boolean isBerryItem(ItemStack stack) {
+ return stack.is(RetoldTags.BERRY_FOODS)
+ || isBerryItem(getItemPath(stack));
+ }
+
private static boolean isGrazerFoodItem(String itemPath) {
return itemPath.equals("wheat")
|| itemPath.equals("hay_block")
@@ -1176,6 +1296,11 @@ private static boolean isGrazerFoodItem(String itemPath) {
|| itemPath.equals("large_fern");
}
+ private static boolean isGrazerFoodItem(ItemStack stack) {
+ return stack.is(RetoldTags.GRAZER_FOODS)
+ || isGrazerFoodItem(getItemPath(stack));
+ }
+
private static boolean isSmallPassiveFoodItem(String itemPath) {
return itemPath.equals("wheat_seeds")
|| itemPath.equals("beetroot_seeds")
@@ -1187,6 +1312,11 @@ private static boolean isSmallPassiveFoodItem(String itemPath) {
|| itemPath.equals("dandelion");
}
+ private static boolean isSmallPassiveFoodItem(ItemStack stack) {
+ return stack.is(RetoldTags.SMALL_PASSIVE_FOODS)
+ || isSmallPassiveFoodItem(getItemPath(stack));
+ }
+
private static boolean isNetherFungusItem(String itemPath) {
return itemPath.equals("crimson_fungus")
|| itemPath.equals("warped_fungus")
@@ -1194,6 +1324,16 @@ private static boolean isNetherFungusItem(String itemPath) {
|| itemPath.equals("red_mushroom");
}
+ private static boolean isNetherFungusItem(ItemStack stack) {
+ return stack.is(RetoldTags.NETHER_FUNGUS_FOODS)
+ || isNetherFungusItem(getItemPath(stack));
+ }
+
+ private static boolean isFlowerFoodItem(ItemStack stack) {
+ return stack.is(RetoldTags.FLOWER_FOODS)
+ || isFlower(getItemPath(stack));
+ }
+
private static boolean isOrganicItem(String itemPath) {
return isMeatItem(itemPath)
|| isFishItem(itemPath)
diff --git a/src/main/java/cz/xefensor/retold/behavior/species/RetoldArmadilloDefenseEvents.java b/src/main/java/cz/xefensor/retold/behavior/species/RetoldArmadilloDefenseEvents.java
index c0e4c3c..06d681b 100644
--- a/src/main/java/cz/xefensor/retold/behavior/species/RetoldArmadilloDefenseEvents.java
+++ b/src/main/java/cz/xefensor/retold/behavior/species/RetoldArmadilloDefenseEvents.java
@@ -19,6 +19,7 @@
import cz.xefensor.retold.behavior.profiles.RetoldMobRules;
import cz.xefensor.retold.behavior.profiles.RetoldMobState;
import cz.xefensor.retold.behavior.profiles.RetoldMobStates;
+import cz.xefensor.retold.registry.RetoldTags;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
@@ -26,7 +27,6 @@
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.block.Block;
-import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import net.neoforged.bus.api.SubscribeEvent;
@@ -569,14 +569,7 @@ private static boolean isScrubRange(
BlockState state = level.getBlockState(pos);
- return state.is(Blocks.GRASS_BLOCK)
- || state.is(Blocks.DIRT)
- || state.is(Blocks.COARSE_DIRT)
- || state.is(Blocks.ROOTED_DIRT)
- || state.is(Blocks.PODZOL)
- || state.is(Blocks.SAND)
- || state.is(Blocks.RED_SAND)
- || state.is(Blocks.TERRACOTTA);
+ return state.is(RetoldTags.ARMADILLO_SCRUB_RANGE_BLOCKS);
}
private static boolean isScaryMob(LivingEntity entity) {
diff --git a/src/main/java/cz/xefensor/retold/behavior/species/RetoldBatColonyEvents.java b/src/main/java/cz/xefensor/retold/behavior/species/RetoldBatColonyEvents.java
index b7f7cd5..a499679 100644
--- a/src/main/java/cz/xefensor/retold/behavior/species/RetoldBatColonyEvents.java
+++ b/src/main/java/cz/xefensor/retold/behavior/species/RetoldBatColonyEvents.java
@@ -1910,9 +1910,8 @@ private static void consumeDroppedFood(
Vec3 foodSource = food.position();
ItemStack stack = food.getItem();
- String itemPath = RetoldMobRules.getItemPath(stack);
- state.addHunger(-RetoldMobRules.foodRelief(bat, itemPath));
+ state.addHunger(-RetoldMobRules.foodRelief(bat, stack));
state.markFed(gameTime);
bat.ate();
stack.shrink(1);
diff --git a/src/main/java/cz/xefensor/retold/behavior/species/RetoldIllagerRoamingEvents.java b/src/main/java/cz/xefensor/retold/behavior/species/RetoldIllagerRoamingEvents.java
index cb35605..5d4f510 100644
--- a/src/main/java/cz/xefensor/retold/behavior/species/RetoldIllagerRoamingEvents.java
+++ b/src/main/java/cz/xefensor/retold/behavior/species/RetoldIllagerRoamingEvents.java
@@ -9,6 +9,7 @@
import cz.xefensor.retold.behavior.core.RetoldBehaviorMovement;
import cz.xefensor.retold.behavior.core.RetoldBehaviorTiming;
import cz.xefensor.retold.behavior.profiles.RetoldMobRules;
+import cz.xefensor.retold.registry.RetoldTags;
import cz.xefensor.retold.territory.RetoldTerritoryConfig;
import cz.xefensor.retold.territory.RetoldTerritoryConfigs;
@@ -19,7 +20,6 @@
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
-import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.Vec3;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.event.tick.EntityTickEvent;
@@ -225,7 +225,9 @@ private static boolean hasBellNearby(
for (int dy = -BELL_SIGNAL_VERTICAL_RADIUS; dy <= BELL_SIGNAL_VERTICAL_RADIUS; dy++) {
BlockPos pos = center.offset(dx, dy, dz);
- if (level.getBlockState(pos).is(Blocks.BELL)) {
+ if (level.getBlockState(pos).is(
+ RetoldTags.ILLAGER_VILLAGE_SIGNAL_BLOCKS
+ )) {
return true;
}
}
diff --git a/src/main/java/cz/xefensor/retold/behavior/species/RetoldPandaBambooEvents.java b/src/main/java/cz/xefensor/retold/behavior/species/RetoldPandaBambooEvents.java
index 80f68d6..72ed788 100644
--- a/src/main/java/cz/xefensor/retold/behavior/species/RetoldPandaBambooEvents.java
+++ b/src/main/java/cz/xefensor/retold/behavior/species/RetoldPandaBambooEvents.java
@@ -19,12 +19,12 @@
import cz.xefensor.retold.behavior.profiles.RetoldMobRules;
import cz.xefensor.retold.behavior.profiles.RetoldMobState;
import cz.xefensor.retold.behavior.profiles.RetoldMobStates;
+import cz.xefensor.retold.registry.RetoldTags;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
-import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.Vec3;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.event.tick.EntityTickEvent;
@@ -377,7 +377,7 @@ static boolean tryNibbleBamboo(
) {
if (!RetoldBehaviorCoordinator.canCompleteMeal(panda)
|| !isBamboo(level, foodSource)
- || !RetoldMobGriefing.canModifyBlocks(level, panda)
+ || !RetoldMobGriefing.canBreakBlock(level, panda, foodSource)
|| !level.destroyBlock(foodSource, false, panda)) {
return false;
}
@@ -516,7 +516,6 @@ private static boolean isBamboo(
ServerLevel level,
BlockPos pos
) {
- return level.getBlockState(pos).is(Blocks.BAMBOO)
- || level.getBlockState(pos).is(Blocks.BAMBOO_SAPLING);
+ return level.getBlockState(pos).is(RetoldTags.PANDA_BAMBOO_BLOCKS);
}
}
diff --git a/src/main/java/cz/xefensor/retold/behavior/species/RetoldSpiderLairEvents.java b/src/main/java/cz/xefensor/retold/behavior/species/RetoldSpiderLairEvents.java
index 47f9512..e512b6d 100644
--- a/src/main/java/cz/xefensor/retold/behavior/species/RetoldSpiderLairEvents.java
+++ b/src/main/java/cz/xefensor/retold/behavior/species/RetoldSpiderLairEvents.java
@@ -17,6 +17,7 @@
import cz.xefensor.retold.behavior.profiles.RetoldMobRules;
import cz.xefensor.retold.behavior.profiles.RetoldMobState;
import cz.xefensor.retold.behavior.profiles.RetoldMobStates;
+import cz.xefensor.retold.registry.RetoldTags;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
@@ -181,7 +182,8 @@ static boolean tryBuildOrRepairLair(
if (placement == null
|| placement.equals(spider.blockPosition())
- || !RetoldBlockTargetSearch.canPlaceCobwebAt(level, placement)) {
+ || !RetoldBlockTargetSearch.canPlaceCobwebAt(level, placement)
+ || !RetoldMobGriefing.canPlaceBlock(level, spider, placement)) {
return false;
}
@@ -251,7 +253,9 @@ static int countCobwebs(
center.getZ() + dz
);
- if (level.getBlockState(mutable).is(Blocks.COBWEB)) {
+ if (level.getBlockState(mutable).is(
+ RetoldTags.SPIDER_LAIR_WEB_BLOCKS
+ )) {
count++;
}
}
diff --git a/src/main/java/cz/xefensor/retold/behavior/species/RetoldTerritoryGuardEvents.java b/src/main/java/cz/xefensor/retold/behavior/species/RetoldTerritoryGuardEvents.java
index 80a5b29..3f64595 100644
--- a/src/main/java/cz/xefensor/retold/behavior/species/RetoldTerritoryGuardEvents.java
+++ b/src/main/java/cz/xefensor/retold/behavior/species/RetoldTerritoryGuardEvents.java
@@ -11,12 +11,12 @@
import cz.xefensor.retold.combat.RetoldFactionTargetMemory;
import cz.xefensor.retold.combat.RetoldTargetSource;
+import cz.xefensor.retold.registry.RetoldTags;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
-import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.event.tick.EntityTickEvent;
@@ -520,32 +520,11 @@ private static boolean isStructureAnchorBlock(
String path = RetoldMobRules.getEntityTypePath(guard.getType());
if (path.equals("wither_skeleton") || path.equals("blaze")) {
- return state.is(Blocks.NETHER_BRICKS)
- || state.is(Blocks.NETHER_BRICK_FENCE)
- || state.is(Blocks.NETHER_BRICK_STAIRS)
- || state.is(Blocks.NETHER_BRICK_SLAB)
- || state.is(Blocks.CRACKED_NETHER_BRICKS)
- || state.is(Blocks.CHISELED_NETHER_BRICKS)
- || state.is(Blocks.RED_NETHER_BRICKS)
- || state.is(Blocks.RED_NETHER_BRICK_STAIRS)
- || state.is(Blocks.RED_NETHER_BRICK_SLAB)
- || state.is(Blocks.RED_NETHER_BRICK_WALL)
- || state.is(Blocks.NETHER_BRICK_WALL);
+ return state.is(RetoldTags.NETHER_REMNANT_GUARD_ANCHOR_BLOCKS);
}
if (isOceanMonumentGuard(path)) {
- return state.is(Blocks.PRISMARINE)
- || state.is(Blocks.PRISMARINE_BRICKS)
- || state.is(Blocks.DARK_PRISMARINE)
- || state.is(Blocks.SEA_LANTERN)
- || state.is(Blocks.WET_SPONGE)
- || state.is(Blocks.PRISMARINE_STAIRS)
- || state.is(Blocks.PRISMARINE_SLAB)
- || state.is(Blocks.PRISMARINE_WALL)
- || state.is(Blocks.PRISMARINE_BRICK_STAIRS)
- || state.is(Blocks.PRISMARINE_BRICK_SLAB)
- || state.is(Blocks.DARK_PRISMARINE_STAIRS)
- || state.is(Blocks.DARK_PRISMARINE_SLAB);
+ return state.is(RetoldTags.OCEAN_MONUMENT_GUARD_ANCHOR_BLOCKS);
}
return false;
diff --git a/src/main/java/cz/xefensor/retold/behavior/species/RetoldTurtleBeachEvents.java b/src/main/java/cz/xefensor/retold/behavior/species/RetoldTurtleBeachEvents.java
index ea66099..4101074 100644
--- a/src/main/java/cz/xefensor/retold/behavior/species/RetoldTurtleBeachEvents.java
+++ b/src/main/java/cz/xefensor/retold/behavior/species/RetoldTurtleBeachEvents.java
@@ -17,6 +17,7 @@
import cz.xefensor.retold.behavior.profiles.RetoldMobRules;
import cz.xefensor.retold.behavior.profiles.RetoldMobState;
import cz.xefensor.retold.behavior.profiles.RetoldMobStates;
+import cz.xefensor.retold.registry.RetoldTags;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
@@ -24,7 +25,6 @@
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.player.Player;
-import net.minecraft.world.level.block.Blocks;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.event.tick.EntityTickEvent;
@@ -491,8 +491,7 @@ private static boolean isSand(
ServerLevel level,
BlockPos pos
) {
- return level.getBlockState(pos).is(Blocks.SAND)
- || level.getBlockState(pos).is(Blocks.RED_SAND);
+ return level.getBlockState(pos).is(RetoldTags.TURTLE_BEACH_BLOCKS);
}
private static boolean isWater(
diff --git a/src/main/java/cz/xefensor/retold/client/RetoldClientEvents.java b/src/main/java/cz/xefensor/retold/client/RetoldClientEvents.java
index 2a195a7..271d252 100644
--- a/src/main/java/cz/xefensor/retold/client/RetoldClientEvents.java
+++ b/src/main/java/cz/xefensor/retold/client/RetoldClientEvents.java
@@ -5,6 +5,7 @@
import cz.xefensor.retold.client.enchanting.RetoldClientEnchantmentCatalog;
import cz.xefensor.retold.client.enchanting.RetoldClientEnchantmentKnowledge;
import cz.xefensor.retold.client.enchanting.RetoldEnchantmentTooltip;
+import cz.xefensor.retold.client.recipe.RetoldClientRecipeKnowledge;
import cz.xefensor.retold.client.render.GaleCoreRenderer;
import cz.xefensor.retold.client.render.RetoldAenderEyeRenderer;
import cz.xefensor.retold.client.render.RetoldEndermanEyesLayer;
@@ -64,6 +65,7 @@ public static void register(IEventBus modEventBus) {
private static void onClientLogout(ClientPlayerNetworkEvent.LoggingOut event) {
RetoldClientEnchantmentCatalog.clear();
RetoldClientEnchantmentKnowledge.clear();
+ RetoldClientRecipeKnowledge.clear();
}
private static void registerSpriteSources(RegisterSpriteSourcesEvent event) {
diff --git a/src/main/java/cz/xefensor/retold/client/recipe/RetoldClientRecipeKnowledge.java b/src/main/java/cz/xefensor/retold/client/recipe/RetoldClientRecipeKnowledge.java
new file mode 100644
index 0000000..d791074
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/client/recipe/RetoldClientRecipeKnowledge.java
@@ -0,0 +1,31 @@
+package cz.xefensor.retold.client.recipe;
+
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.world.item.crafting.Recipe;
+
+import java.util.Collection;
+import java.util.Set;
+
+/** Read-only client snapshot used by recipe UIs and optional viewer integrations. */
+public final class RetoldClientRecipeKnowledge {
+ private static Set>> knownRecipes = Set.of();
+
+ private RetoldClientRecipeKnowledge() {
+ }
+
+ public static boolean isKnown(ResourceKey> recipeId) {
+ return recipeId != null && knownRecipes.contains(recipeId);
+ }
+
+ public static Set>> snapshot() {
+ return knownRecipes;
+ }
+
+ public static void replace(Collection>> recipeIds) {
+ knownRecipes = Set.copyOf(recipeIds);
+ }
+
+ public static void clear() {
+ knownRecipes = Set.of();
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/compatibility/RetoldWorldProtectionGameTests.java b/src/main/java/cz/xefensor/retold/compatibility/RetoldWorldProtectionGameTests.java
new file mode 100644
index 0000000..bd607b0
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/compatibility/RetoldWorldProtectionGameTests.java
@@ -0,0 +1,154 @@
+package cz.xefensor.retold.compatibility;
+
+import cz.xefensor.retold.Retold;
+import cz.xefensor.retold.api.world.RetoldWorldMutationType;
+import cz.xefensor.retold.api.world.RetoldWorldProtection;
+import cz.xefensor.retold.behavior.food.RetoldFoodBehaviorEvents;
+import cz.xefensor.retold.behavior.profiles.RetoldMobState;
+import cz.xefensor.retold.behavior.profiles.RetoldMobStates;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Holder;
+import net.minecraft.gametest.framework.BuiltinTestFunctions;
+import net.minecraft.gametest.framework.FunctionGameTestInstance;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.gametest.framework.TestData;
+import net.minecraft.gametest.framework.TestEnvironmentDefinition;
+import net.minecraft.resources.Identifier;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.entity.animal.cow.Cow;
+import net.minecraft.world.level.block.Blocks;
+import net.neoforged.neoforge.event.RegisterGameTestsEvent;
+
+import java.util.function.Consumer;
+
+public final class RetoldWorldProtectionGameTests {
+ private static final Identifier EMPTY_STRUCTURE =
+ Identifier.withDefaultNamespace("empty");
+ private static final Identifier TEST_INTEGRATION =
+ Identifier.fromNamespaceAndPath("examplemod", "test_claims");
+
+ private RetoldWorldProtectionGameTests() {
+ }
+
+ public static void register(
+ RegisterGameTestsEvent event,
+ Holder> environment
+ ) {
+ TestData>> testData =
+ new TestData<>(environment, EMPTY_STRUCTURE, 40, 0, true);
+
+ event.registerTest(
+ Identifier.fromNamespaceAndPath(
+ Retold.MODID,
+ "world_protection_rules_preserve_defaults_and_block_mutations"
+ ),
+ new InlineGameTest(
+ testData,
+ RetoldWorldProtectionGameTests::rulesPreserveDefaultsAndBlockMutations
+ )
+ );
+ }
+
+ private static void rulesPreserveDefaultsAndBlockMutations(
+ GameTestHelper helper
+ ) {
+ ServerLevel level = helper.getLevel();
+ long gameTime = level.getGameTime();
+ Cow cow = helper.spawn(EntityTypes.COW, 4, 2, 4);
+ BlockPos relativeForage = new BlockPos(5, 2, 4);
+ BlockPos forage = helper.absolutePos(relativeForage);
+ RetoldMobState state = RetoldMobStates.getOrCreate(cow, gameTime);
+ state.setHunger(100);
+ helper.setBlock(relativeForage, Blocks.GRASS_BLOCK);
+
+ helper.assertTrue(
+ RetoldWorldProtection.canMobBreak(level, forage, cow),
+ "No installed protection rules must preserve Retold's default allow behavior"
+ );
+
+ RetoldWorldProtection.Registration registration =
+ RetoldWorldProtection.register(
+ TEST_INTEGRATION,
+ context -> context.level() != level
+ || !context.pos().equals(forage)
+ || context.type() != RetoldWorldMutationType.MOB_BREAK
+ );
+
+ try {
+ helper.assertFalse(
+ RetoldFoodBehaviorEvents.tryConsumeForageBlock(
+ level,
+ cow,
+ forage,
+ gameTime
+ ),
+ "A denied mob-break mutation must not consume forage"
+ );
+ helper.assertTrue(
+ level.getBlockState(forage).is(Blocks.GRASS_BLOCK),
+ "Denied forage must leave the world unchanged"
+ );
+ helper.assertTrue(
+ state.hunger() == 100,
+ "Denied forage must not grant food before the world mutation succeeds"
+ );
+ helper.assertTrue(
+ RetoldWorldProtection.canWorldModify(level, forage, null),
+ "Rules must be able to distinguish mutation categories"
+ );
+
+ boolean duplicateRejected = false;
+
+ try {
+ RetoldWorldProtection.register(TEST_INTEGRATION, ignored -> true);
+ } catch (IllegalStateException expected) {
+ duplicateRejected = true;
+ }
+
+ helper.assertTrue(
+ duplicateRejected,
+ "Duplicate integration identifiers must not replace an existing rule silently"
+ );
+ } finally {
+ registration.close();
+ }
+
+ helper.assertTrue(
+ RetoldFoodBehaviorEvents.tryConsumeForageBlock(
+ level,
+ cow,
+ forage,
+ gameTime
+ ),
+ "Closing a rule registration must restore standalone behavior"
+ );
+ helper.assertTrue(
+ level.getBlockState(forage).is(Blocks.DIRT),
+ "The existing grass-forage mutation must remain unchanged when allowed"
+ );
+ helper.assertTrue(
+ state.hunger() < 100,
+ "Allowed forage must retain its existing hunger relief"
+ );
+ helper.succeed();
+ }
+
+ private static final class InlineGameTest extends FunctionGameTestInstance {
+ private final Consumer test;
+
+ private InlineGameTest(
+ TestData>> testData,
+ Consumer test
+ ) {
+ super(BuiltinTestFunctions.ALWAYS_PASS, testData);
+ this.test = test;
+ }
+
+ @Override
+ public void run(GameTestHelper helper) {
+ test.accept(helper);
+ }
+
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/event/RetoldFactionCombatEvents.java b/src/main/java/cz/xefensor/retold/event/RetoldFactionCombatEvents.java
index 077cdde..38a4ff3 100644
--- a/src/main/java/cz/xefensor/retold/event/RetoldFactionCombatEvents.java
+++ b/src/main/java/cz/xefensor/retold/event/RetoldFactionCombatEvents.java
@@ -12,21 +12,25 @@
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.PathfinderMob;
+import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal;
import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal;
import net.minecraft.world.entity.raid.Raid;
import net.minecraft.world.phys.AABB;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent;
+import net.neoforged.neoforge.event.entity.EntityLeaveLevelEvent;
import net.neoforged.neoforge.event.tick.EntityTickEvent;
-import java.util.Collections;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
import java.util.Map;
-import java.util.Set;
+import java.util.UUID;
import java.util.WeakHashMap;
public final class RetoldFactionCombatEvents {
- private static final Set PATCHED_ENTITIES = Collections.newSetFromMap(new WeakHashMap<>());
+ private static final Map> FACTION_TARGET_GOALS = new HashMap<>();
+ private static final Map> RETALIATION_GOALS = new HashMap<>();
private static final int FORCED_TARGET_CHECK_INTERVAL_TICKS = 10;
private static final int FORCED_TARGET_REFRESH_INTERVAL_TICKS = 20;
@@ -49,24 +53,19 @@ public static void onEntityJoinLevel(EntityJoinLevelEvent event) {
return;
}
- if (!PATCHED_ENTITIES.add(entity)) {
- return;
+ if (entity instanceof Mob mob) {
+ updateFactionGoals(mob);
}
+ }
- if (entity instanceof Mob) {
- Mob mob = (Mob) entity;
-
- if (RetoldFactionRelations.hasPotentialFactionTarget(mob)) {
- addFactionTargetGoal(mob);
- }
+ @SubscribeEvent
+ public static void onEntityLeaveLevel(EntityLeaveLevelEvent event) {
+ if (event.getLevel().isClientSide()) {
+ return;
}
- if (entity instanceof PathfinderMob) {
- PathfinderMob pathfinderMob = (PathfinderMob) entity;
-
- if (RetoldFactionMembers.hasFaction(pathfinderMob)) {
- addRetaliationGoal(pathfinderMob);
- }
+ if (event.getEntity() instanceof Mob mob) {
+ removeFactionGoals(mob);
}
}
@@ -91,6 +90,7 @@ public static void onEntityTickPost(EntityTickEvent.Post event) {
ServerLevel level = (ServerLevel) mob.level();
+ updateFactionGoals(mob);
RetoldFactionTargetMemory.cleanupTargetState(mob);
if (!RetoldFactionRelations.hasPotentialFactionTarget(mob)) {
@@ -114,18 +114,65 @@ public static void onEntityTickPost(EntityTickEvent.Post event) {
updateForcedTarget(level, mob, gameTime);
}
- private static void addFactionTargetGoal(Mob mob) {
- mob.targetSelector.addGoal(
- 2,
- new NearestAttackableTargetGoal<>(
- mob,
- LivingEntity.class,
- 10,
- true,
- false,
- (target, level) -> isValidFactionTarget(mob, target)
- )
- );
+ private static void updateFactionGoals(Mob mob) {
+ boolean needsFactionTargetGoal = RetoldFactionRelations.hasPotentialFactionTarget(mob);
+ UUID entityId = mob.getUUID();
+ Goal factionTargetGoal = getGoal(FACTION_TARGET_GOALS.get(entityId));
+
+ if (needsFactionTargetGoal && factionTargetGoal == null) {
+ factionTargetGoal = new NearestAttackableTargetGoal<>(
+ mob,
+ LivingEntity.class,
+ 10,
+ true,
+ false,
+ (target, level) -> isValidFactionTarget(mob, target)
+ );
+ FACTION_TARGET_GOALS.put(entityId, new WeakReference<>(factionTargetGoal));
+ mob.targetSelector.addGoal(2, factionTargetGoal);
+ } else if (!needsFactionTargetGoal && factionTargetGoal != null) {
+ mob.targetSelector.removeGoal(factionTargetGoal);
+ FACTION_TARGET_GOALS.remove(entityId);
+ }
+
+ if (!(mob instanceof PathfinderMob pathfinderMob)) {
+ return;
+ }
+
+ boolean needsRetaliationGoal = RetoldFactionMembers.hasFaction(pathfinderMob);
+ Goal retaliationGoal = getGoal(RETALIATION_GOALS.get(entityId));
+
+ if (needsRetaliationGoal && retaliationGoal == null) {
+ retaliationGoal = new HurtByTargetGoal(pathfinderMob);
+ RETALIATION_GOALS.put(entityId, new WeakReference<>(retaliationGoal));
+ pathfinderMob.targetSelector.addGoal(1, retaliationGoal);
+ } else if (!needsRetaliationGoal && retaliationGoal != null) {
+ pathfinderMob.targetSelector.removeGoal(retaliationGoal);
+ RETALIATION_GOALS.remove(entityId);
+ }
+ }
+
+ private static void removeFactionGoals(Mob mob) {
+ UUID entityId = mob.getUUID();
+ Goal factionTargetGoal = getGoal(FACTION_TARGET_GOALS.remove(entityId));
+
+ if (factionTargetGoal != null) {
+ mob.targetSelector.removeGoal(factionTargetGoal);
+ }
+
+ if (!(mob instanceof PathfinderMob pathfinderMob)) {
+ return;
+ }
+
+ Goal retaliationGoal = getGoal(RETALIATION_GOALS.remove(entityId));
+
+ if (retaliationGoal != null) {
+ pathfinderMob.targetSelector.removeGoal(retaliationGoal);
+ }
+ }
+
+ private static Goal getGoal(WeakReference reference) {
+ return reference == null ? null : reference.get();
}
private static boolean isValidFactionTarget(Mob mob, LivingEntity target) {
@@ -168,10 +215,6 @@ private static boolean isValidFactionTarget(Mob mob, LivingEntity target) {
return mob.getSensing().hasLineOfSight(target);
}
- private static void addRetaliationGoal(PathfinderMob mob) {
- mob.targetSelector.addGoal(1, new HurtByTargetGoal(mob));
- }
-
private static void updateForcedTarget(ServerLevel level, Mob mob, long gameTime) {
LivingEntity currentForcedTarget = FORCED_TARGETS.get(mob);
diff --git a/src/main/java/cz/xefensor/retold/event/RetoldOceanMonumentSupport.java b/src/main/java/cz/xefensor/retold/event/RetoldOceanMonumentSupport.java
index 4308385..7ba37d7 100644
--- a/src/main/java/cz/xefensor/retold/event/RetoldOceanMonumentSupport.java
+++ b/src/main/java/cz/xefensor/retold/event/RetoldOceanMonumentSupport.java
@@ -1,11 +1,11 @@
package cz.xefensor.retold.event;
+import cz.xefensor.retold.registry.RetoldTags;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.monster.ElderGuardian;
-import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BuiltinStructures;
import net.minecraft.world.level.levelgen.structure.Structure;
@@ -27,11 +27,7 @@ static StructureStart findAt(ServerLevel level, BlockPos pos) {
}
static boolean isProtectedBlock(BlockState state) {
- return state.is(Blocks.PRISMARINE)
- || state.is(Blocks.PRISMARINE_BRICKS)
- || state.is(Blocks.DARK_PRISMARINE)
- || state.is(Blocks.SEA_LANTERN)
- || state.is(Blocks.WET_SPONGE);
+ return state.is(RetoldTags.OCEAN_MONUMENT_PROTECTED_BLOCKS);
}
static boolean isValidMonumentAt(ServerLevel level, BlockPos pos) {
diff --git a/src/main/java/cz/xefensor/retold/event/RetoldPlayerSyncEvents.java b/src/main/java/cz/xefensor/retold/event/RetoldPlayerSyncEvents.java
index e8a2ce8..490354c 100644
--- a/src/main/java/cz/xefensor/retold/event/RetoldPlayerSyncEvents.java
+++ b/src/main/java/cz/xefensor/retold/event/RetoldPlayerSyncEvents.java
@@ -3,6 +3,7 @@
import cz.xefensor.retold.enchanting.RetoldEnchantmentKnowledge;
import cz.xefensor.retold.network.RetoldEndSkySeedSyncPayload;
import cz.xefensor.retold.network.RetoldStageSyncPayload;
+import cz.xefensor.retold.recipe.RetoldRecipeBookEvents;
import cz.xefensor.retold.sky.RetoldEndSkyData;
import cz.xefensor.retold.stage.RetoldWorldData;
import net.minecraft.server.level.ServerLevel;
@@ -62,6 +63,7 @@ private static void syncPlayer(PlayerEvent event) {
);
RetoldEnchantmentKnowledge.syncToPlayer(serverPlayer);
+ RetoldRecipeBookEvents.syncToPlayer(serverPlayer);
AenderChronolithEvents.syncToPlayer(serverPlayer);
}
}
diff --git a/src/main/java/cz/xefensor/retold/faction/RetoldFactionMembers.java b/src/main/java/cz/xefensor/retold/faction/RetoldFactionMembers.java
index e337278..44397b0 100644
--- a/src/main/java/cz/xefensor/retold/faction/RetoldFactionMembers.java
+++ b/src/main/java/cz/xefensor/retold/faction/RetoldFactionMembers.java
@@ -1,57 +1,74 @@
package cz.xefensor.retold.faction;
+import cz.xefensor.retold.Retold;
+import cz.xefensor.retold.registry.RetoldTags;
+
import net.minecraft.core.registries.BuiltInRegistries;
-import net.minecraft.resources.Identifier;
+import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.TamableAnimal;
+import net.minecraft.world.entity.animal.equine.AbstractHorse;
import net.minecraft.world.entity.animal.wolf.Wolf;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.raid.Raid;
import net.minecraft.world.entity.raid.Raider;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.neoforge.event.TagsUpdatedEvent;
-import java.util.HashMap;
+import java.util.EnumMap;
+import java.util.EnumSet;
import java.util.Map;
-import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
public final class RetoldFactionMembers {
- private static final Map EXACT_MEMBERS = new HashMap<>();
-
- private static final Set ILLAGER_LOOSE_ALLIES = Set.of(
- id("witch")
- );
+ private static final Map>> FACTION_TAGS =
+ new EnumMap<>(RetoldFaction.class);
+ private static final Map, StaticMembership> STATIC_MEMBERSHIPS =
+ new ConcurrentHashMap<>();
static {
- register(RetoldFaction.NETHER_REMNANTS, "piglin", "piglin_brute", "blaze");
- register(RetoldFaction.ILLAGERS, "pillager", "vindicator", "evoker", "illusioner", "ravager", "vex");
- register(
- RetoldFaction.UNDEAD,
- "zombie",
- "zombie_villager",
- "husk",
- "drowned",
- "skeleton",
- "stray",
- "bogged",
- "wither_skeleton",
- "zombified_piglin",
- "phantom",
- "ghast",
- "zoglin",
- "wither"
+ FACTION_TAGS.put(
+ RetoldFaction.NETHER_REMNANTS,
+ RetoldTags.FACTION_NETHER_REMNANTS
+ );
+ FACTION_TAGS.put(RetoldFaction.ILLAGERS, RetoldTags.FACTION_ILLAGERS);
+ FACTION_TAGS.put(RetoldFaction.UNDEAD, RetoldTags.FACTION_UNDEAD);
+ FACTION_TAGS.put(RetoldFaction.SLIMES, RetoldTags.FACTION_SLIMES);
+ FACTION_TAGS.put(
+ RetoldFaction.AQUATIC_HOSTILES,
+ RetoldTags.FACTION_AQUATIC_HOSTILES
+ );
+ FACTION_TAGS.put(RetoldFaction.CREEPERS, RetoldTags.FACTION_CREEPERS);
+ FACTION_TAGS.put(
+ RetoldFaction.ARTHROPODS,
+ RetoldTags.FACTION_ARTHROPODS
+ );
+ FACTION_TAGS.put(
+ RetoldFaction.SILVERFISH,
+ RetoldTags.FACTION_SILVERFISH
+ );
+ FACTION_TAGS.put(
+ RetoldFaction.ENDERMITES,
+ RetoldTags.FACTION_ENDERMITES
+ );
+ FACTION_TAGS.put(
+ RetoldFaction.NETHER_BEASTS,
+ RetoldTags.FACTION_NETHER_BEASTS
);
- register(RetoldFaction.SLIMES, "slime", "magma_cube");
- register(RetoldFaction.AQUATIC_HOSTILES, "guardian", "elder_guardian");
- register(RetoldFaction.CREEPERS, "creeper");
- register(RetoldFaction.ARTHROPODS, "spider", "cave_spider");
- register(RetoldFaction.SILVERFISH, "silverfish");
- register(RetoldFaction.ENDERMITES, "endermite");
- register(RetoldFaction.NETHER_BEASTS, "hoglin");
- register(RetoldFaction.BREEZES, "breeze");
- register(RetoldFaction.WARDENS, "warden");
- register(RetoldFaction.BOSSES, "ender_dragon");
- register(RetoldFaction.CREAKINGS, "creaking");
- register(RetoldFaction.VILLAGE_DEFENDERS, "iron_golem", "snow_golem");
- register(RetoldFaction.ENDERS, "enderman", "shulker");
+ FACTION_TAGS.put(RetoldFaction.BREEZES, RetoldTags.FACTION_BREEZES);
+ FACTION_TAGS.put(RetoldFaction.WARDENS, RetoldTags.FACTION_WARDENS);
+ FACTION_TAGS.put(RetoldFaction.BOSSES, RetoldTags.FACTION_BOSSES);
+ FACTION_TAGS.put(
+ RetoldFaction.CREAKINGS,
+ RetoldTags.FACTION_CREAKINGS
+ );
+ FACTION_TAGS.put(
+ RetoldFaction.VILLAGE_DEFENDERS,
+ RetoldTags.FACTION_VILLAGE_DEFENDERS
+ );
+ FACTION_TAGS.put(RetoldFaction.ENDERS, RetoldTags.FACTION_ENDERS);
}
private RetoldFactionMembers() {
@@ -70,7 +87,34 @@ public static RetoldFaction getFaction(Entity entity) {
return RetoldFaction.VILLAGE_DEFENDERS;
}
- return EXACT_MEMBERS.get(BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()));
+ RetoldFaction faction = getFaction(entity.getType());
+
+ if (faction == RetoldFaction.UNDEAD && isTamedUndeadMount(entity)) {
+ return null;
+ }
+
+ return faction;
+ }
+
+ public static RetoldFaction getFaction(EntityType> entityType) {
+ if (entityType == null) {
+ return null;
+ }
+
+ return staticMembership(entityType).faction();
+ }
+
+ public static TagKey> getFactionTag(RetoldFaction faction) {
+ if (faction == null) {
+ return null;
+ }
+
+ return FACTION_TAGS.get(faction);
+ }
+
+ public static boolean hasConflictingFactionTags(EntityType> entityType) {
+ return entityType != null
+ && staticMembership(entityType).conflictingFactionTags();
}
public static boolean isMemberOf(Entity entity, RetoldFaction faction) {
@@ -95,14 +139,15 @@ public static boolean isAlignedWith(Entity entity, RetoldFaction faction) {
}
public static boolean isLooseAllyOf(Entity entity, RetoldFaction faction) {
- if (entity == null || faction == null) {
+ if (entity == null || faction != RetoldFaction.ILLAGERS) {
return false;
}
- Identifier id = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType());
+ StaticMembership membership = staticMembership(entity.getType());
- return faction == RetoldFaction.ILLAGERS
- && ILLAGER_LOOSE_ALLIES.contains(id);
+ return membership.faction() == null
+ && !membership.conflictingFactionTags()
+ && membership.illagerLooseAlly();
}
public static boolean isCombatAlignedWith(
@@ -218,6 +263,65 @@ public static boolean isVillageDefender(Entity entity) {
return isMemberOf(entity, RetoldFaction.VILLAGE_DEFENDERS);
}
+ @SubscribeEvent
+ public static void onTagsUpdated(TagsUpdatedEvent event) {
+ STATIC_MEMBERSHIPS.clear();
+
+ if (!(event instanceof TagsUpdatedEvent.ServerDataLoad)) {
+ return;
+ }
+
+ for (EntityType> entityType : BuiltInRegistries.ENTITY_TYPE) {
+ staticMembership(entityType);
+ }
+ }
+
+ private static StaticMembership staticMembership(EntityType> entityType) {
+ return STATIC_MEMBERSHIPS.computeIfAbsent(
+ entityType,
+ RetoldFactionMembers::classify
+ );
+ }
+
+ private static StaticMembership classify(EntityType> entityType) {
+ EnumSet matches = EnumSet.noneOf(RetoldFaction.class);
+
+ for (Map.Entry>> entry
+ : FACTION_TAGS.entrySet()) {
+ if (entityType.builtInRegistryHolder().is(entry.getValue())) {
+ matches.add(entry.getKey());
+ }
+ }
+
+ boolean looseAlly = entityType.builtInRegistryHolder().is(
+ RetoldTags.ILLAGER_LOOSE_ALLIES
+ );
+
+ if (matches.size() > 1) {
+ Retold.LOGGER.error(
+ "Entity type {} belongs to conflicting Retold faction tags {}; "
+ + "treating it as unfactioned",
+ BuiltInRegistries.ENTITY_TYPE.getKey(entityType),
+ matches
+ );
+ return new StaticMembership(null, true, false);
+ }
+
+ RetoldFaction faction = matches.isEmpty() ? null : matches.iterator().next();
+
+ if (faction != null && looseAlly) {
+ Retold.LOGGER.warn(
+ "Entity type {} is both a full {} member and an Illager loose ally; "
+ + "full faction membership takes precedence",
+ BuiltInRegistries.ENTITY_TYPE.getKey(entityType),
+ faction
+ );
+ looseAlly = false;
+ }
+
+ return new StaticMembership(faction, false, looseAlly);
+ }
+
private static boolean isDefendingTamedWolf(Entity entity) {
if (!(entity instanceof Wolf wolf)) {
return false;
@@ -234,16 +338,19 @@ private static boolean isDefendingTamedWolf(Entity entity) {
&& target.level() == wolf.level();
}
- private static Identifier id(String path) {
- return Identifier.fromNamespaceAndPath("minecraft", path);
+ private static boolean isTamedUndeadMount(Entity entity) {
+ if (entity instanceof AbstractHorse horse) {
+ return horse.isTamed();
+ }
+
+ return entity instanceof TamableAnimal tamableAnimal
+ && tamableAnimal.isTame();
}
- private static void register(
+ private record StaticMembership(
RetoldFaction faction,
- String... paths
+ boolean conflictingFactionTags,
+ boolean illagerLooseAlly
) {
- for (String path : paths) {
- EXACT_MEMBERS.put(id(path), faction);
- }
}
}
diff --git a/src/main/java/cz/xefensor/retold/gametest/RetoldGameTests.java b/src/main/java/cz/xefensor/retold/gametest/RetoldGameTests.java
index 55412e1..643c96f 100644
--- a/src/main/java/cz/xefensor/retold/gametest/RetoldGameTests.java
+++ b/src/main/java/cz/xefensor/retold/gametest/RetoldGameTests.java
@@ -42,6 +42,7 @@
import cz.xefensor.retold.combat.RetoldFactionTargetMemory;
import cz.xefensor.retold.combat.RetoldCombatTargets;
import cz.xefensor.retold.combat.RetoldTargetSource;
+import cz.xefensor.retold.compatibility.RetoldWorldProtectionGameTests;
import cz.xefensor.retold.enderman.RetoldEndermanDefense;
import cz.xefensor.retold.enchanting.RetoldEnchantingGameTests;
import cz.xefensor.retold.faction.RetoldFaction;
@@ -51,8 +52,10 @@
import cz.xefensor.retold.event.RetoldSnowballGameTests;
import cz.xefensor.retold.event.RetoldVexGameTests;
import cz.xefensor.retold.progression.RetoldToolProgressionGameTests;
+import cz.xefensor.retold.recipe.RetoldRecipeCompatibilityGameTests;
import cz.xefensor.retold.progression.RetoldProgressionAcquisitionGameTests;
import cz.xefensor.retold.registry.RetoldBlocks;
+import cz.xefensor.retold.registry.RetoldTags;
import cz.xefensor.retold.stage.RetoldElementType;
import cz.xefensor.retold.stage.RetoldRaidProgression;
import cz.xefensor.retold.stage.RetoldStageManager;
@@ -74,6 +77,7 @@
import cz.xefensor.retold.worldgen.air.RetoldGaleCoreGameTests;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
+import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.gametest.framework.BuiltinTestFunctions;
import net.minecraft.gametest.framework.FunctionGameTestInstance;
import net.minecraft.gametest.framework.GameTestHelper;
@@ -82,7 +86,9 @@
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.monster.Guardian;
@@ -104,6 +110,7 @@
import java.util.EnumSet;
import java.util.List;
+import java.util.Map;
import java.util.function.Consumer;
public final class RetoldGameTests {
@@ -180,6 +187,18 @@ public static void register(RegisterGameTestsEvent event) {
"indiscriminate_factions_follow_living_target_rules",
RetoldGameTests::indiscriminateFactionsFollowLivingTargetRules
);
+ registerTest(
+ event,
+ environment,
+ "faction_tags_preserve_defaults_and_standard_undead",
+ RetoldGameTests::factionTagsPreserveDefaultsAndStandardUndead
+ );
+ registerTest(
+ event,
+ environment,
+ "faction_tags_drive_targeting_and_retaliation",
+ RetoldGameTests::factionTagsDriveTargetingAndRetaliation
+ );
registerTest(
event,
environment,
@@ -228,6 +247,24 @@ public static void register(RegisterGameTestsEvent event) {
"creeper_explosion_respects_mob_griefing",
RetoldGameTests::creeperExplosionRespectsMobGriefing
);
+ registerTest(
+ event,
+ environment,
+ "compatibility_block_tags_preserve_existing_defaults",
+ RetoldGameTests::compatibilityBlockTagsPreserveExistingDefaults
+ );
+ registerTest(
+ event,
+ environment,
+ "compatibility_item_tags_preserve_existing_defaults",
+ RetoldGameTests::compatibilityItemTagsPreserveExistingDefaults
+ );
+ registerTest(
+ event,
+ environment,
+ "compatibility_food_and_forage_preserve_existing_behavior",
+ RetoldGameTests::compatibilityFoodAndForagePreserveExistingBehavior
+ );
registerTest(
event,
environment,
@@ -236,6 +273,7 @@ public static void register(RegisterGameTestsEvent event) {
);
RetoldAenderGameTests.register(event, environment);
+ RetoldWorldProtectionGameTests.register(event, environment);
RetoldMobAvailabilityGameTests.register(event, environment);
RetoldAiPerformanceGameTests.register(event);
RetoldPerMobTpsGameTests.register(event);
@@ -264,6 +302,7 @@ public static void register(RegisterGameTestsEvent event) {
RetoldEnchantingGameTests.register(event, environment);
RetoldToolProgressionGameTests.register(event, environment);
RetoldProgressionAcquisitionGameTests.register(event, environment);
+ RetoldRecipeCompatibilityGameTests.register(event, environment);
RetoldTerritoryGameTests.register(event, environment);
RetoldVillagerCommunalFoodGameTests.register(event);
RetoldVillagerGolemConstructionGameTests.register(event);
@@ -722,6 +761,173 @@ private static void indiscriminateFactionsFollowLivingTargetRules(
helper.succeed();
}
+ private static void factionTagsPreserveDefaultsAndStandardUndead(
+ GameTestHelper helper
+ ) {
+ Map, RetoldFaction> expectedFactions = Map.ofEntries(
+ Map.entry(EntityTypes.PIGLIN, RetoldFaction.NETHER_REMNANTS),
+ Map.entry(EntityTypes.PIGLIN_BRUTE, RetoldFaction.NETHER_REMNANTS),
+ Map.entry(EntityTypes.BLAZE, RetoldFaction.NETHER_REMNANTS),
+ Map.entry(EntityTypes.PILLAGER, RetoldFaction.ILLAGERS),
+ Map.entry(EntityTypes.VINDICATOR, RetoldFaction.ILLAGERS),
+ Map.entry(EntityTypes.EVOKER, RetoldFaction.ILLAGERS),
+ Map.entry(EntityTypes.ILLUSIONER, RetoldFaction.ILLAGERS),
+ Map.entry(EntityTypes.RAVAGER, RetoldFaction.ILLAGERS),
+ Map.entry(EntityTypes.VEX, RetoldFaction.ILLAGERS),
+ Map.entry(EntityTypes.ZOMBIE, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.ZOMBIE_VILLAGER, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.HUSK, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.DROWNED, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.ZOMBIE_HORSE, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.CAMEL_HUSK, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.ZOMBIFIED_PIGLIN, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.ZOGLIN, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.ZOMBIE_NAUTILUS, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.SKELETON, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.STRAY, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.WITHER_SKELETON, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.SKELETON_HORSE, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.BOGGED, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.PARCHED, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.WITHER, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.PHANTOM, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.GHAST, RetoldFaction.UNDEAD),
+ Map.entry(EntityTypes.SLIME, RetoldFaction.SLIMES),
+ Map.entry(EntityTypes.MAGMA_CUBE, RetoldFaction.SLIMES),
+ Map.entry(EntityTypes.GUARDIAN, RetoldFaction.AQUATIC_HOSTILES),
+ Map.entry(EntityTypes.ELDER_GUARDIAN, RetoldFaction.AQUATIC_HOSTILES),
+ Map.entry(EntityTypes.CREEPER, RetoldFaction.CREEPERS),
+ Map.entry(EntityTypes.SPIDER, RetoldFaction.ARTHROPODS),
+ Map.entry(EntityTypes.CAVE_SPIDER, RetoldFaction.ARTHROPODS),
+ Map.entry(EntityTypes.SILVERFISH, RetoldFaction.SILVERFISH),
+ Map.entry(EntityTypes.ENDERMITE, RetoldFaction.ENDERMITES),
+ Map.entry(EntityTypes.HOGLIN, RetoldFaction.NETHER_BEASTS),
+ Map.entry(EntityTypes.BREEZE, RetoldFaction.BREEZES),
+ Map.entry(EntityTypes.WARDEN, RetoldFaction.WARDENS),
+ Map.entry(EntityTypes.ENDER_DRAGON, RetoldFaction.BOSSES),
+ Map.entry(EntityTypes.CREAKING, RetoldFaction.CREAKINGS),
+ Map.entry(EntityTypes.IRON_GOLEM, RetoldFaction.VILLAGE_DEFENDERS),
+ Map.entry(EntityTypes.SNOW_GOLEM, RetoldFaction.VILLAGE_DEFENDERS),
+ Map.entry(EntityTypes.ENDERMAN, RetoldFaction.ENDERS),
+ Map.entry(EntityTypes.SHULKER, RetoldFaction.ENDERS)
+ );
+
+ for (EntityType> entityType : BuiltInRegistries.ENTITY_TYPE) {
+ RetoldFaction expected = expectedFactions.get(entityType);
+ RetoldFaction actual = RetoldFactionMembers.getFaction(entityType);
+
+ helper.assertTrue(
+ actual == expected,
+ BuiltInRegistries.ENTITY_TYPE.getKey(entityType)
+ + " must retain faction " + expected + ", got " + actual
+ );
+ helper.assertFalse(
+ RetoldFactionMembers.hasConflictingFactionTags(entityType),
+ BuiltInRegistries.ENTITY_TYPE.getKey(entityType)
+ + " must not have conflicting default faction tags"
+ );
+
+ if (expected != null) {
+ helper.assertTrue(
+ entityType.builtInRegistryHolder().is(
+ RetoldFactionMembers.getFactionTag(expected)
+ ),
+ BuiltInRegistries.ENTITY_TYPE.getKey(entityType)
+ + " must be supplied by its Retold faction tag"
+ );
+ }
+ }
+
+ var witch = helper.spawn(EntityTypes.WITCH, 1, 2, 1);
+ helper.assertTrue(
+ RetoldFactionMembers.getFaction(witch) == null
+ && RetoldFactionMembers.isLooseAllyOf(
+ witch,
+ RetoldFaction.ILLAGERS
+ ),
+ "Witches must remain loose Illager allies rather than full members"
+ );
+ helper.succeed();
+ }
+
+ private static void factionTagsDriveTargetingAndRetaliation(
+ GameTestHelper helper
+ ) {
+ var undeadMount = helper.spawn(EntityTypes.SKELETON_HORSE, 1, 2, 1);
+ var cow = helper.spawn(EntityTypes.COW, 3, 2, 1);
+ var defender = helper.spawn(EntityTypes.IRON_GOLEM, 5, 2, 1);
+
+ undeadMount.setTamed(false);
+ int untamedTargetGoalCount = undeadMount.targetSelector
+ .getAvailableGoals()
+ .size();
+ helper.assertValueEqual(
+ RetoldFactionMembers.getFaction(undeadMount),
+ RetoldFaction.UNDEAD,
+ "An untamed standard-tagged undead mount must join the Undead faction"
+ );
+ helper.assertTrue(
+ RetoldFactionRelations.shouldAttack(undeadMount, cow),
+ "A standard-tagged Undead member must inherit Undead targeting"
+ );
+ helper.assertTrue(
+ RetoldCombatTargets.applyAttackTarget(
+ undeadMount,
+ cow,
+ RetoldTargetSource.FACTION_COMBAT
+ ),
+ "Faction targeting must accept a standard-tagged Undead attacker"
+ );
+ helper.assertTrue(
+ RetoldFactionTargetMemory.isOwnedByAny(
+ undeadMount,
+ cow,
+ RetoldTargetSource.FACTION_COMBAT
+ ),
+ "Faction targeting must retain FACTION_COMBAT ownership"
+ );
+ helper.assertTrue(
+ RetoldCombatTargets.applyAttackTarget(
+ defender,
+ undeadMount,
+ RetoldTargetSource.RETALIATION
+ ),
+ "A tagged Village Defender must accept an immediate retaliation target"
+ );
+ helper.assertTrue(
+ RetoldFactionTargetMemory.isOwnedByAny(
+ defender,
+ undeadMount,
+ RetoldTargetSource.RETALIATION
+ ),
+ "Defender retaliation must retain RETALIATION ownership"
+ );
+
+ RetoldCombatTargets.clearTargetReferencesAndAggression(
+ undeadMount,
+ cow,
+ true
+ );
+ undeadMount.setTamed(true);
+ helper.runAfterDelay(2, () -> {
+ helper.assertTrue(
+ RetoldFactionMembers.getFaction(undeadMount) == null,
+ "A tamed undead mount must not retain indiscriminate Undead hostility"
+ );
+ helper.assertFalse(
+ RetoldFactionRelations.shouldAttack(undeadMount, cow),
+ "A tamed undead mount must not inherit generic Undead targeting"
+ );
+ helper.assertValueEqual(
+ undeadMount.targetSelector.getAvailableGoals().size(),
+ untamedTargetGoalCount - 2,
+ "Loaded mobs must remove their faction target and retaliation goals "
+ + "when effective membership changes"
+ );
+ helper.succeed();
+ });
+ }
+
private static void ignitedCreeperCausesDelayedFlightExceptZombies(
GameTestHelper helper
) {
@@ -1405,6 +1611,489 @@ private static void extinguishedTorchesDropMatchingLitItems(
helper.succeed();
}
+ private static void compatibilityBlockTagsPreserveExistingDefaults(
+ GameTestHelper helper
+ ) {
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.ARMADILLO_GRUB_SOILS,
+ Blocks.GRASS_BLOCK,
+ Blocks.DIRT,
+ Blocks.COARSE_DIRT,
+ Blocks.ROOTED_DIRT,
+ Blocks.PODZOL,
+ Blocks.RED_SAND,
+ Blocks.TERRACOTTA,
+ Blocks.MUD,
+ Blocks.MUDDY_MANGROVE_ROOTS
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.ARMADILLO_SCRUB_RANGE_BLOCKS,
+ Blocks.GRASS_BLOCK,
+ Blocks.DIRT,
+ Blocks.COARSE_DIRT,
+ Blocks.ROOTED_DIRT,
+ Blocks.PODZOL,
+ Blocks.SAND,
+ Blocks.RED_SAND,
+ Blocks.TERRACOTTA
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.PANDA_BAMBOO_BLOCKS,
+ Blocks.BAMBOO,
+ Blocks.BAMBOO_SAPLING
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.TURTLE_BEACH_BLOCKS,
+ Blocks.SAND,
+ Blocks.RED_SAND
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.DESERT_BROWSE_BLOCKS,
+ Blocks.DEAD_BUSH
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.GOAT_SCRAPE_BLOCKS,
+ Blocks.STONE,
+ Blocks.SNOW_BLOCK,
+ Blocks.PACKED_ICE,
+ Blocks.GRAVEL
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.MOOSHROOM_GRAZING_BLOCKS,
+ Blocks.MYCELIUM
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.FORAGE_CROPS,
+ Blocks.WHEAT,
+ Blocks.CARROTS,
+ Blocks.POTATOES,
+ Blocks.BEETROOTS,
+ Blocks.MELON_STEM,
+ Blocks.PUMPKIN_STEM,
+ Blocks.ATTACHED_MELON_STEM,
+ Blocks.ATTACHED_PUMPKIN_STEM
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.FORAGE_FLOWERS,
+ Blocks.POPPY,
+ Blocks.DANDELION,
+ Blocks.BLUE_ORCHID,
+ Blocks.ALLIUM,
+ Blocks.AZURE_BLUET,
+ Blocks.RED_TULIP,
+ Blocks.ORANGE_TULIP,
+ Blocks.WHITE_TULIP,
+ Blocks.PINK_TULIP,
+ Blocks.OXEYE_DAISY,
+ Blocks.CORNFLOWER,
+ Blocks.LILY_OF_THE_VALLEY,
+ Blocks.SUNFLOWER,
+ Blocks.LILAC,
+ Blocks.ROSE_BUSH,
+ Blocks.PEONY
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.GRAZER_FORAGE_PLANTS,
+ Blocks.GRASS_BLOCK,
+ Blocks.SHORT_GRASS,
+ Blocks.TALL_GRASS,
+ Blocks.FERN,
+ Blocks.LARGE_FERN
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.SMALL_PASSIVE_FORAGE_PLANTS,
+ Blocks.SHORT_GRASS,
+ Blocks.TALL_GRASS
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.TURTLE_FORAGE_BLOCKS,
+ Blocks.SEAGRASS
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.HOGLIN_FORAGE_BLOCKS,
+ Blocks.CRIMSON_FUNGUS
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.PIGLIN_FORAGE_BLOCKS,
+ Blocks.CRIMSON_FUNGUS,
+ Blocks.RED_MUSHROOM,
+ Blocks.BROWN_MUSHROOM
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.STRIDER_FORAGE_BLOCKS,
+ Blocks.WARPED_FUNGUS
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.SPIDER_LAIR_WEB_BLOCKS,
+ Blocks.COBWEB
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.ILLAGER_VILLAGE_SIGNAL_BLOCKS,
+ Blocks.BELL
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.NETHER_REMNANT_GUARD_ANCHOR_BLOCKS,
+ Blocks.NETHER_BRICKS,
+ Blocks.NETHER_BRICK_FENCE,
+ Blocks.NETHER_BRICK_STAIRS,
+ Blocks.NETHER_BRICK_SLAB,
+ Blocks.CRACKED_NETHER_BRICKS,
+ Blocks.CHISELED_NETHER_BRICKS,
+ Blocks.RED_NETHER_BRICKS,
+ Blocks.RED_NETHER_BRICK_STAIRS,
+ Blocks.RED_NETHER_BRICK_SLAB,
+ Blocks.RED_NETHER_BRICK_WALL,
+ Blocks.NETHER_BRICK_WALL
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.OCEAN_MONUMENT_GUARD_ANCHOR_BLOCKS,
+ Blocks.PRISMARINE,
+ Blocks.PRISMARINE_BRICKS,
+ Blocks.DARK_PRISMARINE,
+ Blocks.SEA_LANTERN,
+ Blocks.WET_SPONGE,
+ Blocks.PRISMARINE_STAIRS,
+ Blocks.PRISMARINE_SLAB,
+ Blocks.PRISMARINE_WALL,
+ Blocks.PRISMARINE_BRICK_STAIRS,
+ Blocks.PRISMARINE_BRICK_SLAB,
+ Blocks.DARK_PRISMARINE_STAIRS,
+ Blocks.DARK_PRISMARINE_SLAB
+ );
+ assertTaggedDefaults(
+ helper,
+ RetoldTags.OCEAN_MONUMENT_PROTECTED_BLOCKS,
+ Blocks.PRISMARINE,
+ Blocks.PRISMARINE_BRICKS,
+ Blocks.DARK_PRISMARINE,
+ Blocks.SEA_LANTERN,
+ Blocks.WET_SPONGE
+ );
+ helper.succeed();
+ }
+
+ private static void compatibilityItemTagsPreserveExistingDefaults(
+ GameTestHelper helper
+ ) {
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.CAMPFIRE_CONSUMABLE_IGNITERS,
+ Items.FLINT
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.LEAF_PRESERVING_TOOLS,
+ Items.SHEARS
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.MEAT_FOODS,
+ Items.BEEF,
+ Items.COOKED_BEEF,
+ Items.PORKCHOP,
+ Items.COOKED_PORKCHOP,
+ Items.MUTTON,
+ Items.COOKED_MUTTON,
+ Items.CHICKEN,
+ Items.COOKED_CHICKEN,
+ Items.RABBIT,
+ Items.COOKED_RABBIT,
+ Items.ROTTEN_FLESH
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.FISH_FOODS,
+ Items.COD,
+ Items.COOKED_COD,
+ Items.SALMON,
+ Items.COOKED_SALMON,
+ Items.TROPICAL_FISH,
+ Items.PUFFERFISH
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.BERRY_FOODS,
+ Items.SWEET_BERRIES,
+ Items.GLOW_BERRIES
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.GRAZER_FOODS,
+ Items.WHEAT,
+ Items.HAY_BLOCK,
+ Items.APPLE,
+ Items.CARROT,
+ Items.POTATO,
+ Items.BEETROOT,
+ Items.SHORT_GRASS,
+ Items.TALL_GRASS,
+ Items.FERN,
+ Items.LARGE_FERN
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.SMALL_PASSIVE_FOODS,
+ Items.WHEAT_SEEDS,
+ Items.BEETROOT_SEEDS,
+ Items.MELON_SEEDS,
+ Items.PUMPKIN_SEEDS,
+ Items.CARROT,
+ Items.POTATO,
+ Items.BEETROOT,
+ Items.DANDELION
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.FLOWER_FOODS,
+ Items.POPPY,
+ Items.DANDELION,
+ Items.BLUE_ORCHID,
+ Items.ALLIUM,
+ Items.AZURE_BLUET,
+ Items.RED_TULIP,
+ Items.ORANGE_TULIP,
+ Items.WHITE_TULIP,
+ Items.PINK_TULIP,
+ Items.OXEYE_DAISY,
+ Items.CORNFLOWER,
+ Items.LILY_OF_THE_VALLEY,
+ Items.SUNFLOWER,
+ Items.LILAC,
+ Items.ROSE_BUSH,
+ Items.PEONY
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.NETHER_FUNGUS_FOODS,
+ Items.CRIMSON_FUNGUS,
+ Items.WARPED_FUNGUS,
+ Items.RED_MUSHROOM,
+ Items.BROWN_MUSHROOM
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.BAT_FOODS,
+ Items.SPIDER_EYE
+ );
+ assertTaggedItemDefaults(
+ helper,
+ RetoldTags.FELINE_SCAVENGE_FOODS,
+ Items.PHANTOM_MEMBRANE
+ );
+ helper.succeed();
+ }
+
+ private static void compatibilityFoodAndForagePreserveExistingBehavior(
+ GameTestHelper helper
+ ) {
+ var wolf = helper.spawn(EntityTypes.WOLF, 1, 2, 1);
+ var fox = helper.spawn(EntityTypes.FOX, 1, 2, 1);
+ var cat = helper.spawn(EntityTypes.CAT, 1, 2, 1);
+ var cow = helper.spawn(EntityTypes.COW, 1, 2, 1);
+ var chicken = helper.spawn(EntityTypes.CHICKEN, 1, 2, 1);
+ var bee = helper.spawn(EntityTypes.BEE, 1, 2, 1);
+ var turtle = helper.spawn(EntityTypes.TURTLE, 1, 2, 1);
+ var armadillo = helper.spawn(EntityTypes.ARMADILLO, 1, 2, 1);
+ var panda = helper.spawn(EntityTypes.PANDA, 1, 2, 1);
+ var bat = helper.spawn(EntityTypes.BAT, 1, 2, 1);
+ var hoglin = helper.spawn(EntityTypes.HOGLIN, 1, 2, 1);
+ var piglin = helper.spawn(EntityTypes.PIGLIN, 1, 2, 1);
+ var strider = helper.spawn(EntityTypes.STRIDER, 1, 2, 1);
+ var guardian = helper.spawn(EntityTypes.GUARDIAN, 1, 2, 1);
+
+ helper.assertTrue(
+ RetoldMobRules.canEatDroppedItem(
+ wolf,
+ new ItemStack(Items.BEEF)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ wolf,
+ new ItemStack(Items.COD)
+ )
+ && !RetoldMobRules.canEatDroppedItem(
+ wolf,
+ new ItemStack(Items.WHEAT)
+ ),
+ "Predator meat/fish eligibility must remain unchanged"
+ );
+ helper.assertTrue(
+ RetoldMobRules.canEatDroppedItem(
+ fox,
+ new ItemStack(Items.SWEET_BERRIES)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ cat,
+ new ItemStack(Items.PHANTOM_MEMBRANE)
+ ),
+ "Fox berry and feline scavenging foods must remain eligible"
+ );
+ helper.assertTrue(
+ RetoldMobRules.canEatDroppedItem(
+ cow,
+ new ItemStack(Items.WHEAT)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ chicken,
+ new ItemStack(Items.WHEAT_SEEDS)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ bee,
+ new ItemStack(Items.POPPY)
+ ),
+ "Herbivore and Bee dropped-food eligibility must remain unchanged"
+ );
+ helper.assertTrue(
+ RetoldMobRules.canEatDroppedItem(
+ turtle,
+ new ItemStack(Items.SEAGRASS)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ armadillo,
+ new ItemStack(Items.SPIDER_EYE)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ panda,
+ new ItemStack(Items.BAMBOO)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ bat,
+ new ItemStack(Items.SPIDER_EYE)
+ ),
+ "Species-specific dropped foods must remain eligible"
+ );
+ helper.assertTrue(
+ RetoldMobRules.canEatDroppedItem(
+ hoglin,
+ new ItemStack(Items.CRIMSON_FUNGUS)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ piglin,
+ new ItemStack(Items.BROWN_MUSHROOM)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ strider,
+ new ItemStack(Items.WARPED_FUNGUS)
+ )
+ && RetoldMobRules.canEatDroppedItem(
+ guardian,
+ new ItemStack(Items.SALMON)
+ ),
+ "Nether and aquatic dropped foods must remain eligible"
+ );
+ helper.assertTrue(
+ RetoldMobRules.canForageBlock(
+ cow,
+ Blocks.GRASS_BLOCK.defaultBlockState()
+ )
+ && RetoldMobRules.canForageBlock(
+ cow,
+ Blocks.WHEAT.defaultBlockState()
+ )
+ && RetoldMobRules.canForageBlock(
+ cow,
+ Blocks.POPPY.defaultBlockState()
+ )
+ && RetoldMobRules.canForageBlock(
+ chicken,
+ Blocks.SHORT_GRASS.defaultBlockState()
+ ),
+ "Ordinary grazer and small-passive forage must remain eligible"
+ );
+ helper.assertTrue(
+ RetoldMobRules.canForageBlock(
+ turtle,
+ Blocks.SEAGRASS.defaultBlockState()
+ )
+ && RetoldMobRules.canForageBlock(
+ hoglin,
+ Blocks.CRIMSON_FUNGUS.defaultBlockState()
+ )
+ && RetoldMobRules.canForageBlock(
+ piglin,
+ Blocks.BROWN_MUSHROOM.defaultBlockState()
+ )
+ && RetoldMobRules.canForageBlock(
+ strider,
+ Blocks.WARPED_FUNGUS.defaultBlockState()
+ ),
+ "Turtle and Nether forage must remain eligible"
+ );
+ helper.assertTrue(
+ RetoldMobRules.isFlowerBlock(
+ Blocks.POPPY.defaultBlockState()
+ )
+ && !RetoldMobRules.isFlowerBlock(
+ Blocks.STONE.defaultBlockState()
+ ),
+ "Bee flower classification must remain unchanged"
+ );
+ helper.assertValueEqual(
+ RetoldMobRules.foodRelief(wolf, new ItemStack(Items.BEEF)),
+ 28,
+ "Predator meat relief must remain unchanged"
+ );
+ helper.assertValueEqual(
+ RetoldMobRules.foodRelief(cow, new ItemStack(Items.WHEAT)),
+ 28,
+ "High-value grazer food relief must remain unchanged"
+ );
+ helper.assertValueEqual(
+ RetoldMobRules.forageRelief(
+ cow,
+ Blocks.WHEAT.defaultBlockState()
+ ),
+ 24,
+ "Grazer crop relief must remain unchanged"
+ );
+
+ helper.succeed();
+ }
+
+ private static void assertTaggedDefaults(
+ GameTestHelper helper,
+ TagKey tag,
+ Block... blocks
+ ) {
+ for (Block block : blocks) {
+ helper.assertTrue(
+ block.defaultBlockState().is(tag),
+ block + " must remain in " + tag.location()
+ );
+ }
+ }
+
+ private static void assertTaggedItemDefaults(
+ GameTestHelper helper,
+ TagKey- tag,
+ Item... items
+ ) {
+ for (Item item : items) {
+ helper.assertTrue(
+ item.getDefaultInstance().is(tag),
+ item + " must remain in " + tag.location()
+ );
+ }
+ }
+
private static void assertOnlyDrop(
GameTestHelper helper,
ServerLevel level,
diff --git a/src/main/java/cz/xefensor/retold/mixin/ServerRecipeBookMixin.java b/src/main/java/cz/xefensor/retold/mixin/ServerRecipeBookMixin.java
index c3789b5..b05329b 100644
--- a/src/main/java/cz/xefensor/retold/mixin/ServerRecipeBookMixin.java
+++ b/src/main/java/cz/xefensor/retold/mixin/ServerRecipeBookMixin.java
@@ -1,10 +1,9 @@
package cz.xefensor.retold.mixin;
-import cz.xefensor.retold.recipe.RetoldKnownRecipeData;
import cz.xefensor.retold.recipe.RetoldRecipeUnlockContext;
+import cz.xefensor.retold.api.recipe.RetoldRecipeKnowledge;
import net.minecraft.network.protocol.game.ClientboundRecipeBookAddPacket;
import net.minecraft.resources.ResourceKey;
-import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.stats.ServerRecipeBook;
import net.minecraft.world.item.crafting.Recipe;
@@ -45,12 +44,6 @@ public abstract class ServerRecipeBookMixin {
ServerPlayer player,
CallbackInfoReturnable cir
) {
- if (!(player.level() instanceof ServerLevel serverLevel)) {
- cir.setReturnValue(0);
- return;
- }
-
- RetoldKnownRecipeData data = RetoldKnownRecipeData.get(serverLevel);
boolean internalUnlock = RetoldRecipeUnlockContext.isInternalUnlock();
List entries = new ArrayList<>();
@@ -59,7 +52,8 @@ public abstract class ServerRecipeBookMixin {
for (RecipeHolder> recipe : recipes) {
ResourceKey> recipeId = recipe.id();
- if (!internalUnlock && !data.hasKnown(player, recipeId)) {
+ if (!internalUnlock
+ && !RetoldRecipeKnowledge.isVisibleTo(player, recipe)) {
continue;
}
@@ -87,4 +81,4 @@ public abstract class ServerRecipeBookMixin {
cir.setReturnValue(added);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/cz/xefensor/retold/module/RetoldFactionModule.java b/src/main/java/cz/xefensor/retold/module/RetoldFactionModule.java
index 9bfc016..49b3142 100644
--- a/src/main/java/cz/xefensor/retold/module/RetoldFactionModule.java
+++ b/src/main/java/cz/xefensor/retold/module/RetoldFactionModule.java
@@ -3,6 +3,7 @@
import cz.xefensor.retold.event.RetoldFactionAssistEvents;
import cz.xefensor.retold.event.RetoldFactionCombatEvents;
import cz.xefensor.retold.event.RetoldInvalidTargetEvents;
+import cz.xefensor.retold.faction.RetoldFactionMembers;
import net.neoforged.bus.api.IEventBus;
public final class RetoldFactionModule {
@@ -13,5 +14,6 @@ public static void registerGameBus(IEventBus gameEventBus) {
gameEventBus.register(RetoldInvalidTargetEvents.class);
gameEventBus.register(RetoldFactionCombatEvents.class);
gameEventBus.register(RetoldFactionAssistEvents.class);
+ gameEventBus.register(RetoldFactionMembers.class);
}
}
diff --git a/src/main/java/cz/xefensor/retold/network/RetoldNetworking.java b/src/main/java/cz/xefensor/retold/network/RetoldNetworking.java
index 1a21ce2..3f8c98d 100644
--- a/src/main/java/cz/xefensor/retold/network/RetoldNetworking.java
+++ b/src/main/java/cz/xefensor/retold/network/RetoldNetworking.java
@@ -4,6 +4,7 @@
import cz.xefensor.retold.client.enchanting.RetoldClientEnchantmentCatalog;
import cz.xefensor.retold.client.enchanting.RetoldClientEnchantmentKnowledge;
import cz.xefensor.retold.client.enchanting.RetoldEnchantingScreenFeedback;
+import cz.xefensor.retold.client.recipe.RetoldClientRecipeKnowledge;
import cz.xefensor.retold.client.render.RetoldChronolithBeamClient;
import cz.xefensor.retold.client.stage.RetoldClientStage;
import cz.xefensor.retold.enchanting.RetoldEnchantingMenuActions;
@@ -111,6 +112,14 @@ public static void registerPayloads(RegisterPayloadHandlersEvent event) {
)
);
+ registrar.playToClient(
+ RetoldRecipeKnowledgeSyncPayload.TYPE,
+ RetoldRecipeKnowledgeSyncPayload.STREAM_CODEC,
+ (payload, context) -> RetoldClientRecipeKnowledge.replace(
+ payload.knownRecipes()
+ )
+ );
+
registrar.playToClient(
RetoldChronolithBeamPayload.TYPE,
RetoldChronolithBeamPayload.STREAM_CODEC,
diff --git a/src/main/java/cz/xefensor/retold/network/RetoldRecipeKnowledgeSyncPayload.java b/src/main/java/cz/xefensor/retold/network/RetoldRecipeKnowledgeSyncPayload.java
new file mode 100644
index 0000000..edc745e
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/network/RetoldRecipeKnowledgeSyncPayload.java
@@ -0,0 +1,65 @@
+package cz.xefensor.retold.network;
+
+import cz.xefensor.retold.Retold;
+import cz.xefensor.retold.recipe.RetoldKnownRecipeData;
+import io.netty.buffer.ByteBuf;
+import net.minecraft.network.codec.ByteBufCodecs;
+import net.minecraft.network.codec.StreamCodec;
+import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
+import net.minecraft.resources.Identifier;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.world.item.crafting.Recipe;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public record RetoldRecipeKnowledgeSyncPayload(
+ List>> knownRecipes
+) implements CustomPacketPayload {
+ private static final int MAX_KNOWN_RECIPES = 65_536;
+
+ public static final Type TYPE =
+ new Type<>(Identifier.fromNamespaceAndPath(
+ Retold.MODID,
+ "recipe_knowledge_sync"
+ ));
+
+ private static final StreamCodec>> RECIPE_KEY_CODEC =
+ Identifier.STREAM_CODEC.map(
+ RetoldKnownRecipeData::recipeKeyFromIdentifier,
+ ResourceKey::identifier
+ );
+
+ private static final StreamCodec>>>
+ RECIPE_LIST_CODEC = ByteBufCodecs.collection(
+ ArrayList::new,
+ RECIPE_KEY_CODEC,
+ MAX_KNOWN_RECIPES
+ );
+
+ public static final StreamCodec STREAM_CODEC =
+ StreamCodec.composite(
+ RECIPE_LIST_CODEC,
+ RetoldRecipeKnowledgeSyncPayload::knownRecipes,
+ RetoldRecipeKnowledgeSyncPayload::new
+ );
+
+ public RetoldRecipeKnowledgeSyncPayload(
+ Collection>> knownRecipes
+ ) {
+ this(knownRecipes.stream()
+ .sorted((first, second) -> first.identifier()
+ .compareTo(second.identifier()))
+ .toList());
+ }
+
+ public RetoldRecipeKnowledgeSyncPayload {
+ knownRecipes = List.copyOf(knownRecipes);
+ }
+
+ @Override
+ public Type extends CustomPacketPayload> type() {
+ return TYPE;
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/progression/RetoldCampfireProgressionEvents.java b/src/main/java/cz/xefensor/retold/progression/RetoldCampfireProgressionEvents.java
index b53cc5a..1aeb943 100644
--- a/src/main/java/cz/xefensor/retold/progression/RetoldCampfireProgressionEvents.java
+++ b/src/main/java/cz/xefensor/retold/progression/RetoldCampfireProgressionEvents.java
@@ -1,5 +1,6 @@
package cz.xefensor.retold.progression;
+import cz.xefensor.retold.registry.RetoldTags;
import net.minecraft.core.BlockPos;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
@@ -8,7 +9,6 @@
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
-import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.CampfireBlock;
@@ -55,7 +55,8 @@ static boolean lightWithFlint(
ItemStack stack
) {
BlockState state = level.getBlockState(pos);
- if (!stack.is(Items.FLINT) || !CampfireBlock.canLight(state)) {
+ if (!stack.is(RetoldTags.CAMPFIRE_CONSUMABLE_IGNITERS)
+ || !CampfireBlock.canLight(state)) {
return false;
}
@@ -77,7 +78,7 @@ static boolean lightWithFlint(
level.getRandom().nextFloat() * 0.4F + 0.8F
);
level.gameEvent(player, GameEvent.BLOCK_CHANGE, pos);
- player.awardStat(Stats.ITEM_USED.get(Items.FLINT));
+ player.awardStat(Stats.ITEM_USED.get(stack.getItem()));
if (!player.getAbilities().instabuild) {
stack.shrink(1);
diff --git a/src/main/java/cz/xefensor/retold/progression/RetoldLeafStickLootModifier.java b/src/main/java/cz/xefensor/retold/progression/RetoldLeafStickLootModifier.java
index 5520715..cdd91bc 100644
--- a/src/main/java/cz/xefensor/retold/progression/RetoldLeafStickLootModifier.java
+++ b/src/main/java/cz/xefensor/retold/progression/RetoldLeafStickLootModifier.java
@@ -59,7 +59,8 @@ protected ObjectArrayList doApply(
int silkTouchLevel = tool.getEnchantmentLevel(
enchantments.getOrThrow(Enchantments.SILK_TOUCH)
);
- if (tool.is(Items.SHEARS) || silkTouchLevel > 0) {
+ if (tool.is(RetoldTags.LEAF_PRESERVING_TOOLS)
+ || silkTouchLevel > 0) {
return generatedLoot;
}
diff --git a/src/main/java/cz/xefensor/retold/recipe/RetoldKnownRecipeData.java b/src/main/java/cz/xefensor/retold/recipe/RetoldKnownRecipeData.java
index ca87ec5..85035ec 100644
--- a/src/main/java/cz/xefensor/retold/recipe/RetoldKnownRecipeData.java
+++ b/src/main/java/cz/xefensor/retold/recipe/RetoldKnownRecipeData.java
@@ -37,9 +37,13 @@ private RetoldKnownRecipeData(Map> savedData) {
}
public static ResourceKey> recipeKeyFromString(String id) {
+ return recipeKeyFromIdentifier(Identifier.parse(id));
+ }
+
+ public static ResourceKey> recipeKeyFromIdentifier(Identifier id) {
return ResourceKey.create(
Registries.RECIPE,
- Identifier.parse(id)
+ id
);
}
@@ -84,4 +88,20 @@ public boolean markKnown(ServerPlayer player, ResourceKey> recipeId) {
return added;
}
-}
\ No newline at end of file
+
+ public Set>> knownRecipes(ServerPlayer player) {
+ Set knownRecipes = knownRecipesByPlayer.get(player.getUUID());
+
+ if (knownRecipes == null || knownRecipes.isEmpty()) {
+ return Set.of();
+ }
+
+ Set>> result = new HashSet<>();
+
+ for (String recipeId : knownRecipes) {
+ result.add(recipeKeyFromString(recipeId));
+ }
+
+ return Set.copyOf(result);
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeBookEvents.java b/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeBookEvents.java
index 5adcc85..64d44a5 100644
--- a/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeBookEvents.java
+++ b/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeBookEvents.java
@@ -1,5 +1,6 @@
package cz.xefensor.retold.recipe;
+import cz.xefensor.retold.network.RetoldRecipeKnowledgeSyncPayload;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
@@ -12,6 +13,7 @@
import net.minecraft.world.item.crafting.RecipeType;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
+import net.neoforged.neoforge.network.PacketDistributor;
import java.util.ArrayList;
import java.util.List;
@@ -132,7 +134,9 @@ public static void markKnownRecipe(
}
RetoldKnownRecipeData data = RetoldKnownRecipeData.get(serverLevel);
- data.markKnown(player, recipe.id());
+ if (data.markKnown(player, recipe.id())) {
+ syncToPlayer(player);
+ }
}
public static void markKnownRecipeAndUnlockCookingSiblings(
@@ -189,6 +193,19 @@ public static void markKnownAndUnlockRecipe(
markAndUnlockRecipe(player, recipe);
}
+ public static void syncToPlayer(ServerPlayer player) {
+ if (player.connection == null
+ || !player.connection.getConnection().isConnected()) {
+ return;
+ }
+
+ RetoldKnownRecipeData data = RetoldKnownRecipeData.get(player.level());
+ PacketDistributor.sendToPlayer(
+ player,
+ new RetoldRecipeKnowledgeSyncPayload(data.knownRecipes(player))
+ );
+ }
+
private static void unlockRecipeSilently(
ServerPlayer player,
RecipeHolder> recipe
diff --git a/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeCompatibilityGameTests.java b/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeCompatibilityGameTests.java
new file mode 100644
index 0000000..8218a5a
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeCompatibilityGameTests.java
@@ -0,0 +1,212 @@
+package cz.xefensor.retold.recipe;
+
+import cz.xefensor.retold.Retold;
+import cz.xefensor.retold.api.recipe.RetoldRecipeKnowledge;
+import cz.xefensor.retold.client.recipe.RetoldClientRecipeKnowledge;
+import cz.xefensor.retold.network.RetoldRecipeKnowledgeSyncPayload;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import net.minecraft.core.Holder;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gametest.framework.BuiltinTestFunctions;
+import net.minecraft.gametest.framework.FunctionGameTestInstance;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.gametest.framework.TestData;
+import net.minecraft.gametest.framework.TestEnvironmentDefinition;
+import net.minecraft.resources.Identifier;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.item.crafting.Recipe;
+import net.minecraft.world.item.crafting.RecipeHolder;
+import net.minecraft.world.item.crafting.RecipeType;
+import net.minecraft.world.level.GameType;
+import net.neoforged.neoforge.event.RegisterGameTestsEvent;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+public final class RetoldRecipeCompatibilityGameTests {
+ private static final Identifier EMPTY_STRUCTURE =
+ Identifier.withDefaultNamespace("empty");
+
+ private RetoldRecipeCompatibilityGameTests() {
+ }
+
+ public static void register(
+ RegisterGameTestsEvent event,
+ Holder> environment
+ ) {
+ TestData>> testData =
+ new TestData<>(environment, EMPTY_STRUCTURE, 40, 0, true);
+
+ event.registerTest(
+ id("recipe_visibility_uses_shared_knowledge_authority"),
+ new InlineGameTest(
+ testData,
+ RetoldRecipeCompatibilityGameTests::visibilityUsesSharedAuthority
+ )
+ );
+ event.registerTest(
+ id("unknown_recipe_types_fail_open_for_viewers"),
+ new InlineGameTest(
+ testData,
+ RetoldRecipeCompatibilityGameTests::unknownTypesFailOpen
+ )
+ );
+ }
+
+ private static void visibilityUsesSharedAuthority(GameTestHelper helper) {
+ ServerPlayer learner = (ServerPlayer) helper.makeMockServerPlayer(
+ GameType.SURVIVAL
+ );
+ ServerPlayer otherPlayer = (ServerPlayer) helper.makeMockServerPlayer(
+ GameType.SURVIVAL
+ );
+ RecipeHolder> recipe = helper.getLevel()
+ .getServer()
+ .getRecipeManager()
+ .getRecipes()
+ .stream()
+ .filter(candidate -> candidate.value().getType() == RecipeType.CRAFTING)
+ .findFirst()
+ .orElseThrow();
+
+ for (RecipeType> managedType : List.of(
+ RecipeType.CRAFTING,
+ RecipeType.SMELTING,
+ RecipeType.BLASTING,
+ RecipeType.SMOKING,
+ RecipeType.CAMPFIRE_COOKING,
+ RecipeType.STONECUTTING,
+ RecipeType.SMITHING
+ )) {
+ helper.assertTrue(
+ RetoldRecipeKnowledge.isDiscoveryManaged(managedType),
+ "Every existing Retold discovery recipe type must use the shared authority"
+ );
+ }
+
+ helper.assertFalse(
+ RetoldRecipeKnowledge.isVisibleTo(learner, recipe),
+ "An undiscovered managed recipe must be hidden"
+ );
+ helper.assertFalse(
+ RetoldRecipeKnowledge.isVisibleTo(otherPlayer, recipe),
+ "Recipe visibility must remain per player"
+ );
+
+ RetoldRecipeKnowledge.teachAndUnlock(learner, recipe);
+
+ helper.assertTrue(
+ RetoldRecipeKnowledge.isVisibleTo(learner, recipe),
+ "Learning a recipe must make it visible through the shared authority"
+ );
+ helper.assertFalse(
+ RetoldRecipeKnowledge.isVisibleTo(otherPlayer, recipe),
+ "One player's discovery must not reveal a recipe to another player"
+ );
+ helper.assertTrue(
+ learner.getRecipeBook().contains(recipe.id()),
+ "The existing vanilla recipe-book unlock must remain synchronized"
+ );
+
+ RetoldRecipeKnowledgeSyncPayload original =
+ new RetoldRecipeKnowledgeSyncPayload(
+ RetoldKnownRecipeData.get(helper.getLevel())
+ .knownRecipes(learner)
+ );
+ ByteBuf buffer = Unpooled.buffer();
+
+ try {
+ RetoldRecipeKnowledgeSyncPayload.STREAM_CODEC.encode(buffer, original);
+ RetoldRecipeKnowledgeSyncPayload decoded =
+ RetoldRecipeKnowledgeSyncPayload.STREAM_CODEC.decode(buffer);
+
+ helper.assertTrue(
+ decoded.knownRecipes().contains(recipe.id()),
+ "The synchronized snapshot must retain learned recipe ids"
+ );
+ RetoldClientRecipeKnowledge.replace(decoded.knownRecipes());
+ helper.assertTrue(
+ RetoldClientRecipeKnowledge.isKnown(recipe.id()),
+ "Client recipe viewers must consume the same learned snapshot"
+ );
+ } finally {
+ RetoldClientRecipeKnowledge.clear();
+ buffer.release();
+ }
+
+ helper.succeed();
+ }
+
+ private static void unknownTypesFailOpen(GameTestHelper helper) {
+ ServerPlayer player = (ServerPlayer) helper.makeMockServerPlayer(
+ GameType.SURVIVAL
+ );
+ Identifier typeId = Identifier.fromNamespaceAndPath(
+ "examplemod",
+ "pressing"
+ );
+ RecipeType> unknownType = RecipeType.simple(typeId);
+ ResourceKey> unknownRecipe = ResourceKey.create(
+ Registries.RECIPE,
+ Identifier.fromNamespaceAndPath("examplemod", "pressed_plate")
+ );
+
+ helper.assertFalse(
+ RetoldRecipeKnowledge.isDiscoveryManaged(unknownType),
+ "Unknown third-party recipe types must not be managed implicitly"
+ );
+ helper.assertTrue(
+ RetoldRecipeKnowledge.isVisibleTo(
+ player,
+ unknownRecipe,
+ unknownType
+ ),
+ "Unknown third-party recipe types must fail open for compatibility"
+ );
+
+ try (RetoldRecipeKnowledge.DiscoveryRegistration ignored =
+ RetoldRecipeKnowledge.registerDiscoveryManagedType(unknownType)) {
+ helper.assertTrue(
+ RetoldRecipeKnowledge.isDiscoveryManaged(unknownType),
+ "An integration must be able to opt a custom recipe type into discovery"
+ );
+ helper.assertFalse(
+ RetoldRecipeKnowledge.isVisibleTo(
+ player,
+ unknownRecipe,
+ unknownType
+ ),
+ "An opted-in custom recipe must use the same per-player knowledge authority"
+ );
+ }
+
+ helper.assertTrue(
+ RetoldRecipeKnowledge.isVisibleTo(player, unknownRecipe, unknownType),
+ "Closing an optional registration must restore fail-open behavior"
+ );
+ helper.succeed();
+ }
+
+ private static Identifier id(String path) {
+ return Identifier.fromNamespaceAndPath(Retold.MODID, path);
+ }
+
+ private static final class InlineGameTest extends FunctionGameTestInstance {
+ private final Consumer test;
+
+ private InlineGameTest(
+ TestData>> testData,
+ Consumer test
+ ) {
+ super(BuiltinTestFunctions.ALWAYS_PASS, testData);
+ this.test = test;
+ }
+
+ @Override
+ public void run(GameTestHelper helper) {
+ test.accept(helper);
+ }
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeVisibility.java b/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeVisibility.java
new file mode 100644
index 0000000..84d90ac
--- /dev/null
+++ b/src/main/java/cz/xefensor/retold/recipe/RetoldRecipeVisibility.java
@@ -0,0 +1,108 @@
+package cz.xefensor.retold.recipe;
+
+import cz.xefensor.retold.api.recipe.RetoldRecipeKnowledge;
+import cz.xefensor.retold.client.recipe.RetoldClientRecipeKnowledge;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.item.crafting.Recipe;
+import net.minecraft.world.item.crafting.RecipeHolder;
+import net.minecraft.world.item.crafting.RecipeType;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** Shared recipe-visibility authority for vanilla and optional recipe viewers. */
+public final class RetoldRecipeVisibility {
+ private static final Set> DISCOVERY_MANAGED_TYPES = Set.of(
+ RecipeType.CRAFTING,
+ RecipeType.SMELTING,
+ RecipeType.BLASTING,
+ RecipeType.SMOKING,
+ RecipeType.CAMPFIRE_COOKING,
+ RecipeType.STONECUTTING,
+ RecipeType.SMITHING
+ );
+ private static final CopyOnWriteArrayList> OPTIONAL_MANAGED_TYPES =
+ new CopyOnWriteArrayList<>();
+
+ private RetoldRecipeVisibility() {
+ }
+
+ public static boolean isDiscoveryManaged(RecipeHolder> recipe) {
+ return recipe != null
+ && isDiscoveryManaged(recipe.value().getType());
+ }
+
+ public static boolean isDiscoveryManaged(RecipeType> recipeType) {
+ return recipeType != null
+ && (DISCOVERY_MANAGED_TYPES.contains(recipeType)
+ || OPTIONAL_MANAGED_TYPES.contains(recipeType));
+ }
+
+ public static RetoldRecipeKnowledge.DiscoveryRegistration registerDiscoveryManagedType(
+ RecipeType> recipeType
+ ) {
+ OPTIONAL_MANAGED_TYPES.add(recipeType);
+ return () -> OPTIONAL_MANAGED_TYPES.remove(recipeType);
+ }
+
+ public static boolean isKnown(
+ ServerPlayer player,
+ ResourceKey> recipeId
+ ) {
+ if (player == null || recipeId == null) {
+ return false;
+ }
+
+ return RetoldKnownRecipeData.get(player.level()).hasKnown(player, recipeId);
+ }
+
+ public static boolean isVisibleTo(ServerPlayer player, RecipeHolder> recipe) {
+ if (player == null || recipe == null) {
+ return false;
+ }
+
+ return isVisibleTo(player, recipe.id(), recipe.value().getType());
+ }
+
+ public static boolean isVisibleTo(
+ ServerPlayer player,
+ ResourceKey> recipeId,
+ RecipeType> recipeType
+ ) {
+ if (player == null || recipeId == null || recipeType == null) {
+ return false;
+ }
+
+ return !isDiscoveryManaged(recipeType) || isKnown(player, recipeId);
+ }
+
+ public static boolean isVisibleTo(Player player, RecipeHolder> recipe) {
+ if (player == null || recipe == null) {
+ return false;
+ }
+
+ return isVisibleTo(player, recipe.id(), recipe.value().getType());
+ }
+
+ public static boolean isVisibleTo(
+ Player player,
+ ResourceKey> recipeId,
+ RecipeType> recipeType
+ ) {
+ if (player == null || recipeId == null || recipeType == null) {
+ return false;
+ }
+
+ if (!isDiscoveryManaged(recipeType)) {
+ return true;
+ }
+
+ if (player instanceof ServerPlayer serverPlayer) {
+ return isKnown(serverPlayer, recipeId);
+ }
+
+ return RetoldClientRecipeKnowledge.isKnown(recipeId);
+ }
+}
diff --git a/src/main/java/cz/xefensor/retold/registry/RetoldTags.java b/src/main/java/cz/xefensor/retold/registry/RetoldTags.java
index a22f574..e422958 100644
--- a/src/main/java/cz/xefensor/retold/registry/RetoldTags.java
+++ b/src/main/java/cz/xefensor/retold/registry/RetoldTags.java
@@ -13,6 +13,27 @@ public final class RetoldTags {
Registries.ITEM,
Identifier.fromNamespaceAndPath(Retold.MODID, "torch_igniters")
);
+ public static final TagKey
- CAMPFIRE_CONSUMABLE_IGNITERS = itemTag(
+ "campfire_consumable_igniters"
+ );
+ public static final TagKey
- LEAF_PRESERVING_TOOLS = itemTag(
+ "leaf_preserving_tools"
+ );
+ public static final TagKey
- MEAT_FOODS = itemTag("meat_foods");
+ public static final TagKey
- FISH_FOODS = itemTag("fish_foods");
+ public static final TagKey
- BERRY_FOODS = itemTag("berry_foods");
+ public static final TagKey
- GRAZER_FOODS = itemTag("grazer_foods");
+ public static final TagKey
- SMALL_PASSIVE_FOODS = itemTag(
+ "small_passive_foods"
+ );
+ public static final TagKey
- FLOWER_FOODS = itemTag("flower_foods");
+ public static final TagKey
- NETHER_FUNGUS_FOODS = itemTag(
+ "nether_fungus_foods"
+ );
+ public static final TagKey
- BAT_FOODS = itemTag("bat_foods");
+ public static final TagKey
- FELINE_SCAVENGE_FOODS = itemTag(
+ "feline_scavenge_foods"
+ );
public static final TagKey
- ANIMAL_ARMOR = TagKey.create(
Registries.ITEM,
Identifier.fromNamespaceAndPath(Retold.MODID, "animal_armor")
@@ -93,6 +114,66 @@ public final class RetoldTags {
Registries.BLOCK,
Identifier.fromNamespaceAndPath(Retold.MODID, "weak_mob_barriers")
);
+ public static final TagKey ARMADILLO_GRUB_SOILS = blockTag(
+ "armadillo_grub_soils"
+ );
+ public static final TagKey ARMADILLO_SCRUB_RANGE_BLOCKS = blockTag(
+ "armadillo_scrub_range_blocks"
+ );
+ public static final TagKey PANDA_BAMBOO_BLOCKS = blockTag(
+ "panda_bamboo_blocks"
+ );
+ public static final TagKey TURTLE_BEACH_BLOCKS = blockTag(
+ "turtle_beach_blocks"
+ );
+ public static final TagKey DESERT_BROWSE_BLOCKS = blockTag(
+ "desert_browse_blocks"
+ );
+ public static final TagKey GOAT_SCRAPE_BLOCKS = blockTag(
+ "goat_scrape_blocks"
+ );
+ public static final TagKey MOOSHROOM_GRAZING_BLOCKS = blockTag(
+ "mooshroom_grazing_blocks"
+ );
+ public static final TagKey FORAGE_CROPS = blockTag(
+ "forage_crops"
+ );
+ public static final TagKey FORAGE_FLOWERS = blockTag(
+ "forage_flowers"
+ );
+ public static final TagKey GRAZER_FORAGE_PLANTS = blockTag(
+ "grazer_forage_plants"
+ );
+ public static final TagKey SMALL_PASSIVE_FORAGE_PLANTS = blockTag(
+ "small_passive_forage_plants"
+ );
+ public static final TagKey TURTLE_FORAGE_BLOCKS = blockTag(
+ "turtle_forage_blocks"
+ );
+ public static final TagKey HOGLIN_FORAGE_BLOCKS = blockTag(
+ "hoglin_forage_blocks"
+ );
+ public static final TagKey PIGLIN_FORAGE_BLOCKS = blockTag(
+ "piglin_forage_blocks"
+ );
+ public static final TagKey STRIDER_FORAGE_BLOCKS = blockTag(
+ "strider_forage_blocks"
+ );
+ public static final TagKey SPIDER_LAIR_WEB_BLOCKS = blockTag(
+ "spider_lair_web_blocks"
+ );
+ public static final TagKey ILLAGER_VILLAGE_SIGNAL_BLOCKS = blockTag(
+ "illager_village_signal_blocks"
+ );
+ public static final TagKey NETHER_REMNANT_GUARD_ANCHOR_BLOCKS = blockTag(
+ "nether_remnant_guard_anchor_blocks"
+ );
+ public static final TagKey OCEAN_MONUMENT_GUARD_ANCHOR_BLOCKS = blockTag(
+ "ocean_monument_guard_anchor_blocks"
+ );
+ public static final TagKey OCEAN_MONUMENT_PROTECTED_BLOCKS = blockTag(
+ "ocean_monument_protected_blocks"
+ );
public static final TagKey> AUTOMATIC_BREEDERS =
TagKey.create(
Registries.ENTITY_TYPE,
@@ -101,7 +182,62 @@ public final class RetoldTags {
"automatic_breeders"
)
);
+ public static final TagKey> FACTION_NETHER_REMNANTS =
+ entityTypeTag("factions/nether_remnants");
+ public static final TagKey> FACTION_ILLAGERS =
+ entityTypeTag("factions/illagers");
+ public static final TagKey> FACTION_UNDEAD =
+ entityTypeTag("factions/undead");
+ public static final TagKey> FACTION_SLIMES =
+ entityTypeTag("factions/slimes");
+ public static final TagKey> FACTION_AQUATIC_HOSTILES =
+ entityTypeTag("factions/aquatic_hostiles");
+ public static final TagKey> FACTION_CREEPERS =
+ entityTypeTag("factions/creepers");
+ public static final TagKey> FACTION_ARTHROPODS =
+ entityTypeTag("factions/arthropods");
+ public static final TagKey> FACTION_SILVERFISH =
+ entityTypeTag("factions/silverfish");
+ public static final TagKey> FACTION_ENDERMITES =
+ entityTypeTag("factions/endermites");
+ public static final TagKey> FACTION_NETHER_BEASTS =
+ entityTypeTag("factions/nether_beasts");
+ public static final TagKey> FACTION_BREEZES =
+ entityTypeTag("factions/breezes");
+ public static final TagKey> FACTION_WARDENS =
+ entityTypeTag("factions/wardens");
+ public static final TagKey> FACTION_BOSSES =
+ entityTypeTag("factions/bosses");
+ public static final TagKey> FACTION_CREAKINGS =
+ entityTypeTag("factions/creakings");
+ public static final TagKey> FACTION_VILLAGE_DEFENDERS =
+ entityTypeTag("factions/village_defenders");
+ public static final TagKey> FACTION_ENDERS =
+ entityTypeTag("factions/enders");
+ public static final TagKey> ILLAGER_LOOSE_ALLIES =
+ entityTypeTag("alliances/illager_loose_allies");
private RetoldTags() {
}
+
+ private static TagKey blockTag(String path) {
+ return TagKey.create(
+ Registries.BLOCK,
+ Identifier.fromNamespaceAndPath(Retold.MODID, path)
+ );
+ }
+
+ private static TagKey
- itemTag(String path) {
+ return TagKey.create(
+ Registries.ITEM,
+ Identifier.fromNamespaceAndPath(Retold.MODID, path)
+ );
+ }
+
+ private static TagKey> entityTypeTag(String path) {
+ return TagKey.create(
+ Registries.ENTITY_TYPE,
+ Identifier.fromNamespaceAndPath(Retold.MODID, path)
+ );
+ }
}
diff --git a/src/main/java/cz/xefensor/retold/villager/RetoldVillagerGolemConstructionGameTests.java b/src/main/java/cz/xefensor/retold/villager/RetoldVillagerGolemConstructionGameTests.java
index 3a94108..3c61306 100644
--- a/src/main/java/cz/xefensor/retold/villager/RetoldVillagerGolemConstructionGameTests.java
+++ b/src/main/java/cz/xefensor/retold/villager/RetoldVillagerGolemConstructionGameTests.java
@@ -1,6 +1,10 @@
package cz.xefensor.retold.villager;
import cz.xefensor.retold.Retold;
+import cz.xefensor.retold.behavior.control.RetoldAiControl;
+import cz.xefensor.retold.behavior.profiles.RetoldMobRules;
+import cz.xefensor.retold.behavior.profiles.RetoldMobState;
+import cz.xefensor.retold.behavior.profiles.RetoldMobStates;
import cz.xefensor.retold.event.RetoldGolemEvents;
import cz.xefensor.retold.golem.RetoldGolemAnimation;
import cz.xefensor.retold.stage.RetoldWorldData;
@@ -28,6 +32,7 @@
import net.minecraft.world.entity.animal.golem.SnowGolem;
import net.minecraft.world.entity.npc.villager.Villager;
import net.minecraft.world.entity.npc.villager.VillagerProfession;
+import net.minecraft.world.entity.schedule.Activity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.context.UseOnContext;
@@ -261,6 +266,7 @@ private static void stagesAndConservesVillageEmerald(
return;
}
+ builder.getBrain().setActiveActivityIfPossible(Activity.IDLE);
builder.snapTo(
state.access().getX() + 0.5D,
state.access().getY(),
@@ -294,6 +300,10 @@ private static void stagesAndConservesVillageEmerald(
);
RetoldVillagerGolemConstruction.BuildState current =
RetoldVillagerGolemConstruction.constructionState(builder);
+ RetoldMobState mobState = RetoldMobStates.getOrCreate(
+ builder,
+ level.getGameTime()
+ );
helper.assertTrue(
!golems.isEmpty(),
"The staged structure must animate into a golem; step="
@@ -302,6 +312,28 @@ private static void stagesAndConservesVillageEmerald(
+ countItem(chest, Items.EMERALD.getDefaultInstance())
+ ", top="
+ level.getBlockState(initial.top())
+ + ", profession="
+ + builder.getVillagerData().profession().unwrapKey()
+ .map(ResourceKey::identifier)
+ .orElse(null)
+ + ", alive="
+ + builder.isAlive()
+ + ", removed="
+ + builder.isRemoved()
+ + ", hunger="
+ + mobState.hunger()
+ + ", eatDrive="
+ + RetoldMobRules.hasEatDrive(builder, mobState)
+ + ", urgent="
+ + RetoldVillagerCommunalFood.hasUrgentVanillaActivity(builder)
+ + ", activity="
+ + builder.getBrain().getActiveNonCoreActivity()
+ + ", control="
+ + RetoldAiControl.getOwner(builder)
+ + "/"
+ + RetoldAiControl.getPriority(builder)
+ + "/"
+ + RetoldAiControl.getReason(builder)
);
helper.assertFalse(
golems.getFirst().isPlayerCreated(),
@@ -523,6 +555,7 @@ private static Villager spawnEligibleVillager(
) {
Villager villager = helper.spawn(EntityTypes.VILLAGER, x, y, z);
setProfession(helper, villager, VillagerProfession.CLERIC);
+ villager.setVillagerXp(1);
villager.getBrain().setMemory(MemoryModuleType.LAST_SLEPT, gameTime);
return villager;
}
diff --git a/src/main/java/cz/xefensor/retold/villager/RetoldVillagerTorchRelighting.java b/src/main/java/cz/xefensor/retold/villager/RetoldVillagerTorchRelighting.java
index b67b687..28bfa1f 100644
--- a/src/main/java/cz/xefensor/retold/villager/RetoldVillagerTorchRelighting.java
+++ b/src/main/java/cz/xefensor/retold/villager/RetoldVillagerTorchRelighting.java
@@ -211,7 +211,12 @@ private static void continueCast(
villager,
RetoldAiControlMode.SUPPORT,
RetoldAiControlOwner.VILLAGER_TORCH_RELIGHT
- ) || !isValidTarget(level, villager, cast.target())) {
+ ) || !isValidTarget(level, villager, cast.target())
+ || !RetoldMobGriefing.canPlaceBlock(
+ level,
+ villager,
+ cast.target()
+ )) {
cancel(villager);
return;
}
diff --git a/src/main/java/cz/xefensor/retold/worldgen/air/GaleCoreAttackEvents.java b/src/main/java/cz/xefensor/retold/worldgen/air/GaleCoreAttackEvents.java
index 51a907e..b371ea4 100644
--- a/src/main/java/cz/xefensor/retold/worldgen/air/GaleCoreAttackEvents.java
+++ b/src/main/java/cz/xefensor/retold/worldgen/air/GaleCoreAttackEvents.java
@@ -1,5 +1,6 @@
package cz.xefensor.retold.worldgen.air;
+import cz.xefensor.retold.api.world.RetoldWorldProtection;
import cz.xefensor.retold.behavior.core.RetoldMobGriefing;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
@@ -200,6 +201,13 @@ static void clearCracks(ServerLevel level) {
private static void crackBlock(ServerLevel level, BlockPos pos, double distanceSq, Entity breaker, int progressBonus) {
CrackKey key = new CrackKey(level.dimension(), pos);
+
+ if (!RetoldWorldProtection.canEntityBreak(level, pos, breaker)) {
+ level.destroyBlockProgress(breakerId(key), pos, -1);
+ CRACKS.remove(key);
+ return;
+ }
+
CrackState current = CRACKS.getOrDefault(key, new CrackState(0, level.getGameTime()));
int progress = current.progress() + progressGain(level, pos, distanceSq) + progressBonus;
int durability = blockDurability(level, pos);
diff --git a/src/main/java/cz/xefensor/retold/worldgen/delayed/RetoldDelayedStructureRetrogen.java b/src/main/java/cz/xefensor/retold/worldgen/delayed/RetoldDelayedStructureRetrogen.java
index 7e07060..fb868cc 100644
--- a/src/main/java/cz/xefensor/retold/worldgen/delayed/RetoldDelayedStructureRetrogen.java
+++ b/src/main/java/cz/xefensor/retold/worldgen/delayed/RetoldDelayedStructureRetrogen.java
@@ -1,8 +1,10 @@
package cz.xefensor.retold.worldgen.delayed;
import cz.xefensor.retold.Retold;
+import cz.xefensor.retold.api.world.RetoldWorldProtection;
import cz.xefensor.retold.stage.RetoldStageRuntime;
import cz.xefensor.retold.stage.RetoldWorldStage;
+import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
@@ -310,6 +312,14 @@ private static RetrogenResult tryRetrogenStructure(
return RetrogenResult.TRY_LATER;
}
+ if (!RetoldWorldProtection.canRetrogenStructure(
+ level,
+ pos,
+ Identifier.parse(structureId)
+ )) {
+ return RetrogenResult.TRY_LATER;
+ }
+
placeVanillaStructureStartInChunk(level, start, pos);
placedAny = true;
}
@@ -531,4 +541,4 @@ private enum RetrogenResult {
TRY_LATER,
NO_ACTION
}
-}
\ No newline at end of file
+}
diff --git a/src/main/resources/data/retold/tags/block/armadillo_grub_soils.json b/src/main/resources/data/retold/tags/block/armadillo_grub_soils.json
new file mode 100644
index 0000000..6a10993
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/armadillo_grub_soils.json
@@ -0,0 +1,14 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:grass_block",
+ "minecraft:dirt",
+ "minecraft:coarse_dirt",
+ "minecraft:rooted_dirt",
+ "minecraft:podzol",
+ "minecraft:red_sand",
+ "minecraft:terracotta",
+ "minecraft:mud",
+ "minecraft:muddy_mangrove_roots"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/armadillo_scrub_range_blocks.json b/src/main/resources/data/retold/tags/block/armadillo_scrub_range_blocks.json
new file mode 100644
index 0000000..148d165
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/armadillo_scrub_range_blocks.json
@@ -0,0 +1,13 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:grass_block",
+ "minecraft:dirt",
+ "minecraft:coarse_dirt",
+ "minecraft:rooted_dirt",
+ "minecraft:podzol",
+ "minecraft:sand",
+ "minecraft:red_sand",
+ "minecraft:terracotta"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/desert_browse_blocks.json b/src/main/resources/data/retold/tags/block/desert_browse_blocks.json
new file mode 100644
index 0000000..ac9767c
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/desert_browse_blocks.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:dead_bush"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/forage_crops.json b/src/main/resources/data/retold/tags/block/forage_crops.json
new file mode 100644
index 0000000..cd1560c
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/forage_crops.json
@@ -0,0 +1,13 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:wheat",
+ "minecraft:carrots",
+ "minecraft:potatoes",
+ "minecraft:beetroots",
+ "minecraft:melon_stem",
+ "minecraft:pumpkin_stem",
+ "minecraft:attached_melon_stem",
+ "minecraft:attached_pumpkin_stem"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/forage_flowers.json b/src/main/resources/data/retold/tags/block/forage_flowers.json
new file mode 100644
index 0000000..3aa0324
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/forage_flowers.json
@@ -0,0 +1,21 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:poppy",
+ "minecraft:dandelion",
+ "minecraft:blue_orchid",
+ "minecraft:allium",
+ "minecraft:azure_bluet",
+ "minecraft:red_tulip",
+ "minecraft:orange_tulip",
+ "minecraft:white_tulip",
+ "minecraft:pink_tulip",
+ "minecraft:oxeye_daisy",
+ "minecraft:cornflower",
+ "minecraft:lily_of_the_valley",
+ "minecraft:sunflower",
+ "minecraft:lilac",
+ "minecraft:rose_bush",
+ "minecraft:peony"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/goat_scrape_blocks.json b/src/main/resources/data/retold/tags/block/goat_scrape_blocks.json
new file mode 100644
index 0000000..3e15d9b
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/goat_scrape_blocks.json
@@ -0,0 +1,9 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:stone",
+ "minecraft:snow_block",
+ "minecraft:packed_ice",
+ "minecraft:gravel"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/grazer_forage_plants.json b/src/main/resources/data/retold/tags/block/grazer_forage_plants.json
new file mode 100644
index 0000000..a520254
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/grazer_forage_plants.json
@@ -0,0 +1,10 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:grass_block",
+ "minecraft:short_grass",
+ "minecraft:tall_grass",
+ "minecraft:fern",
+ "minecraft:large_fern"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/hoglin_forage_blocks.json b/src/main/resources/data/retold/tags/block/hoglin_forage_blocks.json
new file mode 100644
index 0000000..7eb95da
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/hoglin_forage_blocks.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:crimson_fungus"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/illager_village_signal_blocks.json b/src/main/resources/data/retold/tags/block/illager_village_signal_blocks.json
new file mode 100644
index 0000000..9c608e7
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/illager_village_signal_blocks.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:bell"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/mooshroom_grazing_blocks.json b/src/main/resources/data/retold/tags/block/mooshroom_grazing_blocks.json
new file mode 100644
index 0000000..0209b80
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/mooshroom_grazing_blocks.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:mycelium"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/nether_remnant_guard_anchor_blocks.json b/src/main/resources/data/retold/tags/block/nether_remnant_guard_anchor_blocks.json
new file mode 100644
index 0000000..8041010
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/nether_remnant_guard_anchor_blocks.json
@@ -0,0 +1,16 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:nether_bricks",
+ "minecraft:nether_brick_fence",
+ "minecraft:nether_brick_stairs",
+ "minecraft:nether_brick_slab",
+ "minecraft:cracked_nether_bricks",
+ "minecraft:chiseled_nether_bricks",
+ "minecraft:red_nether_bricks",
+ "minecraft:red_nether_brick_stairs",
+ "minecraft:red_nether_brick_slab",
+ "minecraft:red_nether_brick_wall",
+ "minecraft:nether_brick_wall"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/ocean_monument_guard_anchor_blocks.json b/src/main/resources/data/retold/tags/block/ocean_monument_guard_anchor_blocks.json
new file mode 100644
index 0000000..aeebe97
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/ocean_monument_guard_anchor_blocks.json
@@ -0,0 +1,17 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:prismarine",
+ "minecraft:prismarine_bricks",
+ "minecraft:dark_prismarine",
+ "minecraft:sea_lantern",
+ "minecraft:wet_sponge",
+ "minecraft:prismarine_stairs",
+ "minecraft:prismarine_slab",
+ "minecraft:prismarine_wall",
+ "minecraft:prismarine_brick_stairs",
+ "minecraft:prismarine_brick_slab",
+ "minecraft:dark_prismarine_stairs",
+ "minecraft:dark_prismarine_slab"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/ocean_monument_protected_blocks.json b/src/main/resources/data/retold/tags/block/ocean_monument_protected_blocks.json
new file mode 100644
index 0000000..d3f0ebf
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/ocean_monument_protected_blocks.json
@@ -0,0 +1,10 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:prismarine",
+ "minecraft:prismarine_bricks",
+ "minecraft:dark_prismarine",
+ "minecraft:sea_lantern",
+ "minecraft:wet_sponge"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/panda_bamboo_blocks.json b/src/main/resources/data/retold/tags/block/panda_bamboo_blocks.json
new file mode 100644
index 0000000..b3c6c76
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/panda_bamboo_blocks.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:bamboo",
+ "minecraft:bamboo_sapling"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/piglin_forage_blocks.json b/src/main/resources/data/retold/tags/block/piglin_forage_blocks.json
new file mode 100644
index 0000000..40340dd
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/piglin_forage_blocks.json
@@ -0,0 +1,8 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:crimson_fungus",
+ "minecraft:red_mushroom",
+ "minecraft:brown_mushroom"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/small_passive_forage_plants.json b/src/main/resources/data/retold/tags/block/small_passive_forage_plants.json
new file mode 100644
index 0000000..e1e10ec
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/small_passive_forage_plants.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:short_grass",
+ "minecraft:tall_grass"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/spider_lair_web_blocks.json b/src/main/resources/data/retold/tags/block/spider_lair_web_blocks.json
new file mode 100644
index 0000000..cdc1c91
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/spider_lair_web_blocks.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:cobweb"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/strider_forage_blocks.json b/src/main/resources/data/retold/tags/block/strider_forage_blocks.json
new file mode 100644
index 0000000..d705efa
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/strider_forage_blocks.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:warped_fungus"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/turtle_beach_blocks.json b/src/main/resources/data/retold/tags/block/turtle_beach_blocks.json
new file mode 100644
index 0000000..c2b982e
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/turtle_beach_blocks.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:sand",
+ "minecraft:red_sand"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/block/turtle_forage_blocks.json b/src/main/resources/data/retold/tags/block/turtle_forage_blocks.json
new file mode 100644
index 0000000..b9fbe93
--- /dev/null
+++ b/src/main/resources/data/retold/tags/block/turtle_forage_blocks.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:seagrass"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/alliances/illager_loose_allies.json b/src/main/resources/data/retold/tags/entity_type/alliances/illager_loose_allies.json
new file mode 100644
index 0000000..278b042
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/alliances/illager_loose_allies.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:witch"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/aquatic_hostiles.json b/src/main/resources/data/retold/tags/entity_type/factions/aquatic_hostiles.json
new file mode 100644
index 0000000..70e3f38
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/aquatic_hostiles.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:guardian",
+ "minecraft:elder_guardian"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/arthropods.json b/src/main/resources/data/retold/tags/entity_type/factions/arthropods.json
new file mode 100644
index 0000000..20edbb7
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/arthropods.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:spider",
+ "minecraft:cave_spider"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/bosses.json b/src/main/resources/data/retold/tags/entity_type/factions/bosses.json
new file mode 100644
index 0000000..5396091
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/bosses.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:ender_dragon"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/breezes.json b/src/main/resources/data/retold/tags/entity_type/factions/breezes.json
new file mode 100644
index 0000000..8901163
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/breezes.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:breeze"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/creakings.json b/src/main/resources/data/retold/tags/entity_type/factions/creakings.json
new file mode 100644
index 0000000..382ba5c
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/creakings.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:creaking"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/creepers.json b/src/main/resources/data/retold/tags/entity_type/factions/creepers.json
new file mode 100644
index 0000000..3a35adc
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/creepers.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:creeper"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/endermites.json b/src/main/resources/data/retold/tags/entity_type/factions/endermites.json
new file mode 100644
index 0000000..1dd3b6b
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/endermites.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:endermite"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/enders.json b/src/main/resources/data/retold/tags/entity_type/factions/enders.json
new file mode 100644
index 0000000..2d21bb6
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/enders.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:enderman",
+ "minecraft:shulker"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/illagers.json b/src/main/resources/data/retold/tags/entity_type/factions/illagers.json
new file mode 100644
index 0000000..25e73b2
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/illagers.json
@@ -0,0 +1,8 @@
+{
+ "replace": false,
+ "values": [
+ "#minecraft:illager",
+ "minecraft:ravager",
+ "minecraft:vex"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/nether_beasts.json b/src/main/resources/data/retold/tags/entity_type/factions/nether_beasts.json
new file mode 100644
index 0000000..c3aec3b
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/nether_beasts.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:hoglin"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/nether_remnants.json b/src/main/resources/data/retold/tags/entity_type/factions/nether_remnants.json
new file mode 100644
index 0000000..6da4c42
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/nether_remnants.json
@@ -0,0 +1,8 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:piglin",
+ "minecraft:piglin_brute",
+ "minecraft:blaze"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/silverfish.json b/src/main/resources/data/retold/tags/entity_type/factions/silverfish.json
new file mode 100644
index 0000000..1fdbd4d
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/silverfish.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:silverfish"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/slimes.json b/src/main/resources/data/retold/tags/entity_type/factions/slimes.json
new file mode 100644
index 0000000..152a84e
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/slimes.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:slime",
+ "minecraft:magma_cube"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/undead.json b/src/main/resources/data/retold/tags/entity_type/factions/undead.json
new file mode 100644
index 0000000..38a414c
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/undead.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "#minecraft:undead",
+ "minecraft:ghast"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/village_defenders.json b/src/main/resources/data/retold/tags/entity_type/factions/village_defenders.json
new file mode 100644
index 0000000..bc34914
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/village_defenders.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:iron_golem",
+ "minecraft:snow_golem"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/entity_type/factions/wardens.json b/src/main/resources/data/retold/tags/entity_type/factions/wardens.json
new file mode 100644
index 0000000..72f089c
--- /dev/null
+++ b/src/main/resources/data/retold/tags/entity_type/factions/wardens.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:warden"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/bat_foods.json b/src/main/resources/data/retold/tags/item/bat_foods.json
new file mode 100644
index 0000000..bb106f0
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/bat_foods.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:spider_eye"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/berry_foods.json b/src/main/resources/data/retold/tags/item/berry_foods.json
new file mode 100644
index 0000000..6ae9c3a
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/berry_foods.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "#c:foods/berry"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/campfire_consumable_igniters.json b/src/main/resources/data/retold/tags/item/campfire_consumable_igniters.json
new file mode 100644
index 0000000..afc5b3a
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/campfire_consumable_igniters.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:flint"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/feline_scavenge_foods.json b/src/main/resources/data/retold/tags/item/feline_scavenge_foods.json
new file mode 100644
index 0000000..e41a5e5
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/feline_scavenge_foods.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:phantom_membrane"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/fish_foods.json b/src/main/resources/data/retold/tags/item/fish_foods.json
new file mode 100644
index 0000000..9aa033d
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/fish_foods.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "#minecraft:fishes"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/flower_foods.json b/src/main/resources/data/retold/tags/item/flower_foods.json
new file mode 100644
index 0000000..3aa0324
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/flower_foods.json
@@ -0,0 +1,21 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:poppy",
+ "minecraft:dandelion",
+ "minecraft:blue_orchid",
+ "minecraft:allium",
+ "minecraft:azure_bluet",
+ "minecraft:red_tulip",
+ "minecraft:orange_tulip",
+ "minecraft:white_tulip",
+ "minecraft:pink_tulip",
+ "minecraft:oxeye_daisy",
+ "minecraft:cornflower",
+ "minecraft:lily_of_the_valley",
+ "minecraft:sunflower",
+ "minecraft:lilac",
+ "minecraft:rose_bush",
+ "minecraft:peony"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/grazer_foods.json b/src/main/resources/data/retold/tags/item/grazer_foods.json
new file mode 100644
index 0000000..b092c70
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/grazer_foods.json
@@ -0,0 +1,15 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:wheat",
+ "minecraft:hay_block",
+ "minecraft:apple",
+ "minecraft:carrot",
+ "minecraft:potato",
+ "minecraft:beetroot",
+ "minecraft:short_grass",
+ "minecraft:tall_grass",
+ "minecraft:fern",
+ "minecraft:large_fern"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/leaf_preserving_tools.json b/src/main/resources/data/retold/tags/item/leaf_preserving_tools.json
new file mode 100644
index 0000000..ff7b7ef
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/leaf_preserving_tools.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "#c:tools/shear"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/meat_foods.json b/src/main/resources/data/retold/tags/item/meat_foods.json
new file mode 100644
index 0000000..d3e4d7e
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/meat_foods.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "#minecraft:meat"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/nether_fungus_foods.json b/src/main/resources/data/retold/tags/item/nether_fungus_foods.json
new file mode 100644
index 0000000..39be56a
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/nether_fungus_foods.json
@@ -0,0 +1,8 @@
+{
+ "replace": false,
+ "values": [
+ "#c:mushrooms",
+ "#minecraft:hoglin_food",
+ "#minecraft:strider_food"
+ ]
+}
diff --git a/src/main/resources/data/retold/tags/item/small_passive_foods.json b/src/main/resources/data/retold/tags/item/small_passive_foods.json
new file mode 100644
index 0000000..548e9c8
--- /dev/null
+++ b/src/main/resources/data/retold/tags/item/small_passive_foods.json
@@ -0,0 +1,13 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:wheat_seeds",
+ "minecraft:beetroot_seeds",
+ "minecraft:melon_seeds",
+ "minecraft:pumpkin_seeds",
+ "minecraft:carrot",
+ "minecraft:potato",
+ "minecraft:beetroot",
+ "minecraft:dandelion"
+ ]
+}