The online racing simulator
Searching in All forums
(312 results)
Bokujishin
S3 licensed
Looking back, I think the issue stems from how Godot InSim handles text in the various InSim packets (and, I believe, many other InSim libraries): basically, codepages are a pain to deal with, so we store everything as UTF8, and only present UTF8 to the user, for both input and output; because of this, Godot InSim gives access to the InSimMSOPacket.msg property as a UTF8-encoded string only, and using TextStart to offset this string (or the "UTF16" intermediate string) doesn't work in the scenarios I presented.

I believe you are most likely right that there is in fact no issue here, and I was just not using TextStart as intended. I will, however, keep calling this property "unreliable" in Godot InSim, because it actually is, as a result of the text handling. I ended up adding utility functions to retrieve a message's author, and the message's contents, based on the regex shown above, which is a workaround I'm okay with.
Bokujishin
S3 licensed
I have a semi-automatic (quarter-automatic?) solution to this, which I could improve a bit with InSim (for taking screenshots automatically).

The idea is as follows:
  • Create all "colors" (skins + car colors) you want to generate
  • Place the camera as needed (R or L in the garage, then V to get into the viewer, or change the camera directly in the viewer)
  • Take a first screenshot with a black background
  • Set the background to the lightest gray you can
  • Take a screenshot for every skin/color you want
  • Use imagemagick to create a mask and automatically remove the background for every car
I have a small Godot program that automates the imagemagick part, and adds some GUI elements to set car name and number, drivers, team, etc.
It uses a JSON file to set the data for each car, which could also be used by InSim to automate the taking of screenshots after the first one.

This can result in things like this:



But of course, since the background is removed by imagemagick, you can also just drop this into a stream overlay.
It would certainly help if we could get a player's colors via some InSim packet, so we don't have to ask them to send their colors or manually recreate them.

Also, one small caveat: the /colour command does not allow us to select one of the default colors (or at least, I don't know what parameter to pass for default colors), so we have to duplicate those first.
Bokujishin
S3 licensed
LFS being the single fastest racing sim to boot up (we're talking 5 seconds tops, it's barely over a second on my PC to get to the menu), I'm not sure this is really a problem Big grin (at least we don't have to sit through a dozen splash screens or various logos, then wait half a minute to get into the menu, and another minute or 2 to go through the menus and load the track + cars).

Now, it's true that credits could be made available from the main menu, but you can just launch LFS again. Also, the credits are not very long and are looping. And finally, it may not be a bug at all that /entry doesn't work there, as it's likely just an exit path in the code: disable all command processing, show credits, exit on input.
Improve pit speed limiter behavior
Bokujishin
S3 licensed
The current behavior of the pit speed limiter is a bit weird and unrealistic, as well as flawed in its speed tracking. I will list the issues I have noticed, what can be improved (some suggestions have already been made in the past), and also show a custom pit limiter I implemented with a PID controller and IS_AIC packets.

The general idea of the limiter is, of course, to avoid exceeding the speed limit. LFS quite clearly uses a PID controller for this, and a heavily damped one, probably to avoid the slight overshoot that a more reactive controller can exhibit. While I believe some real-life limiters do override the throttle application to track the speed limit, it's quite common to see ignition cut as the way to limit the speed. In LFS, this could easily be done by setting throttle to zero when reaching the limit.

Now, on to the list:
  • The limiter is overdamped, causing it to take some time to reach the actual speed limit.
  • It is too conservative, and often tracks at 78-79 km/h on flat surfaces.
  • It doesn't handle uphill properly, speed can drop to 76-77 km/h.
The way I see it, the PID controller uses a lot of Kd to avoid overshoot, and little to no Ki, causing the speed drop when going uphill. Either that or the actual target is wrong when going uphill, e.g. 80 km/h in 3D space which gets projected to the horizontal plane.

I played around with the AI control packets to make a custom pit speed limiter, here's a comparison video:



Key takeaways from this:
  • Using only a PID controller, we can definitely improve speed tracking without overshoot, with less damping, less conservative tracking, and better uphill handling.
  • The PID controller tracks the speed limit (with a 0.5 km/h margin in my implementation), and cuts to zero throttle if the target value is exceeded.
  • My implementation uses a high Ki value for better uphill management, and uses Kd to control the smoothness of the tracking, which can lead to pseudo-simulating ignition cut limiters when Kd is too high.
