Kzeroko KTM2

Reputation runtime

Server incident hooks, persisted ownership, relation overrides and report snapshots.

Updated Sep 30, 2026 9 min read

Reputation is an internal server subsystem under internal.reputation. The public ReputationAPI exposes faction names and identities only; standing and relations stay internal. It combines entity affiliation, relation overrides, incident accounting and report projection; Reputation documents player-visible numbers.

What it does

FactionContextResolver resolves two scopes. entityFactions is the standing scope: ReputationFactionProvider, BaseNpcFactionBridge and matching identity rules. It is what incidents charge and witnesses share. targetingFactions is the relation scope: the standing scope plus affinity rules, a player’s FactionMembershipPower factions and the implicit monster side. A player’s relation scope is their memberships alone. Location membership is a separate operation: cities come from CityAPI.getCitiesAt; village structures add the general and start-chunk-specific village factions. Entity affiliation does not automatically inherit current location.

targetingFactions adds an implicit monster side only for monster entities without a provider, base-NPC identity or matching identity rule; affinity rules do not suppress it. ReputationEntityFactionRegistry resolves the rules an entity type matches by ID, tag and spawn group once per type, re-checks only group rules per entity, and clears that cache when rules or tags reload.

FactionTargetingService distinguishes HOSTILE, NON_HOSTILE and UNSPECIFIED. A hostile relation yields HOSTILE. A friendly relation yields NON_HOSTILE unless the target is a player who is the source’s current attacker, which yields UNSPECIFIED. A neutral relation and an empty faction set on either side yield UNSPECIFIED; null or identical identities yield NON_HOSTILE. The general targeting veto only rejects NON_HOSTILE. spares reports NON_HOSTILE for harm that is not a target choice: the pufferfish sting and the elder guardian’s curse consult it.

serverTickMobTargeting never re-judges an established target; EntityUtil.clearInvalidMobTarget does that earlier in the same tick. A free mob scans every 20 ticks (1 second), staggered by entity ID, within its follow range clamped to 8–48 blocks. A default-hostile mob looks for custom NPCs it is positively hostile to. Any mob that is not tamed or player-ridden then looks for the factions FactionRelationRegistry.pursuedFactions returns from proactive rules, judging a player by their perceived morph identity. The pursuit index only narrows candidates; acquisition still requires HOSTILE, canTarget, detection and sight. Acquisition writes MobEntity.setTarget and the vanilla attack-target memory, so point a proactive rule’s pursuing side at mobs whose AI reads them. Saints Dragons keeps a dragon’s target in its own brain memory and clears an outside target on its next brain tick, which is why the bundled raider and dragon feud is not bidirectional.

FactionSolidarityService answers ICLivingEntityEvents.ON_APPLY_DAMAGE_END and the death handler. It collects MobEntity candidates around the victim that share one of its standing-scope factions, then keeps those whose relation to the victim is friendly and that pass FactionWitnessFinder.witnessesIncident, nearest first, up to 8. Each witness receives the attacker through setAttacker, which vanilla revenge goals and their group revenge, the NPC threat sensor and brain-driven species all read; an idle witness that fights through goals, meaning it has an attack damage attribute, also gets EntityUtil.setMobTarget. A non-player attacker the witness is friendly with is skipped. Harm rallies idle witnesses within 16 blocks once per 40 ticks per victim, tracked in a weak map; a kill rallies witnesses within 24 blocks that are not already targeting the killer. Tamed witnesses and creative or spectator attackers are ignored.

Runtime model

FactionReputationService consumes applied damage, kills, loot filling and container-slot extraction. Damage attribution accepts an attacking player, a direct player source or a projectile’s player owner. Slot extraction snapshots inventory stacks before ScreenHandler.onSlotClick and compares them after return; changed item identity counts the entire previous stack as removed. This is a screen-action hook, not an inventory-wide transaction observer.

The service filters eligible factions, applies wanted-faction fate adjustment per faction, groups by resulting delta, then updates state, vanilla gossip and popups before sending a refreshed report. Directly calling FactionReputationState.add bypasses that orchestration.

Reputation and runtime relations use the overworld persistent-state manager through IsekaiPersistData, with keys faction/reputation_v1 and faction/relations_v1 below the mod namespace. State is keyed by player UUID and normalised faction ID, independent of the player’s current dimension.

Saved data

Both state formats write version 1. Missing version reads as 0; a version above 1 warns and returns empty state. Malformed list entries are skipped. Later duplicate faction or relation records overwrite earlier values. Reputation numbers clamp to −1000–1000 on read.

NBT keyTypeDefaultWhat it does
versionint0 on readState format version; writers emit 1. Shared by both files.
playerslist<compound>emptyReputation file’s player records.
players[].playerUUIDrequired per recordMissing/wrong UUID skips that record.
players[].factionslist<compound>required per recordMissing list skips player record.
players[].factions[].factionstringrequiredNormalised faction ID; registers a definition when read.
players[].factions[].valueintrequiredStored standing, clamped on read.
overrideslist<compound>emptyRelation file’s directed overrides.
overrides[].sourcestringrequiredNormalised source faction.
overrides[].targetstringrequiredNormalised target faction.
overrides[].relationstringrequiredRelation parser accepts canonical names and aliases from the data format; invalid values warn and skip the record.

