Skip to main content
Version: 0.4.0 Nightly

HTML textures

Note: This API is still under development, so breaking changes may occur at any time.

neoOMSI can display an HTML page on a vehicle or scenery texture. This lets content creators build screens such as an IBIS display or dashboard using HTML, CSS, and JavaScript.

This documentation explains how to use the API in HTML. If anything is missing or you encounter any issues, please feel free to open an issue with relevant information and steps to reproduce the problem.

Start with the complete dashboard example. It uses the same vehicle API documented below and is included as a runnable fixture in the engine source repository.

Example model config:​

[htmltexture]
1024
512
html\index.html

##########################################

[mesh]
generic\AFR4\AFR4_Display.o3d

[visible]
AFR4_Display
1

[matl]
afr4.dds
0

[useHtmlTexture]
0

[matl_lightmap]
1px_lm.bmp
0.5

[matl_freetex]
afr4.dds
AFR4_ST_texture

[matl_transmap]
\S:7

[viewpoint]
3

[useHtmlTexture] describes which [htmlTexture] it is; \S:7 is the corresponding script texture ID in this case. Note: An [htmlTexture] is registered as a [scriptTexture], but it is also registered as an [htmlTexture].

This means: If an htmlTexture follows scriptTexture 6, the htmlTexture is automatically scriptTexture 7. But it is not htmlTexture 7.

Html file​

html\index.html is the relative path from the bus path. Example: Data\Vehicles\MAN_NewLionsCity\html\index.html = html\index.html

The basic idea​

Your page is normal HTML with a <script>. The game gives it one global object, window.omsi (you can also just write omsi), and the page uses it for three things:

  1. Read what the bus is doing: speed, doors, the current line, the next stop.
  2. Write back: set a script variable, press a trigger, choose a route.
  3. React to touches and clicks on the texture.

The smallest useful page:

<div id="speed">--</div>
<script>
window.omsi = window.omsi || {};
window.omsi.update = function (d) {
document.getElementById('speed').textContent = d.vehicle.motion.speedKmh + ' km/h';
};
</script>

The game calls omsi.update whenever something the page could care about has changed. You don't poll. You wait for update, look at the data and redraw what you need.

Reading the vehicle​

window.omsi.update = function (d) {
d.num, d.str // script variables that changed since the last call (everything on the first call)
d.vehicle // the normalised snapshot, the same object as omsi.vehicle
d.vars // every variable so far, the same object as omsi.vars
};
omsi.vehicleThe normalised state of the bus, with the same names on every bus (see below). You can also read it from timers.
omsi.vars.num, omsi.vars.strThe latest value of every variable in the bus's own variable list, keyed by lower-case name.
omsi.getVar(name)One variable, any letter case. Gives a number, else text, else undefined.
omsi.apiVersion1

Why two ways? OMSI buses name their variables however the author likes (door_0, Fahrertuer_Rechts, elec_busbar_main ...). If your page wanted to know whether the engine runs, it would have to know every single bus. omsi.vehicle solves that: the game translates the variables into one fixed layout, using the same rules it uses itself. Things a bus doesn't have show up as null (numbers) or false (switches), never as a missing property. So omsi.vehicle.engine.rpm never throws, even on a bus without an engine model.

Numbers are rounded (speed to 0.1, rpm to 1 and so on). That way a value that only wobbles in the last digit doesn't trigger a redraw every frame.