The main issue I have with my current implementation is that I have to rely on the velocity vector included in the IS_AII packet, while pit limiters would normally measure speed at the wheels (driven or not, depending on the system). I believe LFS already does that, since spinning the wheels with the limiter enabled does decrease throttle.

And of course, as has already been suggested (again recently), and is shown in the video, it would be really nice to be able to set a custom speed limit (since currently, only Rockingham has a different limit).

As a final disclaimer, I have not tried any car other than the Coil Cup, so results may vary. However, cutting throttle to zero when exceeding the speed limit should work for all vehicles, and could even help avoid the current behavior where shifting up can cause a car to exceed the limit temporarily.

EDIT: A few additional notes:
  • Performance is further improved by clamping the derivative term to be negative only, so we don't add excessive throttle when resuming from an "ignition cut". I also tried my implementation with a different car, the McLata GT3 (303CB1), as I know it tends to overspeed when shifting to 2nd gear - no change here, the car jumps from 79.5 to ~81.5 from shifting up due to engine inertia alone, so it would be up to the driver not to overspeed. It did however improve the behavior as the speed reaches the limit (less risk of overspeed, contained within the 0.5 km/h margin).
  • Yes, I know that the PID I show here is technically very poorly tuned, as it is very twitchy; that is however exactly what I'm going for. "Proper" tuning is in the order of 10x lower values, much less Ki, and Kd used for "better" damping, but then I get results closer to what LFS currently does.
Last edited by Bokujishin, .
Bokujishin
S3 licensed
I'm finally ready to release the first public version of GIS Hub! It obviously has a lot of room to grow and improve, but this should be quite usable already (it has already seen some real use over the summer for a couple races, and that was still much earlier in the development).

If you're interested, you can head over to GitLab to get the binaries and a few example modules. As a reminder, if you want to develop modules for GIS Hub, you will need the Godot editor and a version of Godot InSim that is compatible with your version of GIS Hub (3.0.1 at the time of this writing).

Don't forget to read the built-in documentation, and you can also have a look at the dedicated documentation website.
Bokujishin
S3 licensed
Here's a selection of additional skins, as color variations of the default ones. Some of them are based on actual color variations, others are made up.
Bokujishin
S3 licensed
And here are the completed default skins, I will soon push an update for the mod including them.

I would like to extend a big thank you to the GPA Racing team for allowing me to use their actual name and logo in the livery!
Bokujishin
S3 licensed
I just released v3.0, based on Godot 4.5 (I believe it should mostly still work with Godot 4.4, but LYTObject now uses a class exposed in Godot 4.5 for proper raycasting to the ground).

You can find the release notes, source code, and demos on the GitLab release page.

I will now try to work toward releasing GIS Hub as Soon™ as possible so people who are interested can give it a try.
Bokujishin
S3 licensed
I've been optimizing the geometry a bit (managed to save about 3k triangles with mostly minimal visual changes), which will help when I find the time to finish the interior, so I can finally remove the WIP status. The skin template was modified slightly to accommodate the changes, of note being a better layout for the rear wing, which now avoids overlaps and allows painting its front and back edges more easily; the plastic triangles between the front bumper blade and DRL covers can also be painted now, the lifting blocks can be painted separately, and the rest is just a few changes from the geometry optimization, which should not require changes unless you were closely following the geometry (if you made skins that only fill the alpha mask, make sure to check the rear corners, where the transition between rear view and side views has changed).

