The District of Joban Difference between revisions of "JCM:Building a Scripted PIDS Preset"

Difference between revisions of "JCM:Building a Scripted PIDS Preset"

From The District of Joban
m (Cleanup & fixes)
Line 5: Line 5:


=== Getting started ===
=== 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.
To get started, [https://www.joban.org/archive/misc/JCM PIDS Tutorial Scripting Pack.zip 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 ====
==== joban_custom_resources.json ====
Line 21: Line 23:
</pre>
</pre>


This is mostly the same from a JSON PIDS Preset, except the <code>scripts</code> property which is a JSON array that points to the actual JS script to be used for this preset.
This is mostly the same from a JSON PIDS Preset, except the <code>scripts</code> property which is a JSON array that points to the scripts 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: <code>scripts/pids_tut.js</code>.
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: <code>scripts/pids_tut.js</code>.


==== scripts/pids_tut.js ====
==== scripts/pids_tut.js ====
Line 42: Line 44:
</pre>
</pre>


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.
Our custom logic can be placed between each curly brackets for each of the function. As seen, there are a total of 3 functions: <code>create</code>, <code>render</code>, <code>dispose</code>.


The <code>create</code> 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 <code>create</code> 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.
Line 49: Line 51:


The <code>dispose</code> 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 <code>dispose</code> 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 <code>render</code> function, since this is where we can obtain up-to-date information and dynamically render our PIDS.
The most useful one we are going to use is the <code>render</code> 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.
But let's not get ahead of ourselves, and instead start from something very simple: ''A hello world script''.


==== Hello, World! ====
==== Hello, World! ====
First, let's insert <code>print("Hello World ^^");</code> to the brackets within the <code>create</code> function
First, let's insert <code>print("Hello World ^^");</code> to the brackets within the <code>create</code> function. This calls the <code>print</code> function with the argument <code>"Hello World ^^"</code>. The <code>print</code> function will output the message in our Minecraft console.


And now, let's insert <code>print("Goodbye World ^^;");</code> within the brackets of the <code>dispose</code> function.
Now, let's insert <code>print("Goodbye World ^^;");</code> within the <code>dispose</code> function as well.


Now F3+T and look at your game console!
After that, we can press F3+T to reload our resource pack and look at your game console!


You should the following message:
You should the following message:
Line 71: Line 71:
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!
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:
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:


<code>[JCM] [Scripting] Goodbye World ^^;</code>
<code>[JCM] [Scripting] Goodbye World ^^;</code>
Line 77: Line 77:
<code>[JCM] [Scripting] Goodbye World ^^;</code>
<code>[JCM] [Scripting] Goodbye World ^^;</code>


There you have it, a simply Hello World script for the Scripted PIDS Preset.
There you have it, a very simple Hello World script running in JCM.


''(In case you got lost, run'' <code>/tp 0 -60 0</code>'').''
''(p.s. In case you got lost, run'' <code>/tp 0 -60 0</code>'').''


So this concludes the Scripted PIDS tutorial, I hope that...
So this concludes the Scripted PIDS tutorial, I hope that...
Line 86: Line 86:
ok I am sorry :<
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!
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 <code>render</code> function to draw a text with "Hello World":
We can insert the following code to the <code>render</code> function to draw a text with the content '''Hello World''':


<pre>
<pre>
Line 108: Line 108:
This creates a text, setting its text content to '''Hello World''', and then drawing it to <code>ctx</code> (Which is a provided parameter in the render function, more on that later).
This creates a text, setting its text content to '''Hello World''', and then drawing it to <code>ctx</code> (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.
A statement is only considered finish by ending with a semicolon (<code>;</code>), 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!
Anyway now let's save the file, reload with F3+T and see what happens!
[[File:JCM PIDS Tutorial v0.1.png|thumb|371x371px|*checks date* It's not April Fools yet is it?|none]]
[[File:JCM PIDS Tutorial v0.1.png|thumb|371x371px|*checks date* It's not April Fools yet is it?|none]]


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.
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 <code>FFFFFF</code>, so we can add the following line to our text:
The hex color code for a solid white color is <code>FFFFFF</code>, so we can add the following line to our text:
Line 119: Line 119:
<code>.color(0xFFFFFF)</code> (<code>0x</code> is used to depict that the number is a hexadecimal number)
<code>.color(0xFFFFFF)</code> (<code>0x</code> is used to depict that the number is a hexadecimal number)


So our text should look something like this:
So our text block should look something like this:


<pre>
<pre>
Line 153: Line 153:
<pre>
<pre>
function render(ctx, state, pids) {
function render(ctx, state, pids) {
    Text.create()
    .text("Hello World")
    .color(0xFFFFFF)
    .draw(ctx);
     Text.create()
     Text.create()
     .text("Joban Client Mod v2!")
     .text("Joban Client Mod v2!")
     .color(0xFFFFFF)
     .color(0xFFFFFF)
     .pos(0, 9)
     .pos(0, 9) // <----
     .draw(ctx);
     .draw(ctx);
}
}
Line 197: Line 202:
     .draw(ctx);
     .draw(ctx);
}
}
</pre>Or a shortened version, without setting variable:
</pre>Or a one-liner version, without setting variable:


<pre>
<pre>
Line 220: Line 225:
# Imagine a script filled with <code>Text.create()</code>, how would you be able to easily tell which text is which?
# Imagine a script filled with <code>Text.create()</code>, 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 <code>Text.create(comment)</code> function. So for example you can do the following:<pre>
For the latter, you could add JS comments with <code>//</code>. However JCM also reserved a slot for comment, and that is within the <code>Text.create(string)</code> function. So as an example, you can do the following:<pre>
Text.create("1st row destination")
Text.create("1st row destination") // <----
.text(firstRowDestination)
.text(firstRowDestination)
.color(0xFFFFFF)
.color(0xFFFFFF)
.pos(0, 0)
.pos(0, 0)
.draw(ctx);
.draw(ctx);
</pre>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.
</pre>The '''<u>1st row destination</u>''' 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:<pre>
Now for the former arrival issue, we can add a null check to ensure that the arrival exists first:<pre>
function render(ctx, state, pids) {
function render(ctx, state, pids) {
     let firstRowArrival = pids.arrivals().get(0);
     let firstRowArrival = pids.arrivals().get(0);
Line 240: Line 245:
     ...
     ...
}
}
</pre>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:<pre>
</pre>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:<pre>
function render(ctx, state, pids) {
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
     for(let i = 0; i < 4; i++) { // Set i to 0. i+1 if i < 4, otherwise don't run this anymore

Revision as of 23:23, 3 November 2024

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 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:

[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 messages shows up in the console:

[JCM] [Scripting] Goodbye World ^^;

[JCM] [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!

*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-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!

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("Hello World")
    .color(0xFFFFFF)
    .draw(ctx);

    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 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);
}
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 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.

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