Things that are specific to one bus (a mod's own switch, a battery voltage) are not standardised by OMSI. Read those with omsi.getVar("their_variable_name").

Time, date, locale​

omsi.time.hour, .minute, .secondSimulation clock, as numbers
omsi.time.asStringHH:MM:SS
omsi.timestampNow as a timestamp: seconds since 1970-01-01 on the simulation's calendar, no time zone
omsi.date.day, .month, .yearSimulation date, as numbers
omsi.date.asStringDD.MM.YYYY, or MM/DD/YYYY when locale is en
omsi.localeInterface language as an ISO 639-1 code (en, de ...)

These are set before omsi.update runs and change with the simulation clock, once per second. Use omsi.locale to pick your texts, and fall back to English for languages you didn't write.

omsi.vehicle (version 1)​

pathmeaning
info.number, .ident, .yard, .route, .nextStoptext variables of the bus
motion.speedKmh, .heading, .pitch, .bank, .steeringDeg, .x, .y, .z, .odometerKmmovement and position
engine.running, .rpm, .throttle, .brake, .clutch, .gear, .tankContentdrive train and pedals
electrics.on, .busbarMain, .busbarAvailable, .failureon-board network
battery.onbattery switch, null when the bus has none
doors.count, .anyOpennumber of door leaves (door_0, door_1 ...) and whether any is open
doors.list[i].number, .open (0..1), .isOpenlist[0] is door 1
passengers.onboardpeople aboard
passengers.entries[i], .exits[i]number, open, requested (PAX_Entry/Exit<n>_Open/_Req)
lights.headlights0 off, 1 parking, 2 dipped, 3 main beam
lights.brake, .reverse, .fog, .interiorlamps (interior is 0..1)
lights.indicator0 off, 1 left, 2 right, 3 hazard. .indicatorLeft and .indicatorRight are the lamps, .hazard the switch
brakes.parking, .stop, .kneelingswitches
wipers.runningwindscreen wipers
cabin.temperaturecabin air in °C
condition.dirt, .crashes, .lastImpactKJ, .streetConditionwear and road
train.trailerscoupled vehicles behind this one
route.*line, stops, delay: see the next section

doors.anyOpen follows what the passengers are told is open (PAX_*_Open) if the bus has those variables, and the door leaves (door_<n>) if not. Some mod buses use door_<n> for other things, so don't be surprised if a strange bus behaves oddly here.

The route and the timetable​

Everything about the current trip lives in omsi.vehicle.route.

pathmeaning
route.activetrue when the bus has a timetable (a duty)
route.sourcewhere the stops come from: "timetable", "ibis" or "none"
route.linethe line text
route.destinationthe text on the destination sign
route.stops[i]one stop: index, name, arrival, departure ("HH:MM"), arrivalSec, departureSec (seconds after midnight), served, current
route.currentthe stop the bus is at or heading for (same fields as a stop)
route.terminusthe last stop (same fields)
route.nextIndexindex of the next stop. null when active is false
route.delaySecdelay in seconds. Negative means early
route.ibis.line, .suffix, .routeIndex, .terminusIndex, .terminusCodethe raw numbers the IBIS itself holds

A few things worth knowing:

  • served is true once a stop has been handled. When the last stop of a trip is served, the trip is over. A page can use that to start the return trip.
  • route.stops can be empty. If the bus has no timetable and nobody typed a route into the IBIS, there is simply nothing to show. Check stops.length before you draw.
  • Without a duty (active is false) the game doesn't advance the stops on its own. A page that wants to show progress then has to keep its own counter.

Departures of a stop​

omsi.getDepartures(stop) gives the departures at a bus stop for the next 2 hours, soonest first, at most 20. stop is the stop's name as in the timetable (Busstops.cfg), any letter case. Works on vehicle and scenery pages.

omsi.update = function () {
var list = omsi.getDepartures('Hauptbahnhof');
for (var i = 0; i < list.length; i++) {
var minutes = Math.round((list[i].time - omsi.timestamp) / 60);
console.log(list[i].line + ' ' + list[i].destination + ' in ' + minutes + ' min');
}
};
fieldmeaning
linethe line text
destinationthe destination text
timedeparture as a timestamp, same scale as omsi.timestamp, delay included
  • The first call for a stop returns []. The game fills the stop a moment later and then calls omsi.update again, so ask inside update.
  • The game keeps the 8 stops a page asked for most recently. Asking for a ninth pushes out the one asked longest ago; asking for that one again later gives [] for a moment until it is made anew. The list is renewed about once a second.
  • Without a timetable on the map the list stays empty.

Acting on the vehicle​

omsi.setVar(name, value)Write a script variable.
omsi.trigger(name)Press a trigger of the bus's scripts, like a button in the cab does.
omsi.setRoute(index)Type route omsi.depot.routes[index] into the IBIS: line, route and destination, the way a driver would.
omsi.setLine(text)Set a line by its text. The game takes the first route of that line in the depot file. It matches the whole text (any letter case) or the leading digits.
omsi.setDestination(index)Only change the destination sign, using omsi.depot.destinations[index]. The line and route stay as they are.
omsi.clearLine()Clear the IBIS: no line, no route. Use it before a route is chosen, or to throw the current one away.
omsi.setNextStop(index)With a timetable: jump to stop index of the trip, forwards or backwards. Skipped stops are not served; going back makes them due again.

Two details that trip people up:

  • setNextStop goes both ways. Forwards, the stops in between are skipped. Backwards (a "back one stop" button), the stops from index on are due again and served is false for them. A request for the stop the bus is already heading for changes nothing. The game applies it on its next tick, so route.nextIndex follows a moment later.
  • clearLine() only touches the IBIS. It does not end the duty in the game. If the page reads route.active afterwards it may still be true, so don't switch screens based on that alone.

Calls like these are handed to the game, which applies them a moment later. They don't take effect inside the same script run. Read omsi.vehicle again in the next update.

The depot file: omsi.depot​

omsi.depot describes what the bus's depot file (.hof) offers, so your page can build a line picker or destination list without hard-coding anything. It is filled once, before the first update. If the bus has no depot file it stays empty.

omsi.depot.routes[i]

fieldmeaning
indexposition in the list, the number you give to setRoute
codethe IBIS route code (line × 100 + route number, by the depot file's own rule)
namename of the trip
linethe line text
terminusCodethe destination code of the route
destinationIndexindex in destinations, or -1 if there is none
destinationthe text of that destination sign
first, lastname of the first and last stop
stopsnames of all stops, in order

Many depot files leave the line column as a placeholder like XXX. When that happens the game works the line out from the route code instead (code 1203 becomes line 12).

omsi.depot.lines[] groups the same route objects by line: each entry has line and its routes, in the order of the depot file.

omsi.depot.destinations[i] has index, code, id and name.

A small example, a line picker that starts the first route it finds:

function pickLine(text) {
var routes = omsi.depot.routes, i;
for (i = 0; i < routes.length; i++) {
if (routes[i].line === text) {
omsi.setRoute(routes[i].index);
return;
}
}
}

A return trip is usually a route on the same line whose first is your last and whose last is your first. Look for that pair and call setRoute with it.

Touch and mouse​

The app passes presses, releases and movements of the pointer on the texture to the page. They turn into events on the element under the pointer and bubble up through its parents:

  • pointerdown, pointerup, mousedown, mouseup, click, mousemove

You can listen in three ways:

<button onclick="doSomething()">Press</button>
el.onclick = function (event) { ...
};
el.addEventListener('click', function (event) { ...
});

The event object has type, x, y (in texture pixels), target, stopPropagation() and preventDefault(). A click needs both the press and the release on the page.

There is no keyboard input, no hover and no drag and drop. Build keypads and lists from buttons or boxes and react to click.

Scenery objects​

A scenery object (.sco) can show pages too: [htmltexture] in its model config and [useHtmlTexture] on the material, the same as in a vehicle. The page is looked up in the model folder, then in the folder of the .sco.

A scenery page only gets the basic API: omsi.setVar, omsi.trigger, omsi.getVar, omsi.vars, omsi.time, omsi.date and omsi.locale. omsi.vehicle, omsi.depot and setRoute, setLine, setDestination, clearLine and setNextStop do not exist there (d.vehicle in update neither). The variables are those of the object's script; an object without a script still shows its page.

The pages run only within 60 m of the camera. Clicks reach them from up to 4 m away, unless the bus is in front of the page.

What the HTML engine supports​

The engine is deliberately small. It is not a browser, so a page that looks fine in Chrome may need a few changes. When something doesn't show up, check this list first.

HTML​

  • Nested elements, <style>, <script>, inline style="", id, class, entities such as &amp;.
  • Block elements: div, p, h1, h2, h3 (with browser-like default sizes and margins).
  • Inline elements: span, b, strong, i, em, a, small, u, label, code, and <br>.
  • <button> comes with a default look (grey, rounded, padded). Boxes with display:inline-block are laid out in rows that wrap, which is what you want for keypads and lists. A box without a width is as wide as its content.
  • <img src width height> (bmp, dds, tga, jpg, png). The file is looked up next to the page (see htmlengine/images.rs). img.src can be changed from a script. Pictures are cached, a missing file is cached too.
  • <head>, <title>, <meta>, <link> are read and then ignored.

Not available: canvas, tables, lists (ul/li), forms and input fields, iframes, video, svg.

CSS​

  • Selectors: tag, #id, .class, *, combinations like div.a#b, descendant chains (.panel span) and comma lists.
  • Properties: color, background / background-color, background-image: url(), background-size, background-repeat, background-position, font-size, font-weight, text-align, line-height, margin (and -top/-right/-bottom/-left), padding (same), width, height, display (none, inline, inline-block), visibility, border-radius.
  • Units: px, %, em, rem, pt, vw, vh.
  • Layout is plain block flow with wrapped inline text.

Not available: flexbox, grid, floats, position, borders, gradients, shadows, opacity, overflow, animations and transitions, custom fonts. Text is drawn in Roboto.

Since there is no overflow, a box doesn't clip its content. If a text is too long for its box, shorten it yourself.

JavaScript​

The language is ES5 plus arrow functions.

  • Language: var, let, const, functions, if, for, while, objects, arrays.
  • Globals: Math (round, floor, ceil, abs, min, max, sqrt, pow, trunc, sin, cos, PI), parseInt, parseFloat, String, Number, isNaN, NaN, Infinity, Object.keys, console.log/warn/error.
  • Strings: split, replace, trim, indexOf, charAt, slice, substring, startsWith, endsWith, includes, toUpperCase, toLowerCase, padStart, padEnd, repeat. Numbers: toFixed, toString.
  • Arrays: push, pop, shift, slice, join, indexOf, includes, map, filter, forEach.
  • Timers: setTimeout, setInterval, clearTimeout, clearInterval. They run while the page is being drawn and use real time, not simulation time.
  • DOM: document.getElementById, document.querySelector, document.body, createElement, appendChild, removeChild, remove, parentNode, getAttribute, setAttribute, classList (add, remove, toggle, contains), textContent, innerText, innerHTML (with markup), className, id, and element.style.*.

Not available: Date (read the time from omsi.time instead), JSON, fetch and any network access, localStorage, Promise/async, classes, new, regular expressions, and the statements switch, do ... while and try/catch. Use if/else chains and while loops instead. typeof, break and continue do work.

At most 64 timers can be active at once. Further setTimeout/setInterval calls are ignored.

Two limits protect the game from runaway pages. A single run of your script may execute about 400,000 steps, and nesting (function calls, elements) is capped at 48 levels. Normal pages never get near either. An endless loop just gets cut off.

Redrawing​

A page is redrawn when the DOM or the styles change. The simplest pattern, and the one that works best in this engine, is to rebuild a block with innerHTML in your update function and only do that when the data actually changed (compare against the last values you saw). Redrawing on every call wastes time on a slow phone.

Tips for beginners​

  • Start from the dashboard example and change one thing at a time. If the texture goes blank, the last thing you touched is the culprit.
  • Write to the log. console.log(...) ends up in the game's log with the prefix htmltexture console:, which is the quickest way to see what omsi.vehicle or omsi.depot really contain on your bus.
  • Guard for missing data. A bus without a depot file has no omsi.depot.routes, a bus without a timetable has no stops. Check the length before you loop.
  • Test on the real bus. Each bus fills the variable list differently, and some values you expect will simply be null.
  • Keep buttons big. The texture is often small on screen and people press it with a finger or a mouse. 60 px high is a good minimum.