Difference between revisions of "JCM:Building a Scripted PIDS Preset"
Views
Actions
Namespaces
Variants
Tools
Line 170: | Line 170: | ||
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. | 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 <code>pids.arrivals()</code>, and then obtain the n<sup>th</sup> arrival info with the <code>.get(n)</code> function. | |||
So to obtain the first arrival entry, we can do so like this: | |||
<pre> | |||
let firstRowArrival = pids.arrivals().get(0); | |||
</pre>You can obtain lots of information in the arrival entry, including 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 name (i.e. Where the train is going), so we can do that with the <code>destination()</code> function. | |||
<pre> | |||
let firstRowArrival = pids.arrivals().get(0); | |||
let firstRowDestination = firstRowArrival.destination(); | |||
</pre>And now we can draw this to our text, by replacing the .text() content with firstRowDestination: | |||
<pre> | |||
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); | |||
} | |||
</pre>Or a shortened version, without setting variable: | |||
<pre> | |||
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); | |||
} | |||
</pre> | |||
[[File:JCM JS PIDS Tutorial v0.5.png|none|thumb|368x368px]] | |||
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, however JCM also reserves a slot for comment, and that is within the <code>Text.create()</code> function. So for example you can do the following:<pre> | |||
Text.create("1st row destination") | |||
.text(firstRowDestination) | |||
.color(0xFFFFFF) | |||
.pos(0, 0) | |||
.draw(ctx); | |||
</pre>The "1st row destination" text is purely cosmetic and you can write any string within that, so this could also be served as a comment. However this depends on personal preferences. | |||
For the former, we can add a null check to ensure that the arrival exists first:<pre> | |||
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); | |||
} | |||
... | |||
} | |||
</pre>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> | |||
function render(ctx, state, pids) { | |||
for(let i = 0; i < 4; i++) { | |||
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); | |||
} | |||
} | |||
} | |||
</pre>The runs everything within the for loop 4 times, setting the <code>i</code> variable to how many time the code has been executed. | |||
[[File:JCM JS PIDS Tutorial v0.6.png|none|thumb|357x357px]] | |||
==== Text size & Language Cycle ==== | |||
To resize a text, we can use the .scale(f) function to scale the text to be larger or smaller. Here, we will scale it by 1.25x:<pre> | |||
Text.create("Arrival destination") | |||
.text(arrival.destination()) | |||
.color(0xFFFFFF) | |||
.pos(0, i*9) | |||
.scale(1.25) // <---- Scale the text by 1.25x | |||
.draw(ctx); | |||
</pre> | |||
[[File:JCM JS PIDS Tutorial v0.7.png|none|thumb|333x333px]] | |||
So the text size indeed got increased, but now they are 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 <code>.pos(0, i*9)</code>. However since our text is scaled, the text height is no longer <code>9</code>, but instead <code>9 * 1.25 (our scale)</code> = 11.25. | |||
But instead of doing that, let's leave even more padding between each row, let's say each row we are going to increase by 16.75:<pre> | |||
Text.create("Arrival destination") | |||
.text(arrival.destination()) | |||
.color(0xFFFFFF) | |||
.pos(0, i*16.75) // <---- | |||
.scale(1.25) | |||
.draw(ctx); | |||
</pre> | |||
[[File:JCM JS PIDS Tutorial v0.8.png|none|thumb|310x310px]] | |||
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 <code>ctx.cycleString(str)</code> function.<pre> | |||
Text.create("Arrival destination") | |||
.text(ctx.cycleString(arrival.destination())) <--- | |||
.color(0xFFFFFF) | |||
.pos(0, i*16.75) | |||
.scale(1.25) | |||
.draw(ctx); | |||
</pre>And there we go, now the text cycles! | |||
[[File:JCM JS PIDS Tutorial v0.9.png|none|thumb|307x307px|[[File:JCM JS PIDS Tutorial v0.10.png|none|thumb|306x306px]]]] | |||
So to re-cap, our code should look similar to this:<pre> | |||
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 ^^;"); | |||
} | |||
</pre> | |||
=== v1: Light Rail, Light Rail Everywhere! === | === v1: Light Rail, Light Rail Everywhere! === | ||
[TODO v1] | [TODO v1] |
Revision as of 16:02, 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.
Getting started
To get started, [download](link here) the tutorial resource pack here and apply it to Minecraft. This resource pack 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
which is a JSON array that points to the actual JS script to load for this preset. Multiple JS files can be loaded for the same preset, however to keep things simple, we are simply going to use 1 single JS file, 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.
In this tutorial, the most useful one is going to be 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, you can 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
method to draw a text with "Hello World":
Text.create().text("Hello World").draw(ctx);
If this looks confusing, we can also split it line by line in the code:
Text.create() .text("Hello World") .draw(ctx);
This creates a text, setting the text content to "Hello World", and then drawing it to the 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.
So now your render function code should look something like this:
function render(ctx, state, pids) { Text.create() .text("Hello World") .draw(ctx); }
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 text on black background is not necessarily a very visible color combination.
Let's change our text to be rendered with the white color so we can see!
The hex color code for a solid white color is "FFFFFF", so we can add the following line to our text, before .draw()
:
.color(0xFFFFFF)
(0x
is used to depict that the number is a hexadecimal number)
So our text code 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 2 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("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 lots of information in the arrival entry, including 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 name (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); }
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, however JCM also reserves a slot for comment, and that is within the Text.create()
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 also be served as a comment. However this depends on personal preferences. For the former, 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); } ... }
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++) { 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 how many time the code has been executed.
Text size & Language Cycle
To resize a text, we can use the .scale(f) function to scale the text to be larger or smaller. Here, 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);
So the text size indeed got increased, but now they are 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.
But instead of doing that, let's leave even more padding between each row, let's say each row we are going to increase by 16.75:
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]