JCM:Building a Scripted PIDS Preset
Views
Actions
Namespaces
Variants
Tools
This is a step-by-step tutorial on building a Scripted PIDS Preset using JavaScript.
By the end of the tutorial, you would have built a Fictional LRT (Light Rail) version of the MTR Railway Vision PIDS, which can adapt to Custom PIDS Messages and handling long string.
Getting started
To get started, download the tutorial resource pack here ,extract the zip and put it in Minecraft's Resource Pack folder.
This resource pack is specifically set-up for this tutorial, and therefore the JS scripts inside are (mostly) empty. However we can still take a look at how a Scripted PIDS Preset is set-up.
joban_custom_resources.json
The file will look like this:
{ "pids_images": [ { "id": "pids_tut", "name": "DIY JS Preset", "scripts": ["jsblock:scripts/pids_tut.js"] } ] }
This is mostly the same from a JSON PIDS Preset, except the scripts
property which is a JSON array that points to the scripts used for this preset.
Multiple scripts can be used for the same preset as well. However to keep things simple, we are simply going to use 1 script to control the rendering: scripts/pids_tut.js
.
scripts/pids_tut.js
By opening up pids_tut.js in the scripts folder in a text editor (Notepad etc.), we can see the following script:
function create(ctx, state, pids) { // Your custom logic here... } function render(ctx, state, pids) { // Your custom logic here... } function dispose(ctx, state, pids) { // Your custom logic here... }
Our custom logic can be placed between each curly brackets for each of the function. As seen, there are a total of 3 functions: create
, render
, dispose
.
The create
function is called when our PIDS enters the player's view. So this means that when a player switches the PIDS preset to our preset, or if a player is approaching a station with our PIDS in sight, everything within the curly bracket of the create function will be run.
The render
function is called every frame (So 60fps = 60 times per second)*. Notice the asterisk? Because running scripts simply takes time, even if a short amount of time. JCM will try to, whenever possible, call your function every frame. However if there are too many scripts or your script is slow, then it may not be called every frame.
The dispose
function is called when a PIDS Preset is switched away from, or the PIDS is no longer in sight for the player. This can be used to do clean-up work in complex PIDS that stores texture etc.
The most useful one we are going to use is the render
function, since this is where we can obtain up-to-date information and dynamically render our PIDS.
But let's not get ahead of ourselves, and instead start from something very simple: A hello world script.
Hello, World!
First, let's insert print("Hello World ^^");
to the brackets within the create
function. This calls the print
function with the argument "Hello World ^^"
. The print
function will output the message in our Minecraft console.
Now, let's insert print("Goodbye World ^^;");
within the dispose
function as well.
After that, we can press F3+T to reload our resource pack and look at your game console!
You should the following message:
[Scripting] Hello World ^^
[Scripting] Hello World ^^
As PIDS in JCM (as well as MTR) are constructed in a way where each "side" is drawn separately, a normally placed PIDS will would call the function 2 times. This is normal behavior, you didn't do anything wrong!
Now, try going very far away from the PIDS, to the point where you can visually no longer sees them. (More technically, when the PIDS chunk is no longer loaded). You will notice the following 2 messages shows up in the console:
[Scripting] Goodbye World ^^;
[Scripting] Goodbye World ^^;
There you have it, a very simple Hello World script running in JCM.
(p.s. In case you got lost, run /tp 0 -60 0
).
So this concludes the Scripted PIDS tutorial, I hope that...
No where are you going!
ok I am sorry :<
Now let's actually draw the "Hello World" text onto the PIDS, because printing to the game console isn't considered exciting for most players apparently.
We can insert the following code to the render
function to draw a text with the content Hello World:
function render(ctx, state, pids) { Text.create().text("Hello World").draw(ctx); }
If this looks confusing, we can also split it line by line in the code:
function render(ctx, state, pids) { Text.create() .text("Hello World") .draw(ctx); }
This creates a text, setting its text content to Hello World, and then drawing it to ctx
(Which is a provided parameter in the render function, more on that later).
A statement is only considered finish by ending with a semicolon (;
), so our code will still work after breaking down to different lines. For readability purposes, opening new line is generally preferred.
Anyway now let's save the file, reload with F3+T and see what happens!
Ok our text do actually get rendered, the only problem is that the default text color is black, and as you might have realized, black-on-black may not necessarily be a very visible color combination. So let's change our text to be rendered with the white color instead.
The hex color code for a solid white color is FFFFFF
, so we can add the following line to our text:
.color(0xFFFFFF)
(0x
is used to depict that the number is a hexadecimal number)
So our text block should look something like this:
Text.create() .text("Hello World") .color(0xFFFFFF) .draw(ctx);
Note that .draw()
is the final command and you cannot append anything afterwards, so any setting must be appended before .draw()
.
Now reload our resource pack again and we now see our text!
With this logic, we can append a second Text block to render a 2nd text as well:
function render(ctx, state, pids) { Text.create() .text("Hello World") .color(0xFFFFFF) .draw(ctx); Text.create() .text("Joban Client Mod v2!") .color(0xFFFFFF) .draw(ctx); }
But it's hard to see, let's move each of them apart. We can use the .pos(x, y)
command to set the position of an element.
By default, a text have a height of 9
, so we can append .pos(0, 9)
to set the text position to 9 unit downwards:
function render(ctx, state, pids) { Text.create() .text("Hello World") .color(0xFFFFFF) .draw(ctx); Text.create() .text("Joban Client Mod v2!") .color(0xFFFFFF) .pos(0, 9) // <---- .draw(ctx); }
Rendering Arrivals
During the tutorial, you might have noticed the pids
object in the create/render/dispose function. This represents our PIDS object, where we can obtain arrivals and more from there.
We can get a list of arrivals with pids.arrivals()
, and then obtain the nth arrival info with the .get(n)
function.
So to obtain the first arrival entry, we can do so like this:
let firstRowArrival = pids.arrivals().get(0);
You can obtain a variety of information in the arrival entry, which includes it's route/LRT number, it's arrival and departure time, the route it's running and more. You can refer to the documentation [TODO] for further information.
Here however, we are only interested in getting the destination (i.e. Where the train is going), so we can do that with the destination()
function.
let firstRowArrival = pids.arrivals().get(0); let firstRowDestination = firstRowArrival.destination();
And now we can draw this to our text, by replacing the .text() content with firstRowDestination:
function render(ctx, state, pids) { let firstRowArrival = pids.arrivals().get(0); let firstRowDestination = firstRowArrival.destination(); Text.create() .text(firstRowDestination) // <--- Here .color(0xFFFFFF) .pos(0, 0) .draw(ctx); Text.create() .text("Joban Client Mod v2!") .color(0xFFFFFF) .pos(0, 9) .draw(ctx); }
Or a one-liner version, without setting variable:
function render(ctx, state, pids) { Text.create() .text(pids.arrivals().get(0).destination()) // <--- Here .color(0xFFFFFF) .pos(0, 0) .draw(ctx); Text.create() .text("Joban Client Mod v2!") .color(0xFFFFFF) .pos(0, 9) .draw(ctx); }
However before we continue, there is a few problem with this:
- An arrival is not guaranteed to exist. For example if the client is still fetching the arrival info, or there are no arrival on that platform. This would cause the script to error out (Which you may have spotted in the console, but JCM will try to re-run the script every 4 second)
- Imagine a script filled with
Text.create()
, how would you be able to easily tell which text is which?
For the latter, you could add JS comments with //
. However JCM also reserved a slot for comment, and that is within the Text.create(string)
function. So as an example, you can do the following:
Text.create("1st row destination") // <---- .text(firstRowDestination) .color(0xFFFFFF) .pos(0, 0) .draw(ctx);
The 1st row destination text is purely cosmetic and you can write any string within that, so this could be served as a comment. However whether this is preferable depends on your personal preferences. Now for the former arrival issue, we can add a null check to ensure that the arrival exists first:
function render(ctx, state, pids) { let firstRowArrival = pids.arrivals().get(0); if(firstRowArrival != null) { // <-- Check if arrival exists first Text.create("1st row destination") .text(firstRowArrival.destination()) .color(0xFFFFFF) .pos(0, 0) .draw(ctx); } ... }
Now this might be fine for rendering 1 arrival, but imagine we have to display 4 arrivals, duplicating this code 4 times is cumbersome. Instead we can change this to a for-loop:
function render(ctx, state, pids) {
for(let i = 0; i < 4; i++) { // Set i to 0. i+1 if i < 4, otherwise don't run this anymore let arrival = pids.arrivals().get(i); // <---- Obtain nth row arrival if(arrival != null) { Text.create("Arrival destination") .text(arrival.destination()) .color(0xFFFFFF) .pos(0, i*9) // <--- nth row * text height .draw(ctx); } }}
The runs everything within the for loop 4 times, setting the i
variable to the nth time our code is executed.
As such, we can use i
to determine our Y position as well as which arrival to obtain.
Text size & Language Cycle
To resize a text, we can use the .scale(factor)
function to scale the text to be larger or smaller.
In our case, we will scale it by 1.25x:
Text.create("Arrival destination") .text(arrival.destination()) .color(0xFFFFFF) .pos(0, i*9) .scale(1.25) // <---- Scale the text by 1.25x .draw(ctx);
The text does seems to be larger! But they are now too close to each other, and are really uncomfortable to look at.
This is because earlier, we set the position of the text to be .pos(0, i*9)
. However since our text is scaled, the text height is no longer 9
, but instead 9*1.25 (our scale)
= 11.25.
While we could just change it to 11.25
, let's leave even more padding between each row for a better-look. Let's say each row should go 16.75 unit downwards:
Text.create("Arrival destination") .text(arrival.destination()) .color(0xFFFFFF) .pos(0, i*16.75) // <---- .scale(1.25) .draw(ctx);
Much better! The last thing we have to deal with is handling different languages. Currently it's just displayed with everything including the pipe character.
To cycle the string, we can wrap our destination string with the ctx.cycleString(str)
function.
Text.create("Arrival destination") .text(ctx.cycleString(arrival.destination())) // <--- .color(0xFFFFFF) .pos(0, i*16.75) .scale(1.25) .draw(ctx);
And there we go, now the text cycles!
So to re-cap, our code should look similar to this:
function create(ctx, state, pids) { print("Hello World ^^"); } function render(ctx, state, pids) { for(let i = 0; i < 4; i++) { let arrival = pids.arrivals().get(i); if(arrival != null) { Text.create("Arrival destination") .text(ctx.cycleString(arrival.destination())) .color(0xFFFFFF) .pos(0, i*16.75) .scale(1.25) .draw(ctx); } } } function dispose(ctx, state, pids) { print("Goodbye World ^^;"); }
v1: Light Rail, Light Rail Everywhere!
[TODO v1]
v2: Something is missing
[TODO v2 plat number]
v3: It's still missing
[TODO v3 header bar]
v4: Go ham!
[TODO v4 overflow adjustment]