The District of Joban JCM:Building a Scripted PIDS Preset

JCM:Building a Scripted PIDS Preset

From The District of Joban
Revision as of 16:20, 3 November 2024 by LX86 (talk | contribs)

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.

Top: Default RV PIDS Preset, Bottom: Custom JS-based PIDS Preset

Getting started

To get started, [download](link here) the tutorial resource pack here and apply it to Minecraft. 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 actual JS script to be used for this preset.

Multiple JS files can be loaded for the same preset as well, however to keep things simple, we are simply going to use 1 single JS file 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 on 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 scripts.

Hello, World!

First, let's insert print("Hello World ^^"); to the brackets within the create function

And now, let's insert print("Goodbye World ^^;"); within the brackets of the dispose function.

Now F3+T and look at your game console!

You should the following message:

[JCM] [Scripting] Hello World ^^

[JCM] [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 also pops up:

[JCM] [Scripting] Goodbye World ^^;

[JCM] [Scripting] Goodbye World ^^;

There you have it, a simply Hello World script for the Scripted PIDS Preset.

(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 console isn't considered exciting for most players apparently!

We can insert the following code to the render function to draw a text with "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!

*checks date* It's not April Fools yet is it?

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 text on black background is not necessarily 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 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!

A white text in our PIDS saying "Hello World"

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);
}
I mean it technically did render twice...

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("Joban Client Mod v2!")
    .color(0xFFFFFF)
    .pos(0, 9)
    .draw(ctx);
}
(The reason the bottom line appears to be more "left" than the top line is due to Minecraft's imperfection when rendering TTF font, it will work fine if you switch to Minecraft font or with most other text combination)

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 shortened 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);
}
JCM JS PIDS Tutorial v0.5.png

However before we continue, there is a few problem with this:

  1. 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)
  2. 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, however JCM also reserved a slot for comment, and that is within the Text.create(comment) function. So for 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. For the former 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 use 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.

JCM JS PIDS Tutorial v0.6.png

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);
JCM JS PIDS Tutorial v0.7.png

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);
JCM JS PIDS Tutorial v0.8.png

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!

JCM JS PIDS Tutorial v0.10.png

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]

v5: It's coming together