Block/entity metadata is separate from those state files:

NBT keyTypeDefaultWhat it does
IsekaiReputationFactionslist<string>emptyBlock entity’s protected faction IDs; normalised and de-duplicated on read. Written only when nonempty.
IsekaiPlayerPlacedContainerOwnerUUIDabsentAny valid UUID marks personal storage, exempt from faction theft for all players. Written only when present.
IsekaiStructureOriginFactionslist<string>emptyEntity’s saved structure-origin affiliations. Included in reputation only through an opting-in faction rule.

Player placement clears protected factions, records the owner and marks the block entity dirty. Loot filling skips player-placed containers; otherwise it records available location factions. Theft chooses a double inventory’s side by slot, checks player placement, then saved faction IDs, then location fallback.

Structure-template entity loading pushes a world onto a thread-local stack. Entity NBT loading claims location factions only while that context exists. This is not a general spawn or current-position affiliation rule.

Contracts

Use mutation and query entry points on the server thread with a loaded server. Internal maps provide no concurrent access contract. World-save state survives restart and data reload; incident cooldown maps are process memory, not NBT. Data reload changes rule registries without erasing saved standing or relation overrides.

FactionRelationService.setRuntimeRelation normalises IDs, optionally writes both directions and marks state dirty. Null server/relation or blank IDs produce no change. clearRuntimeRelation removes the requested direction(s), revealing data-pack defaults again. Across matching override pairs, hostile has precedence over friendly, then neutral; an override result precedes all data rules.

FactionReputationState.add returns the last processed nonnull faction’s resulting value, not the delta or an aggregate. snapshot returns an immutable map. Missing player/faction reads return 0. Use the service’s quest-reward entry point when a reward should also create gossip, popup and report updates.

Report projection

ReputationReportSyncService builds the report from default factions, known definitions and saved entries. Unrecorded, non-default factions at 0 are omitted. Sorting uses explicit faction/kind priority, then server-composed name text, then faction ID; it is not re-sorted by the client’s translated display string.

ReputationFactionNaming maps village, profession, NPC type, unique NPC, citizen origin and citizen culture IDs to settlement, trade-circle, kinship, personal, homeland and culture records. Other IDs use charter presentation. Names, subtitles and descriptions are transmitted as Text; settlement descriptions are rebuilt relative to the requesting player. Names are deterministic finite combinations and can collide.

The packet sends at most 512 rows. The report request packet and successful service mutations rebuild the report; the client applies the received snapshot on its thread. This is a presentation snapshot, not a client-authoritative reputation store.

Naming facade

api.reputation.ReputationAPI and the client-only api.client.ReputationClientAPI let other mods label places with the names and emblems the ledger uses. They read naming rules only: nothing registers a faction, changes standing or touches saved state. The IsekaiTweaks atlas names village pins and draws their faction badge through them.

SignatureSideReturns
ReputationAPI.settlementFactionId(RegistryKey<World>, ChunkPos)EitherThe faction ID a village starting in that chunk receives: minecraft:village/<dimension>/structure/<x>/<z>, using the start chunk’s coordinates.
ReputationAPI.settlementName(RegistryKey<World>, ChunkPos)EitherThe settlement’s place name as a plain string, hashed from the faction ID with a fixed FNV-1a. Every player, session and side sees the same name.
ReputationAPI.factionName(String)EitherThe display Text the ledger gives any faction ID.
ReputationClientAPI.factionIcon(String)ClientThe 48 × 48 emblem texture the ledger draws for that faction ID, chosen by ID prefix. An unrecognised ID gets the villager emblem.

Pass the structure’s start chunk, not the chunk a player stands in; any other chunk yields an unrelated ID. Settlement names are finite combinations and can repeat across villages, so key anything persistent on the faction ID.

Extending it

For an internal story integration, set the runtime relationship through the service:

void changeAlliance(MinecraftServer server) {
    FactionRelationService.setRuntimeRelation(server,
            "example:city", "example:guild", ReputationRelation.FRIENDLY, true);
}

Use clearRuntimeRelation(server, sourceId, targetId, true) to remove both directions. For membership or protector roles, prefer the data rules. A Java affiliation provider must supply actual faction definitions; standing at a location is not sufficient.

Verifying

Check player-placed and generated containers, both halves of a double chest, stack reduction and item replacement. Repeat with an obstructed, sleeping and blinded witness, then with no witness. Confirm cooldown separation by victim/container and dimension.

Check that a player with a membership power is spared by friendly mobs, that hurting one lets it retaliate until its attacker memory expires, and that a neutral pair still fights under its own AI. Spawn both sides of a proactive rule outside each other’s natural targeting, then repeat with one side tamed or ridden, and with a player wearing a pursued form. Reload tags and rules and confirm matching follows the new data.

Set standing on each side of −100 and compare the countenance choices across factions in the same event. Save/restart and reload data to distinguish persistence from rule defaults. Set then clear a runtime relation and confirm its fallback. Request the report from different positions and dimensions, and confirm that duplicate display names still retain distinct faction IDs.

Reputation covers gameplay; Reputation data covers authoring and loader failures.