Skip to main content

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:

PackageWhat it holds
mobDefinitions read from mobs:, spawning (MobService), the BetterModel side (MobModels), what a mob is (CustomMob).
aiThe mob's mind: MobBrain decides every tick, ChaseGoal and TargetPlayerGoal replace the zombie's goals.
skillThe Skill interface, its settings and types (charge, ground_zone, shoot), and the warnings on the ground.
combatThe damage models, the combat: settings, hit intervals and damage numbers.
zoneThe zones of zones.yml (Zone, boxes, weights) and ZoneService, which fills them.
hudThe HUD: its lines, glyphs and settings, and where it gets what it shows.
lootloot: and its drops.
packPublishing 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:

  1. BetterModel generates its pack, plugins/BetterModel/build.zip, from the models.
  2. 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.
  3. The result is copied as <sha1>.zip into pack.publishDirectory, which a web server serves at pack.publicUrl. The hash changes when a file does.
  4. 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.

ScriptWhat it doesWhen
generate-hud.mjsWrites 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.mjsWrites 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.mjsThe 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.mjsPuts 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:

FileWhat it is
gauge.pngThe frame of the gauge of the rows of the card (112 x 13 shown).
gauge_wide.pngThe frame of the wide gauges at the bottom of the screen (81 x 9).
plate.pngThe card behind the rows (158 x 42).
portrait.pngThe 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:

KeyWhat it is
gaugewidth, 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, portraitTheir size, and the ascent of the plate.
head, headAscentpixel: 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, iconOffsetsHow 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.

ConstantWhat it is
HUD_MESSAGE_TOP, HUD_TEXT_TOPWhere the game draws the action bar, and where the first line of the HUD should start.
HUD_BAR_SHIFT_YHow 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_LEFTThe height of the texts above the bars, and how far left of the centre the hearts start.
HUD_ROW_PITCHThe space between the lines of the compact style.
HUD_FULL_FIRST_PITCH, HUD_FULL_ROW_PITCHThe 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_COLORWritten 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:

  1. In the game, Multiplayer, select the server, Edit, Server Resource Packs: Disabled.
  2. Set assumePack: true under hud: in the module's config.yml, then /gcadmin reload: the plugin then shows the HUD to a game that does not have the server's pack.
  3. Select graycore-hud-dev once 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: the Attribute constants need the game's registries, which only exist once MockBukkit.mock() was called. Call MockBukkit.unmock() after. A test that also stubs Bukkit (the module) uses mockStatic(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), which MobService uses, is one of them. After adding a test, look at the Skipped: of the report: it must be 0.
  • FakeBodies (in the tests of mob) 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. FakeBetterModel makes 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. MobFixtures reads a mob from a few lines of YAML, as the module does.