In the meantime though, I started working on default skins that are more interesting than plain colors with the same stickers every time. I have 7 of them, inspired by real Clio Cup liveries, mostly done (paint jobs and some of the common overlays are done, but most of them still need to get a proper logos and brands rework). I am planning to use all of the 9 default skins we can have for a mod, so I need one more (I'll keep the plain white one as well), and will most likely provide 2048x2048 skins in this thread or the mod page itself with color variations.


I will also officially introduce a "Cup" configuration, which will let you write a number on all 4 windows, using your license plate. In this configuration, the actual plate at the rear will read "Coil Cup Series" instead, and the numbers on the windows are made to fit 2 digits and nothing else.
Last edited by Bokujishin, .
Bokujishin
S3 licensed
I must say I didn't think of the possibility to just forward the data to a web client, and I guess you may want to avoid too much data processing there. In my live gaps implementation (not delta), I keep track of the last updated node for each car, and only update a node's timestamp when the node from the IS_MCI packet is different from the last updated one. For gaps between cars, and a decently smooth map animation, 250ms between packets is a decent interval, you can interpolate map positions from there using heading and speed (which is effectively a 2D version of the velocity vector you proposed), and I don't think there's much point in updating gaps in the standings more often than once or twice per second anyway.

If you want some telemetry data for all cars, running IS_MCI at 100Hz (with timestamps and pitch and roll included, instead of just heading) should be good enough (the additional angles would help showing cars rolling over too, if that's needed). You can also prune data from the stream of IS_MCI packets to only keep new nodes (as explained above).

For delta itself, my implementation uses OutSim (because it relies on the indexed distance, which I can recreate approximately from the PTH file (this requires creating an accurate PTH file for custom layouts), and delta only applies to the local car anyway, unless you want to display the delta for every car e.g. during qualifying), and I just compare the reference lap and current lap distances at the current timestamp offset by lap start timestamp, so we don't need nodes here. This assumes that you're driving close to your best time, though, since it can get pretty inaccurate otherwise, being distance-based.
Bokujishin
S3 licensed
I like the general idea of what you're proposing here, but I think there are some bits that can be removed for optimization:

The Cars struct doesn't really need UCID or Info (as long as that Info byte only contains the AI flag, at least), since those can easily be retrieved from the PLID.

Similarly in IS_GPS, Info, PLID, and SubT (what is it supposed to represent?) can be removed, which saves us 4 bytes with the spare Sp0. Although to be honest, I think this could also be achieved with an upgrade IS_MCI (or rather CompCar, which should include pitch and yaw in addition to heading).

Overall, I think your proposed IS_GPS, CarInfo, and Cars structs overlap a bit too much, and similar info could be retrieved by adding pitch and yaw to CompCar, as well as timestamps to IS_MCI. The tracking of timestamps at each node could probably be left to InSim developers (I did that myself, I know KingOfIce did too - not sure how exactly, but likely the same principle), but we would really need to be able to load custom PTH files via InSim for this to work in custom layouts.

And another hurdle is the lack of a signal for race start, which, while it helps prevent perfect starts via InSim, also prevents proper time tracking (even in hotlap mode, where we don't know when the finish line is crossed for the first time). Having a packet sent a few seconds after race start, and including the timestamp of the start, could be pretty nice for this.
Bokujishin
S3 licensed
I certainly hope that turning the headlights off during the night will disable the "physical" lights, not just the textures, otherwise things will look very weird. Your first question is interesting though, as it wouldn't be so surprising that lights are "optimized out" during day time; however, I think that would be a mistake in some situations, e.g. going through a tunnel/underpass (KY3, South City) or just into the shadows of buildings in South City in some lighting conditions. There are also more tunnel-like driveable areas in the updated Kyoto and South City.

Another concern I have regarding headlights is how far they can illuminate: I believe AI turns on the high/full beam at night, and well, that doesn't really look like high beam lights to me, but rather low beam. Regulations may vary for each country, but high beams should typically illuminate at least around 100m, while low beams are around 30m (at least that's what regulations say in France). In the videos, it looks like the lights barely reach 30m, let alone 100, which might make racing even more difficult than it should already be at night.

And on a related note: mods - I assume all vehicles will have either one or two "physical" lights, but will we be able to set their position (and orientation) manually? I imagine beam asymmetry is not too much of a concern and doesn't really need to be accounted for.

EDIT: To be fair, even from real life aerial views at the likes of Spa 24h or Nurburgring 24h, the visible illuminated spot on the road in front of the car doesn't reach that far either, but still a bit more than what we can see here. However, in the attached screenshot, the yellow car at the front doesn't appear to be illuminated by the white car's lights at all, when they look like they should be around 25m apart.
Last edited by Bokujishin, .
Bokujishin
S3 licensed
Quote from Scawen :I guess we'll find out in public testing. Smile

Graphics and physics are both more demanding than in the old version but they are split over two CPU cores instead of just one.

Yes we will... ready to test and report Big grin

Also good to see (at least from what I can see in the 2 videos) that AI seem to have a smoother transition into the pitlane. In the current public version, in some configurations, AI can swerve pretty heavily to get into the pitlane, or when exiting, they will brake and turn (e.g. BL1 into chicane). Or maybe it's just the updated tracks that have their blend lines reworked?

On the multithreading front, I was wondering: you say graphics and physics are split over 2 cores, but aren't you using more threads now for various things? Is the out-of-pit stutter mitigation only about spreading the loading over several frames, but not several threads? I was also wondering about splitting car physics over a few (physical) cores, say 16 cars per core or something.

But in any case, I just hope you (and Eric) are now closing in on the finishing touches leading to the test patch, so we can both enjoy it and hopefully have some useful feedback for you.
Bokujishin
S3 licensed
Hi there, here are a few punctuation issues (missing spaces) I found using regexes:

Question marks:
French.txt:3a_exitprog Quitter LFS?
French.txt:3a_endonlin Finir le jeu en ligne?
French.txt:3a_levonlin Quitter le jeu en ligne?
French.txt:3h_kickplyr Expulser le joueur %s^8? (%s^8)
French.txt:3a_deletset Supprimer le setup?
French.txt:3a_deletcol Supprimer les couleurs?
French.txt:3g_deletrep Supprimer le replay?
French.txt:3a_exitnosv Quitter sans sauvegarder?
French.txt:3a_delchnlq Supprimer le canal?
French.txt:3a_clrall_q Effacer tous les objets de la configuration?
French.txt:3g_delete_l Supprimer la disposition?
French.txt:3g_lockques Verrouiller Live for Speed?
French.txt:3a_overwrex Remplacer le fichier existant?
French.txt:3g_deleteex Supprimer le fichier existant?

Exclamation marks:
French.txt:3g_not_time Vous n'avez pas fini dans les temps!
French.txt:3b_wlcmxinr Bienvenue! Il y a %d pilotes dans le salon
help_French.txt:^3Bienvenue sur Live for Speed!^8

I looked for "?", "!", ":", and ";", so those should be all occurrences (there are some for ":", but they're not used as punctuation, so those are fine as is).
Improve ease of finding info in InSim documentation
Bokujishin
S3 licensed
Anyone who has read through the InSim.txt doc, either to make an InSim library or create an InSim app from scratch, knows that parts of the documentation are a bit difficult to parse, because there are many mentions of "see below", but how far below we need to look is not always obvious.

Using a regex search, I found 50 occurrences of "see below", and I believe 14 of them could use some rewording, or directly reference the corresponding enums. The rest are ok as the corresponding info is almost always directly following the "see below" mention.
  • struct IS_SFP: "Other states must be set by using keypresses or messages (see below) → this one should reference TEXT MESSAGES AND KEY PRESSES
  • struct IS_RST: "race flags (must pit, can reset, etc - see below) → biggest offender, you have to scroll 700 lines to find the corresponding info; this should reference the HOSTF_ enum
  • struct IS_CNL: leave reason → should mention LEAVR_ enum
  • struct IS_LAP, IS_SPX, and IS_PIT: penalty → should mention PENALTY_ enum
  • struct IS_PLA: Fact → should mention PITLANE_ enum
  • struct IS_CCH: Camera → should mention VIEW_ enum
  • struct IS_PEN: OldPen, NewPen, and Reason → should mention PENALTY_ and PENR_ enums
  • struct IS_PFL: Flags → should mention PIF_ enum
  • struct IS_FIN and IS_RES: Confirm, Flags → should mention CONF_ and PIF_ enums
  • struct IS_UCO: UCOAction → no description, should add "see below" and/or mention UCO_ enum
  • BUTTONS: "You can also make buttons visible in all screens - see below" → this should probably mention the Inst byte or reference INST_ALWAYS_ON, as the relevant info is buried in the middle of everything else (buttons are a complex subject with dense documentation).
More of a nitpick, but ALLOWED CARS also has 2 occurrences with SMALL_ALC reading "cars (see below)", with the IS_PLC struct in-between; the car list itself could use a "// Cars" title or something.

And a bonus one, may be intended: CAR LIGHTS uses the Switches word everywhere, like CAR SWITCHES - I was just wondering if it should be Lights instead, or if Switches was intended?
Bokujishin
S3 licensed
Had another read here after some conversation on Discord, and noticed I forgot to link to Scawen's mention of going for 16-byte objects one day: https://www.lfs.net/forum/post/2028120#post2028120

The additional data space could then be used for added precision for pitch for some concrete objects, maybe allow more colors for some objects, could also help store more variants of the letter signs (maybe even some symbols?), etc.
Bokujishin
S3 licensed
How open would you be to opening the development/maintenance of those "auxiliary" tools to the community? I'm not volunteering, but I'm sure some people would be interested. This would probably be best handled by giving access to a select few trusted people, and only disclosing relevant parts of the codebase.

While I'm honestly not certain about the Relay (but I do know it has a different handling of packet size Big grin), the REST API seems to be quite popular: I was told that's what is used to authenticate people on the LFS Discord server, and it's also very handy for fetching data about existing mods.

On the mods side, there are even a few things that I, and most likely the rest of the community (cruise server InSim devs in particular), would like to see:
  • the ability to fetch multiple mods per request: currently, we can get the mod list in a single request, but only a single mod with full details, which means we have to perform thousands of requests to update data for all mods; including the full details in the whole list, or fetching multiple skinIDs would help both ends.
  • there seem to be missing fields: for instance, we cannot get the Approved status of a mod from the API.
If Victor is not interested in maintaining those anymore, and to avoid distracting you even more from your own tasks, maybe it would be a good option to let some of us help in those areas?

Just for reference, I made a REST API demo for Godot InSim, fetching the mod list and then details about a select few mods - cruise server InSim devs are actually feeding entire vehicle databases from those requests.
Last edited by Bokujishin, .
Bokujishin
S3 licensed
Getting people to read is probably the biggest challenge in dev, or any business, really. Or not even in business: take any live stream of a race; even if the laps are displayed on the screen, you can be sure someone in the chat will ask how long the race is.

The main thing you can be sure people will read is API documentation, when they need to implement the API Big grin and even then, some parts may be skipped over, willingly or not...

But surely, if the test patch/next update has a flashing button for the unlock code, I expect people will notice it more easily.
Bokujishin
S3 licensed
Quote from cargame.nl :The original answer was longer and gave both 58 and 59 as its a matter of interpretation. 20 characters, 15, 10, even 5 is ridiculously long for the purpose which is a temporarily handshake, not even a password. But keep focusing on the things which do not matter at all in a discussion, this "community" will get "somewhere" 🫠

I don't know why you felt the need to mention my answer as "focusing on the things which do not matter": you provided a number of combinations (generated by AI, but that doesn't matter in the end), which was wrong, I just pointed that out...

Your follow-up with the updated version does give the right answer, okay (but we didn't really need the whole reasoning, 58^20 was ok from the beginning, the "result" the AI gave was just wrong), now back to the "things that matter": 20 characters is way too long, I guess? This is a code you're supposed to copy-paste anyway, how can it be a problem? Should Scawen make a system that generates 10 characters or less, so people who copy the code by hand have an easier time? Is it a sensible thing to do when inputting the unlock code is something you're supposed to do once and forget about it? People who need to regularly get a new code probably should either get the email on their PC to copy-paste, or share their phone's clipboard via wifi or any other means.
Bokujishin
S3 licensed
Yeah, first of all, why 59? 2*26 letters + 10 digits is 62, remove O, 0, I and l and you get 58.
Then the result itself is wrong: 59^20 is 2.612 403 355 × 10^35, while the AI basically said 7.573 809 985 × 10^33 for some reason...
and 58^20 = 1.855 922 647 × 10^35
Bokujishin
S3 licensed
Random guess here, but if we assume you made a mistake when copying the code manually, and it also didn't work with Ctrl+C, could you maybe have selected the space before the code?

If that's the case, I would suggest the unlock code email display the code on its own line, rather than after a colon and a space (although double-clicking should select the code without the surrounding spaces).
Bokujishin
S3 licensed
There's a pinned thread for this already: https://www.lfs.net/forum/thread/111761-Officially-licensed-vehicle-mods

The Ferat Vampire is missing from that list, though.
Bokujishin
S3 licensed
Just for reference, as a real example, here's a diagram for the Monza circuit (from 2019):
https://www.reddit.com/media?url=https%3A%2F%2Fi.redd.it%2Ftcn3vcy35nk31.jpg

You can spot those LED panels in the F1 pole lap video from last year:
https://www.youtube.com/watch?v=pr1kXumP3dk

From a very rough measurement, there's about 500m between panels 14 and 15 just before T11; on the other hand, panels 11 and 12 are only separated by about 150-180m.

This video also helps to spot the panels (2022):


Also, the LED panels don't actually stay on, they are either off or blinking (or displaying one of the other symbols/messages), every flag blinks at 2Hz, including the double yellows, which is the only flag using the diagonal/triangle shape.
Bokujishin
S3 licensed
Quote from Scawen :Does anyone have any good information about how frequently the LED boards should be placed around the track?

This is from the FIM rather than the FIA, but the wording is similar to what I've seen in a few videos on this matter. In section 9.2, the general idea is that LED panels delimit each marshalling sector (with one panel at the start of each sector), the maximum distance between panels is 250m, and each sector should have line of sight to the previous and next sectors.

For some more details about the panels themselves, I found this FIA document, with Appendix 2 showing the standard messages in addition to the flags (but solid colors and "double-yellows" should be enough, with the standard flashing frequency being 2Hz rather than 1Hz as I suggested before).

Edit: Oh and to keep up with my usual "let's ask for one more thing" habit: if/when you do add the ability to control the LED panels, having them available as layout objects would be great (they're much more visible than standard lights, especially since relatively small light sources are a common problem in games, where they're barely or not visible at all despite being extremely bright, because of their sub-pixel size or when they cover few pixels, like tail lights and stop lights do already, since I doubt the graphics update will solve this despite the reworked emissive materials).
Last edited by Bokujishin, .
IS_BTN rendering order is inconsistent, and the state of LFS text encoding
Bokujishin
S3 licensed
We all know that text encoding in LFS is a bit peculiar, as it relies on the very old CP1252 code pages system from the 90s. Scawen himself is aware of this, and has said a few times (even back then in ~2005) that Unicode would probably be the way to go to support more languages. Now in 2025, LFS is still using code pages, and has even expanded their use to support more languages, such as Chinese, Japanese, and Korean, among others. I believe this happened because it was the easiest way to add support for those languages at the time; while Unicode should be the better solution, implementing it may require an enormous amount of work, and there are definitely other priorities right now.

Still, those code pages bring a few issues with them:
  • Limited language support, as only the available code pages are supported, and I doubt there even exist enough code pages to support most languages.
  • Limited character support: I don't think supporting emoji is a goal here, but there are still many unsupported Unicode characters (basically anything not in the code pages).
  • InSim libraries have to implement a UTF-8 to LFS (and LFS to UTF-8) conversion, as the entire world uses Unicode today ("As of July 2025, UTF-8 is used by 98.7% of surveyed web sites.").
  • For LFS itself, while they still work, they do represent a huge technical debt, where using UTF-8 should make things easier, but changing code pages to UTF-8 is becoming more and more difficult and time consuming.
From reading some older forum posts, I gathered that LFS implements its own font system, because at the time, it was difficult to find free fonts that supported enough characters - this should be a non-issue today, with fonts such as the Noto family supporting a vast amount of characters (possibly through the many Noto fonts dedicated to specific languages), and the general availability of free or even open-source fonts.

-----

Anyway, this is not another "please add Unicode support" thread (although I do want to see Unicode support Big grin), I just made this thread as a recap of sorts of the current situation, and to add an interesting issue I found just yesterday while experimenting with InSim buttons to try and draw road signs.

Take a speed limit sign for example (Europe style): the sign is circular, with a white background, a red border, and black text stating the speed limit. We should be able to draw such a sign using 3 IS_BTN packets. LFS even supports the characters we want to use for that: ● for the background, ○ for the border, and digits for the text (for better control over border thickness, and due to general quality of large text rendering, I ended up using ● for the border as well, resizing the button appropriately). My understanding of InSim button z-order, through earlier testing, has been that buttons with a lower clickID are rendered first, and therefore buttons with a higher clickID appear on top. This is generally true... but not always!

Trying to render my speed limit sign, the text is nowhere to be seen, but if I add more text so it spills out of the sign, I can see it being rendered behind the sign, despite having a higher clickID (in the above example, the text reads "505050"). It would appear that characters from different code pages do not follow the clickID z-order, which becomes a headache when dealing with buttons. (for the record, in the above example, the border button has ID 0, the background button has ID 1, and the text button has ID 2)

Even better, this also applies to characters in a single button: in the following example, I created 2 buttons, same position, same size, with texts "^0●a" and "^7b", and here's what the result looks like:

We only have 2 buttons here, but the "b" character appears both in front and behind characters from the other button, simply because they're from different code pages!

It would be nice to get more info about the rendering logic, or whether this can be fixed for consistency, as it unfortunately makes it impossible to draw those signs, or any other kind of button combination that would rely on characters from different code pages (in this example, ● is converted to a Japanese code page character as [94, 74, 129, 156]).
FGED GREDG RDFGDR GSFDG