Repository navigation
Architecture
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.
| 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.
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
┌──────────────────────────────┐
/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:
-
ClassManagerdefines a set of overridable event hooks; each race subclasses it and overrides only the hooks it cares about. -
SupernaturalsPluginowns every manager instance and dispatches throughgetClassManager(player)bysuperType. - 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.
| 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 |
- Store
instance, create the data directory, grab thePluginManager. - Register 17
SNCommandinstances in thecommandslist. - Instantiate the 6 listeners (their constructors register the events).
- Write the current version to
VERSIONif the file does not exist. - Load
config.yml(write all defaults if it does not exist). - Read
data.ymlintoSuperNManager.supernaturals. - Read
whitelistYaml.yml. - Start the timer:
scheduleSyncRepeatingTask(..., 0, 20). -
HunterManager.createBounties(): draw the initial bounties. -
setupPermissions(): detect permissions plugins; logging only.
- Cancel the timer.
-
SNDataHandler.write()→storage.dat. -
saveData()→data.yml. - Remove cobwebs left by demons.
- Log.
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))
spellEventonly fires for “player hits player”; hitting a mob does not cast a spell.
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.
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
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
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.
| 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 |
| 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}
powergoes through the getter/setter (and is clamped);superPoweris 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.
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)
| 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 |
| 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.
HarnessPlugincan change without compatibility concerns.
-
SNConfigHandler: add config fields, first-start defaults,loadValuesreads, and material/entity list parsing. -
SuperNPlayer: add theisX()predicate. -
New
XManager extends ClassManager: override the event hooks you need (damage, death, interaction, armor…). -
SupernaturalsPlugin: field + getter + agetClassManagerbranch. -
SNConfigHandler.supernaturalTypes: add the type to the default list; an existingconfig.ymlalso needs it added toSupernatural.Types, or/sn convertwill refuse it. - Collection/restriction lists: weapons, armor, truce mobs, food.
-
plugin.yml: add dedicated permission nodes if the race needs them. -
Tests: add at least one
*ITcovering “enter the race → use one ability → leave the race”.
-
Every Bukkit API call is on the main thread.
HarnessPluginand all listeners respect this; asynchronous tasks may only do pure computation. -
ClassManager.damagerEventbelongs to the attacker’s manager, andvictimEventto the victim’s. Swapping them causesClassCastExceptions 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.supernaturalsand everything inSNConfigHandlerare static; account for leftovers in tests or hot reloads. -
Enum names are configuration values: renaming a
MaterialorEntityTypesilently invalidates old config.SNConfigHandlerscans keys containingmaterialat startup and warns; check the log after a version change.