RPG sample, for developers
How samples/rpg-sample/ is built and how to change it. What it does for server owners is on the module's page.
The code
Under eu.grayroot.graycore.samples.rpg, next to the plugin, its bootstrap, the module (RpgSampleModule) and /rpgsample:
| Package | What it holds |
|---|---|
mob | Definitions read from mobs:, spawning (MobService), the BetterModel side (MobModels), what a mob is (CustomMob). |
ai | The mob's mind: MobBrain decides every tick, ChaseGoal and TargetPlayerGoal replace the zombie's goals. |
skill | The Skill interface, its settings and types (charge, ground_zone, shoot), and the warnings on the ground. |
combat | The damage models, the combat: settings, hit intervals and damage numbers. |
zone | The zones of zones.yml (Zone, boxes, weights) and ZoneService, which fills them. |
hud | The HUD: its lines, glyphs and settings, and where it gets what it shows. |
loot | loot: and its drops. |
pack | Publishing BetterModel's resource pack and sending it to players. |
The module is registered by another plugin, with registerModule, as described in Writing a module.
A new skill is a class extending AbstractSkill (which holds its settings and the getters of Skill), its SkillSettings, and
one line in SkillTypes. A skill that launches something (an arrow, a fireball) tells it with caster.launched(projectile, this):
the damage is then the mob's times the skill's damageMultiplier(), and onProjectileHit is where an effect such as poison goes.
The resource pack
In production
You do not run any script on a server. The server builds the pack itself:
- BetterModel generates its pack,
plugins/BetterModel/build.zip, from the models. - The module adds the HUD's files to it: the font, its pictures and the shader, which are inside the plugin's jar. The shader is
written at that moment with the place, size and colours of
hud.yml. - The result is copied as
<sha1>.zipintopack.publishDirectory, which a web server serves atpack.publicUrl. The hash changes when a file does. - Players get a request for that URL and hash when they join. When the hash changes, it is sent again to those connected.
/gcadmin reload does 2 to 4 again, which is how a change of hud.yml reaches players. /rpgsample pack rebuild does it on
demand, even if nothing changed, and sends the pack to everyone: it is the command for a web folder that was emptied, or a pack
that players did not receive. /bettermodel reload rebuilds BetterModel's part, and the module publishes the result by itself.
What a server needs: a web server on pack.publicUrl, and the settings of pack: in the module's config.
In development
The files of the HUD in the jar are generated from pictures and numbers, and the generated files are committed. Nothing in the Maven build runs a script, so when you change a picture, you run the generator and commit its result; otherwise production keeps the old files.
The tools
All in samples/rpg-sample/tools/, in Node 20 or newer. None is used by a server or by the build.
| Script | What it does | When |
|---|---|---|
generate-hud.mjs | Writes the files of the HUD's pack from hud-art/ and tools/hud-layout.json: the font, the pictures of the gauges (cut in pieces when they are too wide for the game), the icons, the transparent sprites of the hunger bar and of the hearts, hud-layout.json and the list of the files, hud-pack-files.txt. It draws a default picture for each one that is missing, and removes what an older run left. | After changing a picture of hud-art/ or a number of tools/hud-layout.json. Commit the result. |
generate-model.mjs | Writes the .bbmodel files of src/main/resources/models (geometry, texture, animations). The output is deterministic, so a diff shows what changed; open it in Blockbench to edit by hand. | After changing a model. Commit the result. |
mob-stats.mjs | The curve of the stats of the mobs (life, damage and defense from a level and a role) and the roster of the tiers. --table prints it, --yaml prints the YAML written in mobs.yml, and <level> <role> gives the numbers of one mob. The server does not run it: the numbers are written out in mobs.yml. | When adding a mob to a tier or changing the curve. |
hud-local-pack.mjs | Puts the HUD's files in a pack folder of your own game, to try a change in a second. | While working, see below. |
hud-import/ | Makes the panel of the first version of the HUD from a picture with its background drawn in. | Not used by the current styles. |
The art of the HUD
hud-art/ holds the pictures; each one is drawn by the generator when missing and is yours to replace:
| File | What it is |
|---|---|
gauge.png | The frame of the gauge of the rows of the card (112 x 13 shown). |
gauge_wide.png | The frame of the wide gauges at the bottom of the screen (81 x 9). |
plate.png | The card behind the rows (158 x 42). |
portrait.png | The frame of the head (34 x 34). |
icons/ | compass, map and the three drops of the mana bar. Any size, scaled to the height iconHeights says. |
The gauges, the card and the portrait have 6 times the pixels they are shown with (gauge.scale), so they are not coarse where one
pixel of the HUD is several of the screen. A picture wider than 256 pixels, which the game cannot load as one glyph, is cut into pieces
by the script, which reads PNGs of 8 bits per channel that are not interlaced (save them as RGBA). The fill of a gauge is generated.
tools/hud-layout.json:
| Key | What it is |
|---|---|
gauge | width, height and ascent (how far its top is above the baseline of its line) of the gauge of the rows, wideWidth of the wide one, border (what the fill leaves clear) and scale. |
plate, portrait | Their size, and the ascent of the plate. |
head, headAscent | pixel: how many pixels of the HUD one pixel of the face is; headAscent: how far above the baseline of its line the head starts. |
iconHeights, iconOffsets | How tall each icon is shown, and how many pixels lower it is drawn (negative raises it by 1 at most). |
To change a size, change the number, delete the picture, run node tools/generate-hud.mjs, change the hud.yml lines that
depend on it (the plate and gauge sizes are used as gaps there), and commit.
The shader
hud-pack-shader/.../text.vsh is the vanilla text shader of Minecraft 26.3 with one addition. The HUD is one message of the action
bar, centred by the game; each part has a colour that says where it goes (64 + 4 x id in red, 92 in green, 36 in blue), and the
shader moves it. The message always advances 82 pixels, whatever it holds, so the shader knows where it starts: each line steps back to
where it began (HudMessage). For another version of the game, copy that version's text.vsh and add the HUD block again.
| Constant | What it is |
|---|---|
HUD_MESSAGE_TOP, HUD_TEXT_TOP | Where the game draws the action bar, and where the first line of the HUD should start. |
HUD_BAR_SHIFT_Y | How far below the message the mana drops (and the bottom gauges) are: the height of the hunger bar. |
HUD_BAR_TEXT_SHIFT_Y, HUD_LIFE_LEFT | The height of the texts above the bars, and how far left of the centre the hearts start. |
HUD_ROW_PITCH | The space between the lines of the compact style. |
HUD_FULL_FIRST_PITCH, HUD_FULL_ROW_PITCH | The space between the lines of the card: the second line is 11 below the first, the others 14. |
HUD_COMPACT_MARGIN, HUD_COMPACT_WIDTH, HUD_COMPACT_SCALE, HUD_MARGIN, HUD_SCALE, HUD_LIFE_COLOR, HUD_MANA_COLOR | Written from hud.yml when the pack is built: the default values in the file are the ones of the bundled hud.yml. |
Trying a change in the game
A rebuild and a restart of the server take minutes. To try a change of the shader or of a picture in a second, put the files in a pack of your own game:
cd samples/rpg-sample
node tools/hud-local-pack.mjs "C:/Users/you/AppData/Roaming/.minecraft/resourcepacks" # the resourcepacks folder of the profile you play
Add --gauges when hud.gauges is on: the pack then makes the hearts clear. The script runs the generator first.
The server's own pack goes above the packs of your game and would hide yours, so:
- In the game, Multiplayer, select the server, Edit, Server Resource Packs: Disabled.
- Set
assumePack: trueunderhud:in the module'sconfig.yml, then/gcadmin reload: the plugin then shows the HUD to a game that does not have the server's pack. - Select
graycore-hud-devonce in Options > Resource Packs.
After each change, run the script again and press F3 + T in the game. What the plugin writes (the text of the lines) needs .\dev.ps1 sample
(from dev/), which rebuilds the plugin and restarts only the first Paper. The numbers of hud.yml (place, size, colours) are written into
the shader by the plugin, so the local pack does not read them: change the constants of the shader for a quick try.
Remove assumePack: true and enable the server's pack again when you are done.
Tests
mvn verify runs the tests of the sample. JaCoCo measures them (samples/rpg-sample/target/site/jacoco), and the build fails when a package of the sample falls under 80 %. The tests that read the generated files check the pack too: that every picture of the font is
in the pack, fits the game's limit of 256 x 256 pixels, and is shown as wide as the plugin counts on.
Testing what needs the game
- Mockito is used for most things, with
mockStatic(Bukkit.class)when a class asks the server for something. - MockBukkit (
mockbukkit-v26.3, for the version of Paper of the project) gives a server where the Paper API cannot work without one: theAttributeconstants need the game's registries, which only exist onceMockBukkit.mock()was called. CallMockBukkit.unmock()after. A test that also stubsBukkit(the module) usesmockStatic(Bukkit.class, CALLS_REAL_METHODS): what it does not stub asks the MockBukkit server, while a plain mock makes the first load of a registry class fail. - MockBukkit does not do everything, and what it does not do throws
UnimplementedOperationException, which JUnit counts as an aborted test: the test is skipped, not failed.World#spawn(location, type, reason, randomize, consumer), whichMobServiceuses, is one of them. After adding a test, look at theSkipped:of the report: it must be 0. FakeBodies(in the tests ofmob) makes a world and bodies of mocks that remember what they are given: their data, their attributes, what rides them, whether they are still in the world.FakeBetterModelmakes BetterModel answer through mocks (its static methods, a tracker, a pipeline), and must be made before any other mock of a BetterModel class, since those classes ask it for its platform when they are loaded.MobFixturesreads a mob from a few lines of YAML, as the module does.