Skip to content

Architecture

VampireAchao edited this page Oct 28, 2026 · 3 revisions

Architecture and Core Classes

This page is for people reading or changing the code: the stack, the directory layout, what the core classes do, how startup and events flow, how data is persisted, how the test harness works, and what to touch when adding a race.


1. Stack and dependencies

Layer Choice Version / notes
Language Java 25 (maven.compiler.release=25)
Build Maven pom.xml, packaging=jar
Server API Paper API 26.3.build.159-beta, scope=provided
Regions/flags WorldGuard 7.0.12, provided, softdepend
Configuration Bukkit YamlConfiguration config.yml
Player data SnakeYAML mapped straight onto the SuperNPlayer bean with a custom !player tag
Auxiliary data Java serialization SNDataHandler implements Serializable → storage.dat
Unit tests JUnit Jupiter 5.11.4, test
Fake-player network Netty 4.1.139.Final, test (the server supplies Netty at runtime)
Integration tests Maven Failsafe 3.5.2, -Pit profile, matching **/*IT.java
Manual server entry exec-maven-plugin 3.5.0, TestServer.main
Release GitHub Actions .github/workflows/release.yml: a v* tag builds, runs unit tests, and publishes; refuses a tag that disagrees with pom.xml

The plugin bundles no dependencies: Paper and WorldGuard are provided, so the jar contains only the classes compiled from its 51 source files plus plugin.yml.


2. Directory layout

mmSupernaturals/
├── pom.xml
├── README.md / CHANGELOG.md / NOTICE / LICENSE / gpl-3.0.txt
├── licenses/GNU-FDL-1.3.txt
├── harness-skins.properties.example      # optional skin override for the hall
├── .github/workflows/release.yml         # tag → build → unit tests → GitHub Release
├── tools/check-status.py                 # checks the download pages the project points at
├── docs/
│   └── mmSupernaturals-cover-v3.png
├── src/main/
│   ├── java/me/matterz/supernaturals/
│   │   ├── SupernaturalsPlugin.java      # JavaPlugin entry point / service locator
│   │   ├── SuperNPlayer.java             # player data bean (race + power)
│   │   ├── commands/    (18)             # /sn subcommands
│   │   ├── io/          (5)              # config, player data, whitelist, version
│   │   ├── listeners/   (6)              # Bukkit event listeners
│   │   ├── manager/     (11)             # race logic + global manager
│   │   └── util/        (9)              # geometry, armor, recipes, timer, material tiers…
│   └── resources/plugin.yml
└── src/test/
    ├── java/me/matterz/supernaturals/
    │   ├── PluginDescriptorTest.java
    │   ├── SuperNPlayerPowerTest.java
    │   ├── PowerCostTest.java            # the >= boundary of a power cost
    │   ├── io/SaveFilePowerTest.java
    │   └── it/                           # scenario harness
    │       ├── TestServer.java           # starts/resets a real Paper server
    │       ├── ControlClient.java        # line-protocol client
    │       ├── Actor.java                # fake-player facade
    │       ├── ServerTest.java / Arena.java
    │       ├── harness/                  # test-only companion plugin
    │       │   ├── HarnessPlugin.java    # control channel + /mmp
    │       │   ├── Nms.java              # the only reflection into server internals
    │       │   ├── Hall.java / HallGuard.java / Shrines.java
    │       │   └── Skins.java / SkinArt.java / SkinServer.java
    │       └── VampireRaceIT.java / SkinsIT.java
    └── resources/harness/plugin.yml

3. Layers and responsibilities

                ┌──────────────────────────────┐
   /sn command ─▶│ commands/  SNCommand + 17 subclasses │
                └──────────────┬───────────────┘
                               │ calls
                ┌──────────────▼───────────────┐
   Bukkit events▶│ listeners/  6 Listener classes │
                └──────────────┬───────────────┘
                               │ dispatch by race
                ┌──────────────▼───────────────┐
                │ manager/    ClassManager family│
                │  + SuperNManager (global state)│
                └──────────────┬───────────────┘
                               │ read/write
                ┌──────────────▼───────────────┐
                │ io/  Config / Data / Player   │
                │      Whitelist / Version      │
                └──────────────────────────────┘

The design is template method plus service locator, not dependency injection:

  • ClassManager defines a set of overridable event hooks; each race subclasses it and overrides only the hooks it cares about.
  • SupernaturalsPlugin owns every manager instance and dispatches through getClassManager(player) by superType.
  • A lot of state is static (configuration, the player list, the timer), which made the old code easy to migrate but means one plugin instance per JVM.

Core classes

Class Responsibility Key detail
SupernaturalsPlugin Plugin entry point; registers commands/listeners; owns every manager; WorldGuard queries onEnable performs config, data, timer and bounty initialization
SuperNPlayer Player data bean: playername, superType, oldSuperType, superPower, oldSuperPower, truce, truceTimer setPower is the only writer of power and clamps to 0–10000; scale(x) applies the power ratio
SuperNManager Global state and shared rules: player list, convert/cure/revert, power changes, jumping, truce, regeneration, target selection, name colours, timer Almost every cross-race rule lives here; canPay(snplayer, cost) is the single affordability check (>=, not >)
ClassManager Abstract base for race managers Hooks: damagerEvent / victimEvent / spellEvent / deathEvent / killEvent / playerInteract / armorCheck / shootArrow
HumanManager Human: no abilities; converts on death under the right conditions PriestManager and HunterManager extend it
VampireManager Altars, sunlight burning, teleport, jump, drowning, wooden weakness standsInSunlight uses sky light to decide whether there is a roof
WereManager Night bonuses, wolf summoning, dash, Wolfbane, food charging Summon/dash/night bonuses check worldTimeIsNight; food charging does not
GhoulManager Water weakness, weapon immunity, zombie summoning, Unholy Bond bonds: ghoul → victim
PriestManager Church altar, donations, six spells, Guardian Angel Spells are triggered by spellEvent (hitting a player)
DemonManager Fire immunity, lava charging, fireball, cobweb, conversion, snowball cure webMap tracks its own cobwebs and is cleared on shutdown
HunterManager Bounties, arrow switching, four special arrows, grapple, hall door arrowMap records each arrow’s type; grappleOwners maps a player to the token of the pull that owns him, and grappleSpent records what each shot actually cost
AngelManager Healing, purification, jump, animal summoning, water charging Summoning has a whole “find ground to stand on” routine
EnderBornManager Pearl teleport toggle, chance-based attack bonus, pearl guard, death counter deathTimesMap is in memory only
SNConfigHandler Reads/saves config.yml; parses strings into Material / EntityType Writes all defaults on first start; warns about invalid material names at startup
SNPlayerHandler SnakeYAML reader/writer for data.yml Re-clamps power on load (1.5.0)
SNDataHandler storage.dat: teleport points, guardian angels, hunter applications Java serialization; written only on shutdown
SNWhitelistHandler Sign-up list in whitelistYaml.yml Only meaningful when EnableJoinCommand is on
SNVersionHandler Reads/writes the VERSION file Used by the config version check
Recipes Material → quantity recipe model playerHasEnough / removeFromPlayer / getRecipeLine
MaterialTiers Resolves a material’s tier and kind so a rule about one member covers the whole tier completeWeapons widens a ban to every kind of the tier; completeArmour only pairs diamond↔netherite; tiersOf turns the ghoul’s immunity list into tier names
GeometryUtil Enumerates and counts blocks in a sphere/cube Altars use countNearby
Armor Estimates damage reduction from armor durability Used by Unholy Bond and Guardian Angel
SNTaskTimer The per-tick global clock Calls advanceClock() first, then schedules advanceTime per online player
ArrowUtil Per-tick grapple pull, one block per tick A Runnable that re-schedules itself one tick later (delay 0 would run the whole chain inside one tick); carries its own token and spent power, stands down if a later pull owns the player, and asks HunterManager.refundGrapple when it moved nobody

4. Startup and shutdown

onEnable() order

  1. Store instance, create the data directory, grab the PluginManager.
  2. Register 17 SNCommand instances in the commands list.
  3. Instantiate the 6 listeners (their constructors register the events).
  4. Write the current version to VERSION if the file does not exist.
  5. Load config.yml (write all defaults if it does not exist).
  6. Read data.yml into SuperNManager.supernaturals.
  7. Read whitelistYaml.yml.
  8. Start the timer: scheduleSyncRepeatingTask(..., 0, 20).
  9. HunterManager.createBounties(): draw the initial bounties.
  10. setupPermissions(): detect permissions plugins; logging only.

onDisable() order

  1. Cancel the timer.
  2. SNDataHandler.write() → storage.dat.
  3. saveData() → data.yml.
  4. Remove cobwebs left by demons.
  5. Log.

5. Event flow

Damage (SNEntityListener.onEntityDamage, priority NORMAL)

1. If already cancelled → return
2. Truce check: victim is a player, attacker is on their truce list → cancel
3. spellEvent: attacker and victim are both players → attacker's manager spell
4. damagerEvent: attacker is a player → attacker's manager modifies damage
5. victimEvent: victim is a player → victim's manager modifies damage
6. Unholy Bond: victim is bonded → halve damage and transfer part of it
7. Guardian Angel: lethal damage → cancel, restore health, consume the guard
8. event.setDamage(round(damage))

spellEvent only fires for “player hits player”; hitting a mob does not cast a spell.

Where the material-tier layer is called

MaterialTiers is consulted at three points, and nowhere else:

Caller Question
SNConfigHandler.loadValues Widen each *.Weapon.Restrictions and pair the tier of each *.Armor list, once per load
GhoulManager.victimEvent Is this a tool or weapon of an immune tier? (tier + kind, so a diamond block is not judged)
AngelManager.damagerEvent / forbiddenArmor Is this a paired weapon (diamond/netherite, any kind)? Is this armour of the golden/diamond/netherite tier?

The defaults themselves are generated from the same class (weaponNames(...) / armourNames(...) in SNConfigHandler), which is why a default can no longer be half a tier.

Death (SNEntityMonitor.onEntityDeath, priority MONITOR)

1. Resolve the last hit (direct living entity / projectile shooter)
2. Dead entity is a wolf → remove it from WereManager's wolf map
3. Dead entity is a Creature with a player attacker → killEvent(attacker, null)
4. Dead entity is not a player → return
5. Player death:
   a. Witch hunter suicide → cure automatically
   b. Human kills a supernatural → record superType; invite after 3 distinct
   c. Attacker's Manager.killEvent(...)
   d. Victim's Manager.deathEvent(...)   # power penalty, death conversion, enderborn count

Interaction (SNPlayerListener.onPlayerInteract, priority LOW)

1. Ender pearl right-click on clay → convert to enderborn
2. Multi-world permission check
3. Right/left-click an iron door: scan a 5×5×5 box for signs, match WitchHunter → hall logic
4. Hand over to the player's race Manager.playerInteract(...)
5. If not cancelled and it was a right-click on a block:
   - gold block → vampire infect altar
   - lapis block → vampire cure altar
   - diamond block → priest altar

Global clock (SNTaskTimer)

Every 20 ticks (1 second):
  SuperNManager.advanceClock()          # advance taskCounter once, server-wide
  for each online player:
      scheduleSyncDelayedTask(advanceTime(player))

advanceTime uses the race and taskCounter % N to decide which effects run this second, then runs armorCheck for everyone.

Other listeners

Listener Event Purpose
SNPlayerListener PlayerItemConsumeEvent Vampires eating food → cancel
SNPlayerListener PlayerKickEvent Vampire using a jump item flagged for flying → cancel the kick
SNPlayerMonitor PlayerPortalEvent Check for full leather when entering the Nether (demon prerequisite)
SNPlayerMonitor PlayerJoinEvent Whitelist notice, infinite-power refill, name colours, login broadcast
SNBlockListener BlockBreakEvent Clean up a demon cobweb when broken
SNBlockListener SignChangeEvent Players without permission cannot create WitchHunter signs
SNServerMonitor PluginEnableEvent Detect a permissions plugin that loads later

6. Data and persistence

File Format Written when Contents
config.yml Bukkit YAML First start; /sn save; church add/remove, banish point set; the one-time 1.5.1 ghoul-truce widening Every tunable setting
data.yml SnakeYAML list with !player tags Convert/cure/revert, truce countdown change, /sn save, shutdown Each player’s race, old race, power, truce state
storage.dat Java serialization Shutdown only Vampire teleport points, guardian angel map, hunter applications
whitelistYaml.yml Bukkit YAML Manual/external save only WhitelistedPlayers
VERSION Plain text Startup / version change The plugin version last loaded

data.yml is a list of SuperNPlayer beans with a !player tag. SnakeYAML maps both public fields and getter/setter properties, so one concept can appear under two keys:

Concept Possible keys
Player name playername / name
Current race superType / type
Old race oldSuperType / oldType
Old power oldSuperPower / oldPower
Current power superPower / power
Truce state truce
Truce countdown truceTimer

A minimal entry (the form exercised by the unit tests):

- !player {name: Steve, playername: Steve, superType: vampire, power: 10000.0, superPower: 10000.0}

power goes through the getter/setter (and is clamped); superPower is the field (written directly). Since 1.5.0, the loader writes the value back through the setter once, so even a hand-edited file obeys the 0–10000 bound.


7. Test harness

The scenario tests are not mocks: they start a real Paper server and use “players with no client” to click blocks, take hits and die.

TestServer (JUnit fixture / main)
   │ 1. reset target/it/server (keeping paper.jar, libraries, versions)
   │ 2. install mmSupernaturals.jar + mmHarness.jar
   │ 3. write server.properties (offline mode, superflat, fixed seed)
   │ 4. start the child process, wait for the control port to answer ping
   ▼
HarnessPlugin (test-only companion plugin, never shipped)
   │ provides /mmp and a 127.0.0.1 line protocol
   │ uses Nms reflection to create real ServerPlayer objects
   ▼
ControlClient (one request line / one response line)
   ▼
Actor (spawn / give / perm / tp / interact / block / count / quit)
   ▼
*IT (VampireRaceIT, SkinsIT)

Key classes

Class Purpose
TestServer Downloads Paper, resets the directory, writes configuration, starts/stops, exposes run / probe / spawnActor
ControlClient Line-protocol client; any response not starting with ok throws, so a scenario cannot silently continue
HarnessPlugin Control-channel server; every operation is scheduled onto the main thread (the Bukkit API is not thread-safe)
Nms The only place reflecting into net.minecraft.* / CraftBukkit; creates a clientless ServerPlayer, an in-memory Netty connection, and reads skin properties
Actor Fake-player facade: race(), grant(), give(), teleport(), interact(), blockAt(), count()
ServerTest Scenario base class; ground level, altar construction, forced chunk loading
Arena Test-site coordinates and block commands (vampire altar at 8,8; cure altar at 24,8)
Shrines Single source of altar shapes: obsidian base + gold block / glowstone base + lapis / quartz base + diamond block
Hall / HallGuard The manual server’s visual hall: one world per race, one room per step, signs as actions; HallGuard prevents damage and cleans up
Skins Decides what each stand-in wears: the drawn skin by default, or a texture named in harness-skins.properties
SkinArt Draws the stand-ins’ skins at run time — 64×64, two flat colours and a trim stripe per race, painted rectangle by rectangle, so the harness owns its artwork
SkinServer Loopback HTTP server that serves those PNGs on a port picked at startup; the client is what fetches a skin, so it has to be reachable over HTTP

Control protocol (internal, not stable)

Request Purpose
ping Health check; returns pong
spawn <name> / quit <name> Add/remove a fake player
type <name> Read the race the plugin has on file
give <name> <material> <count> Give items
perm <name> <node> <true / false> Set a permission and read back whether it took effect
tp <name> <x> <y> <z> Teleport
interact <name> <x> <y> <z> Right-click a block; returns cancelled=… block=…
block <name> <x> <y> <z> Read a block
count <name> <material> Count a material in the inventory
run <command> Run a server command as the console
probe <...> Query server-side state such as the hall, skins or equipment

The protocol serves the tests only; it is not a public API. HarnessPlugin can change without compatibility concerns.


8. Adding a race

  1. SNConfigHandler: add config fields, first-start defaults, loadValues reads, and material/entity list parsing.
  2. SuperNPlayer: add the isX() predicate.
  3. New XManager extends ClassManager: override the event hooks you need (damage, death, interaction, armor…).
  4. SupernaturalsPlugin: field + getter + a getClassManager branch.
  5. SNConfigHandler.supernaturalTypes: add the type to the default list; an existing config.yml also needs it added to Supernatural.Types, or /sn convert will refuse it.
  6. Collection/restriction lists: weapons, armor, truce mobs, food.
  7. plugin.yml: add dedicated permission nodes if the race needs them.
  8. Tests: add at least one *IT covering “enter the race → use one ability → leave the race”.

Constraints when changing code

  • Every Bukkit API call is on the main thread. HarnessPlugin and all listeners respect this; asynchronous tasks may only do pure computation.
  • ClassManager.damagerEvent belongs to the attacker’s manager, and victimEvent to the victim’s. Swapping them causes ClassCastExceptions or rules that never fire.
  • getItemInHand() never returns null on modern APIs, though the code still keeps old null checks; do not rely on it to distinguish an empty hand.
  • Static state: SuperNManager.supernaturals and everything in SNConfigHandler are static; account for leftovers in tests or hot reloads.
  • Enum names are configuration values: renaming a Material or EntityType silently invalidates old config. SNConfigHandler scans keys containing material at startup and warns; check the log after a version change.