Marble Blast Platinum Support

We encourage you first of all to find solutions to your problem in this section's threads. If you do not thus find any solution, please take account of the following:

If you have a problem which prevents you from playing Marble Blast Platinum, please create a new thread on this board with a description of your problem as title. In your post please indicate the computer involved (Mac/PC, operating system).

If in playing Marble Blast Platinum you discover a bug, please post in the Community found Bugs/Mistakes sub-board.

If you need hints for the Marble Blast Platinum game, please post in the Hints, Videos and other Tips sub-board.

question-circle Basic Coding in PlatinumQuest Open Source (TorqueScript)

  • main_gi
  • main_gi's Avatar Topic Author
  • Offline
  • Experienced Marbler
  • Experienced Marbler
22 Mar 2021 14:16 - 23 Mar 2021 10:21 #1 by main_gi
Hi, so I've been working with PQ's open source for a month now. I know enough to make a lot of changes, but not enough to know the extreme details of TorqueScript. That means some parts that I'm unsure about could be wrong, and since I wrote this sentence after writing everything, I'm suddenly a lot less confident about this post. Despite that, I hope it can be helpful.

For a long long time, I thought programmers were hiding sacred knowledge, with the locked source files and everything. And really, if you were like me, who at the time:

- got nervous about sending private messages to people, and
- could not edit images at all
- had no access to the GUI files in MBG
- had some poor tutorials about QuArK
- had no programming experience and was not going to be considered for knowing private things,

Well I don't know how people expected anything other than noob mods to come with that. So it's like ten years later now, here's my programming "guide". Basically, most people are lacking critical knowledge or critical tools that no one ever mentioned.

- Download a better text editor than Notepad. I personally use Sublime Text , which is a very good free editor, although it nags you to pay. There are also editors like Atom or Notepad++ which have about as many features. It has syntax highlighting (that means code coloring), tabs, a much better find feature, and most importantly, filesystem search.

- Use hotkeys when coding. It's when you hold one of the special keys in the bottom of your keyboard (Ctrl, Shift, or Alt) and then push another key.
Ctrl+F: Find (search for text in the current file). Ctrl+H: Find and Replace. Ctrl+Shift+F: Filesystem Search.
Ctrl+C: Copy. Ctrl+V: Paste. Ctrl+X: Cut (copy and also delete the selection).

- Look up documentation and constantly search for things. The version of TorqueScript in PQ is not exactly the same as the documentation online, and there are some hacked functions the devteam made that won't appear in documentation (such as "getFieldValue"), but it's accurate enough. Check the TorqueScript Documentation , a list of random script functions here and/or Torque3D Documentation . Alternatively, you can enjoy this 5755 page pdf . Now, cause it's a dead language, searching is not going to be as useful since people won't have made a solution to copypaste.

- Know some basic programming concepts. If condition, else condition, for loop, while loop, string, number, object, variable, boolean, function, function arguments (I call arguments 'parameters' in this topic), return, break, the fact that indexes start at 0, working with arrays/strings (especially getting the length). You should understand or learn every term that I wrote there, because they are involved in almost everything, as well as having some sort of knowledge of code visualization (like, if you coded something involving a 2D array, be able to visualize what it's doing). This is basic stuff.

- Use the ingame console to test code. It can be accessed by pressing the ~ button at the top left of the keyboard. Every line of code, can also be put in the console. Unfortunately Torque's console is bad, so you can't copy a massive amount of code and put it in the console, you can only do one line at a time, but it still works for debugging. (RandomityGuy says: there is a function called dump() that can list properties of objects and stuff, but it is disabled in vanilla PQ, you have to patch it to be usable in /platinum/core/ui/ConsoleDlg.gui.)

You can also look at the status of current variables with the echo() command, which displays the value to the console. Yes it's the print equivalent in Python or whatever. There's only devecho() which only shows the message when you toggle Debug in the console. When you close the game, you can see everything the console wrote, in console.log in the main directory.

So that console, the intimidating looking thing that was only ever used to type in $testCheats=1; is actually meaningful. It means this global variable (that's what $ means) is being set to 1. Also (this is a purely logical deducation, not special knowledge), it works because the debug editor keys are checking for that variable, meaning you can search testCheats in the filesystem to see where that function is.


Okay, now here is the PQ/TorqueScript specific stuff.

* You're gonna want to have a working modded copy of PQ in order to actually test your code. Copying the extender plugins, and then copying in the open source files, doesn't work for some reason. So copy a fully working copy of PQ, then drag all the open source files in.

(Although you're supposed to be able to compile the whole thing with just the open source and the base PQ files, it didn't work for me. RandomityGuy: "I got my copy of PQ open source working without merging the source files to the existing PQ. All you need to do is, copy these files in the pic, media.discordapp.net/attachments/417263211742756874/823582903577477170/unknown.png , then in the plugins folder. Though do note that I compiled the plugins myself rather than copying from public PQ build.")

* Yeah, you gotta put semicolons at the end of things all the time, besides stuff with braces. This is one of those languages.

* There are a few places code can hide. Mostly in .cs files, which contain TorqueScript. .gui is for interface, and can also contain TorqueScript (the interface is TorqueScript as well, but functions can also be defined in it). Then there's the .mcs files which are just levels that can contain code. Then there is the MBExtender, which uses .cpp (C++ code) and is basically engine hackery. You don't have to mess with the MBExtender very much, and testing MBExtender edits are more annoying due to having to manually compile MBExtender. If you want to make a small or medium size change, messing with the MBExtender probably is not needed. Unless it deals with UI keybindings like ctrl+backspace.
* This is all unimportant, but .dso files are compiled .cs files, and there is some really inaccessible code, the hardcoded MBG code which is hiding in the .exe or .dll files. Don't bother trying to edit them, really, but there is some code locked there, like some marble physics, and the Super Jump and Super Speed effects.

* /platinum/client/scripts/ and /platinum/server/scripts/ contain most of the code. The next most common places to find code are /platinum/client/ui/ and /platinum/core/. /platinum/shared/ also contains code with many useful functions.
* Yes you'll need to look in the server code, because they are active even in singleplayer. It's like the game has a simulated server running all the time. There are some rules to server-code-to-client-code that I don't fully get myself, but any server code to client code seems to have to specifically reference a client function (by convention), and not do things like reference and modify client variables directly (like PlayGui). Instead, it should make callbacks, commandToServer();, and commandToClient(); (which interestingly are seemingly like the only places in the code that use single quotes).

* To save time/processing power you can choose to limit filesystem searches to those areas instead (if you search /platinum/ instead, you'll see that /platinum/data/ contains so many large files and slows the search).
* You can also optionally copy in all the MBExtender code, as there are functions in it that can get called that don't appear anywhere else, such as _innerRadarLoop in radar.cs, which is directly pointing to an MBExtender function.
* The way to think about all these different files in these folders is that some script must be calling them to use them (and that means a filesystem search for the filename shows you where it is). Those are mainly /client/scripts/init.cs, /server/scripts/init.cs, /server/scripts/game.cs, which load a lot of scripts at once. You might occasionally need to check these files in case you do something that, for some reason, has weird interactions with the order of the script executions.

* If you're making mode modifications, look in /server/scripts/modes/null.cs, the function names correspond to stuff you can hook onto in the modes, as in these registerCallback things: this.registerCallback("onClientLeaveGame"); which requires you to define the onClientLeaveGame function in the mode.

* % means local variable (roughly, that means if the variable is inside braces, it can't be used outside those braces). It doesn't work in the console.
* $ means global variable.
* All variables also have to be called with that symbol in front all the time. You can't just define a variable as %something and then type only something later to refer to it. You need the symbol. This does mean you can have both %something and $something as variables.
* That's also why there is no concept of setting variables, just like Python. Variables are empty by default (I believe they are blank strings) and get set when they're called.

* %this. While the concept of the this keyword is a general concept, it's confusing that TorqueScript's %this:

* has the local variable symbol next to it
* all functions with :: in it, pass %this as the first parameter
* all functions CALLING that function with :: ignore placing the first parameter

These implications mean that it makes no sense why you have to do this every time. And yes. It doesn't. But you have to do it. For example:

If a function is defined this way: function PlayGui::setMaxGems(%this,%variable1,%variable2)
Then it's called this way: PlayGui.setMaxGems(%variable1,%variable2). Use a dot and skip that first parameter.

Also if you don't know what %this is, it's the parent (outer function). If you're inside function PlayGui::setGemCount(), %this is PlayGui.

* There is shorthand for string concatenations: @ is nothing ("one" @ "two" = "onetwo"), SPC is space, TAB is tab, NL is newline. Note that you cannot concatenate twice in a row, even though it makes sense - like, "h" TAB TAB "i" is a syntax error. Not a big deal, you can write "h" TAB "" TAB "i".

* Time is stored in milliseconds. Moving on.

* Useful functions and PQ terminology:
* This is a general-programming thing, but the "question mark colon" notation is a short version of if-else. 5 > 3 ? "truestring" : "falsestring" returns "truestring" because the condition before the ? is true, and it is on the left side of the colon.
* There are a lot of global variables that can be pretty useful. Check out the files directly in /platinum/client/, config.cs, mbpPrefs.cs, lbprefs.cs, defaults.cs (init.cs is not that useful). The first 3 files are openly visible and moddable in vanilla PQ, so you can use an optional pref to enable features. They will start with $pref:: or $LBPref::.
* lb() checks if you're on the leaderboards. Do it so that you don't make actual leaderboards server calls in singleplayer.
* mp() checks if you're on a moving platform. No it doesn't, it means multiplayer, stupid.
* getSimTime() gets the current "sim time", which is like "ingame elapsed time", but does not reset when you restart the level. It's still useful for stuff - the Fireball timer uses it by storing getSimTime() when you get the fireball, and then tracking getSimTime() when it disappears.
* Angular velocity is spin.

* formatTime() takes a time and formats it to have the colon and period. It checks thousandths so it will spit out whatever the player has on at the time. formatScore() takes a score and formats it to add commas every 3 digits, because as you know there's a lot of 4-digit scores out there. formatCommas() basically does the same thing.
* endGameSetup(); finishes the level. But you might want to check what other variables should be set when finishing it as well.
* PlayGui contains pretty much every ingame UI element, and occasionally, it is referred to as the "PG" as an abbreviation. It's in /platinum/client/scripts/playGui.gui. Although it sounds like it's only for UI, the values in PlayGui are sometimes useful, like PlayGui.currentTime, PlayGui.totalTime, PlayGui.totalBonus, PlayGui.bonusTime.
* MissionInfo is also useful, like with MissionInfo.time or MissionInfo.ultimateTime. This seems to only work when ingame, in playMissionGui.gui it uses PlayMissionGui.getMissionInfo() instead.
* $TimeColor["normal"] $TimeColor["stopped"] $TimeColor["danger"] for the colors that are traditionally white/red/green. Despite the name those colors are also used for the speedometer and gem counter (when it turns green).
* $Sim, $Game, $Client, $MP are occasionally places variables can hide. ($MP::MyMarble is called a lot, as well as MPGetMyMarble() even when multiplayer is not involved. No, I really have no idea when you're supposed to use $MP::MyMarble instead of the other.)
* alx refers to OpenAL audio effects.
* "RSG" in some commented code refers to Ready Set Go. (not random seed generator)

* Specific files (mostly the unclear filenames will be mentioned):
* /platinum/client/ui/ExitGameDlg.gui is the ingame pause screen GUI.
* /platinum/client/ui/playMissionGui.gui is the level select GUI.
* /platinum/client/scripts/loadinggui.cs is the loading screen code, but you probably will not see it unless you used the Level Search function or started the gaeme with Preload Levels on, because regular loading doesn't bring up a loading screen.
* /platinum/client/scripts/messagehud.cs involves both the middle-bottom-of-screen help-textmessages, and positions the chat and elements near the chat because it can be on/off.
* /platinum/client/scripts/chathud.cs appears to have nothing to do with the leaderboards chat, but instead refers to the bottom-left messages and achievement messages (and maybe the middle-bottom-of-screen help-text messages). In another stroke of genius naming it also refers to "ChatBubble", which can be accidentally found if you filesystem searched for "Bubble".
* centerprint.cs in both client and server, I have no idea what it does. It seemes like it's for spectator text? It definitely isn't the message bubbles. Probably some old MBG file.
* platinum/client/scripts/redundancycheck.cs.dso is NOT in the open source (the .cs file isn't). It is a high-size file (like 1 MB) that contains a function that is used by the LB's anticheat. This seems like it contains info of all the dso files and maybe the lb missions, and which files to check. This file needs to be updated every time an official game update is made. Too bad we don't know how.
* platinum/client/scripts/hats.cs is for Winterfest hats.
* platinum/client/scripts/physics.cs will not contain the physics you want, probably. This is mostly just a list of some triggers like the 2D camera-lock and a part of the water physics.
* moving.cs in both client and server, I think it's not for moving platforms, but instead the "PathNode"s which are a feature added in PQ to make other moving objects move, and for camera paths.
* In the server files, there are a bunch of files that have most of the stuff. Check commands.cs, game.cs, marble.cs.
* platinum/server/scripts/inventory.cs has nothing to do with the game, looks like it's just some unused Torque feature.
* platinum/shared/mp/defaults.cs has a bunch of constants for multiplayer.

* Type conversion and string comparison. TorqueScript has some type conversion, so "400" == 400 and 3.5 == "3.5", but also, string comparisons need to use $= (string equal) or !$= (string not equal). There is also a string version of the switch statement, using switch$.
* String functions are weird compared to most languages. There is no startsWith, endsWith, or regex replace. You're gonna need to simulate them in various ways with getSubStr() or strreplace(). You can see how much they care about string functions seeing as the first thing in this documentation is about *checking for bad words*.

* Here's the syntax for some usual split statements in other languages and how you'd write them in TorqueScript.
* "01234".length in JavaScript = strlen("01234") = 5
"PQPQ".replace("PQ", "x") = strreplace("PQPQ", "PQ", "x") = "xx". Very normal stuff.
"Zero One Two".split(" ")[2] in JavaScript = getWord("Zero One Two", 2) = "Two"
findWord("Zero One Two", "Two") = "Two"
findWord("Zero One Two", "Three") = -1 (kinda like the -1 you'd get from an indexOf in JavaScript)
"Zero One Two".indexOf("One Tw") = strstr("Zero One Two", "One Tw"). Yes. It's called strstr.

There are also versions of the "Word" functions that are "Field" and "Record" instead: like getField and getRecord, as well as the rest of what you'd expect (such as setWord(sourcestring, index, newword), getWordCount(sourcestring)).
A word is a part of a string separated by space, newline, or tab. getWord("Zero" SPC "One" NL "Two" TAB "Three", 2) = "Two"
A field is that, but a newline or a tab. getField("Zero" SPC "One" NL "Two" TAB "Three", 0) = "Zero One"
A record is that, but only a newline. getRecord("Zero" SPC "One" NL "Two" TAB "Three", 1) = "Two" TAB "Three" (can't render tabs in example code, so, lol)

There is also getWords("Zero One Two Three", 1, 2) = "One Two".

There is also getFieldValue and setFieldValue which are apparently PQ functions that were added in. This is basically for a string that acts like an object. Really I'm not sure how it works, but all I know is you can do setFieldValue("a", "b") and then getFieldValue("a") will return "b". This isn't reaaally how you'd expect a field to work, but just treat it as the abstraction it is.

"0123456789"[3:4] = getSubStr("0123456789", 3, 1) = "3". getSubStr uses the number of characters after, it's not a slice.
"0123456789"[3:7] = getSubStr("0123456789", 3, 4) = "3456"
"0123456789"[3:] = getSubStr("0123456789", 3, 1000) = "3456789". No way to omit the ending in TorqueScript, you gotta put in a stupidly high number to search to the end. Alternatively, you can search it by a single character, strchr("0123456789", "3") will also return "3456789".

* Formatting text.
* <color:FFFFFF>, change color using a hexcode
* <font:48>, make the current font as size 48
* <bold:48>
* <font:Marker Felt:48>. Fonts are not 'defined' anywhere, they are just in /platinum/core/ui/cache as .gft files. I'm not sure what .gft means, it's some old as hell filetype you need special software to convert fonts into. Although the fonts have underscores they turn into spaces in the code. Probably best to name the fonts without underscores.
* <shadow:1:1><shadowcolor:0000007f> to add shadows. <shadow:1:1> is an offset of 1 x, 1 y.

* You might notice that there are no ending tags. Actually the tags are called <spush> and <spop>, which saves and restores formatting. You want something temporarily bold, you do <spush><bold:48>BOLD<spop> NOT BOLD ANYMORE.

A larger list is in this link garagegames.com/community/blogs/view/15117 but I already listed the essentials.

* Math functions. Won't need to do them too often, but mostly it happens when you're doing logic for cooldowns and timers. There's mAbs mFloor mCeil mRound which all take one parameter and do what you'd expect (the m means math). If you don't understand, use a damn search engine. There are also vectors and matrixes (I'm not calling it "matrices"), which you'll only have to deal with if you do anything involving physics, which unfortunately I don't know enough about yet, but a good place to start is client/scripts/cannon.cs, which approximates a curve when drawing the shooting preview. But any function starting with m and then a capital letter is probably related to math, just in case you get confused by the mAtan function.

* Please cut your nails properly. I keep seeing videos of people with fingernails that look way too short. If it is tempting to cut them completely short, I understand, but if you cut past the yellowish-white region, it might regrow over and over or show cracks at the very least, or create infections at the worst. Be even more careful with toenails, try to not cut them short at all and make horizontal cuts. This info is programming related because it relates to typing.

* schedule() executes a function in the future. This is actually a highly useful function that appears a lot, for stuff like making sure timers are updated and doing animations. But as expected, the syntax is unintuitive. Call it with schedule(1000, 0, functionName, param1, param2, ...) where the second argument is zero, the third is the function name WITHOUT PARENTHESES, the rest are the parameters for that function.

You might want to cancel the schedule as well for whatever reason. To do that, you have to assign a variable to the schedule, like %variablewhatever = schedule(1000, 0, functionName, param1, param2, ...), and then do cancel(%variablewhatever).

There is also another version of the schedule function that ties it into an object, so the schedule gets autocancelled if you destroy the object. It *has different syntax*. For example, in my code that puts powerup timers onscreen, one line looks like this: %this.powerupTimersSchedules[%curIndex] = %this.schedule(%duration, popPowerupTimer, %curIndex);. %this being PlayGui, by the way. But I did not define popPowerupTimer as global function, it was function PlayGui::popPowerupTimer, it was tied to PlayGui as you can see. I could not get a normal schedule to work so I used this version of the schedule function instead.

(Edit: Although the normal "schedule" function that has the "0" as second argument can be the function instead. This should work the same way as the function in the above paragraph.)

*You can't change a schedule's parameters after it's been made*. This was a problem for me since I was using a 'powerup queue' which could have elements displaced at any moment, so I solved it by making the schedule parameters be indexes, so I could change whatever that index pointed to when doing the displacement.

* GUI. Turns out modifying a GUI is kind of simple. The main things to care about are 'position' and 'extent' (which means width and height), and that most everything in the .gui files are "sub-elements" that are defined inside another element (hinted at by the indents), meaning they will be based off the outer-element's position and extent.

If this is not making sense to you, please, please visualize every UI element as a rectangle that is taking up space, this includes the space of text and images, even if it's not shaped like a rectangle. A sub-object is like a number in one of the timers. It's 'inside' the timer, so its position and extent are based off the timer. (technical term: an outer element is called a parent, a sub-element is called a child, so the timer box is the parent of the timer number, and the timer number is a child of the timer box)

There's way too many various functions for the GUI and I don't fully understand it all, but again, precision copypasting works well here. I did the whole pause menu redesign that way.

* You can change GUI position with elementblahblah.setPosition(). Use that instead of elementblahblah.position = numberblah due to various HiGuyish reasons. The same works with setExtent. However, these just don't work in the console when testing. Seems like console setPosition ignores all the outer-element offsets.
* Changing the extent of a center-aligned element will not really appear to change anything, unless something was clipping due to low extent somehow or it was an image or something. (not sure about this)
* The size of GUI elements stays the same absolute size between resolutions. That means it looks big and more cramped on small screens, and small and far on large screens. This can lead to ugly stuff without some extreme resolution-check hackery that no one cares to implement.
* It's possible to programatically add GUI elements. See this code:
$newthing = new GuiMLTextCtrl("newthing") {
  profile = "GuiMLTextProfile";
  horizSizing = "right";
  position = "200 200";
  extent = "100 100";
  minExtent = "8 8";
  visible = "1";
  helpTag = "0";
  lineSpacing = "2";
  allowColorChars = "0";
  maxChars = "-1";
Which can later be called by $newthing as well.

* Arrays are half fake. There is not even a push or pop function or split or join, and there is a crazy convention: $pref::DefaultFontPointPopups is equal to $pref::DefaultFont["PointPopups"], %variable[1] is equal to %variable1. So it's almost like arrays are not real. The thing that is kind of dumb is that many array-likes cannot be accessed using %array[2] or whatever, but some function like %array.getEntry(2).
%count = %group.getCount();
  for (%i = 0; %i < %count; %i++) {
(Warning: I'm not sure .getCount() actually works on arrays. This official Torque3D article tries to explain arrays, but there is no length or getCount mentioned, just terrible hardcoded examples.) There is another point of awkwardness, you can't delete elements, so the function checking for 'length' is really checking for values that are legit. That means a different kind of for loop condition is actually used in the code sometimes:
for (%i = 0; $pref::Video::TexturePack[%i] !$= ""; %i ++) {
See the $pref::Video::TexturePack[%i] !$= "" part, is just checking that there is something there and not a blank string - which is not an ordinary array length loop.

Due to the fakery of arrays, I recommend finding other solutions. The "word", "field", and "record" functions mentioned earlier can effectively simulate arrays. The first time I tried to program an array I made a "length variable" which goes up and down by 1 as elements are 'added' or 'removed' to simulate a real length, which might legitimately be more performant.

* SimSet, SimGroup, SimObject. AHHHHH TERMS I DON'T RECOGNIZE! Wait, it's just a set, group, and object, corresponding to the game (simulation). Wow, that really was not intimidating. These things are objectlike, meaning they have properties and methods to check (like %object.isHidden()) with the period syntax.

Anyway, it's probably easier to just look at /platinum/server/scripts/game.cs, to see how a function iterates over a SimGroup. Technically, this function is called starting with countVisibleGems(MissionGroup); most of the time, but that is a type of SimGroup. Note that there's recursion in this function, which is another general programming concept (the function is calling itself to execute the same function on all of its groups).
function countVisibleGems(%group) {
  // Count up all gems out there are in the world
  %gems = 0;
  %count = %group.getCount();
  for (%i = 0; %i < %count; %i++) {
    %object = %group.getObject(%i);
    %type = %object.getClassName();
    if (%type $= "SimGroup")
      %gems += countVisibleGems(%object);
    else if (%type $= "Item" && %object.getDatablock().classname $= "Gem" && !%object.isHidden())
  return %gems;
Watch out for all the different names for the same type of game mechanic that do the same thing. There's all the gem items, there's the duplicated powerups from MBG to PQ, the pads, a lot of stuff you could miss. So make sure to uh, not miss those.

There is also the .getSize() method, and I literally do not know why it exists. TorqueScript documentation says that .getSize() is really for graphics, but the code occasionally is showing it being used to count arrays. But that's what .getCount() does! Whatever, just be aware that .getSize() exists in case your .getCount() fails.

* Mass-file actions. Not sure where to put this, but you should really be aware that mass-file actions also exist. It's where you use a program to change many files at once, instead of doing it manually. I've used code before to delete all the fake and negative Secondglasses from Marble Blast Stop 2 in one fell swoop, instead of manually going through each file. This is even more relevant than ever when levels have 3 different versions: the normal, the leaderboards, and the co-op, that you have to change them all. For example, if you want to change Marble Blast Gold level startpads (since currently they appear as the default MBP startpads), you will literally have to go through 300 level files.

So just learn Python instead, or whatever other language that can do mass-file replacing, and learn to iterate over directories and modify files, search stuff like os.walk and .read() and .write(). It will be a more efficient use of time than manually modifying the 300 level files.

* Finally I should talk about GitHub, although the old devs don't use it for us anymore. It's a base of a command line tool called 'git', which is described as a "version control" system. The idea is that instead of storing versions as entire copies, it stores the differences ('diffs') between files. GitHub is an online interface which works with git, and provides file storage. The problem is that, while it works with git, the actual GitHub online interface does not act like git, and is way worse in functionality. For example, you can do rebases (history revisions) and undo commits in git (and a lot of other features), but it's not possible to do it with only the GitHub web interface.
* The proper way to submit changes (if the old PQ repository was being maintained) is to make a fork of the open source PlatinumQuest, and don't touch the fork directly. Instead make a new branch of it and modify that instead.
* I bring this up because real programmers are probably using git - although, you probably don't need to. Instead of having a hacked-together copy with a bunch of random changes, you split every change you make into its own branch, and then when you're done, merge the branches you actually want together. The "hacked together" way of coding will still work, but submitting those changes will be more annoying to do, since if many different changes are modifying the same file, you will basically have to undo the irrelevant code by memorizing what you changed, which is not a thing with git.

some comments from RandomityGuy:

-Git: It's literally a time machine/rewind for code, very powerful once mastered. Learn it through learngitbranching.js.org/ .
-Torque Game Engine Source: github.com/ldarren/tge-152-fork , this is one of the requirements if you want to engine hack, because you are gonna search for the functions that you are gonna override in the source, then use MBExtender to patch that function. (If the address of the function that you want to patch does not exist, you'll have to get a disassembler and disassemble MB to find the required function address).
-Overall MBExtender is a very powerful tool since it allows you to literally call C++ call from torquescript and other magic stuff. Combined with this and the vast C++ libraries that exist to do various tasks that torquescript can't do. There is absolutely nothing that cannot be done in MB given enough time.
-The misconception of x cant be figured out because y devs did not give documentation is absolutely gross, I despise it. If you want to figure how x works, just apply the scientific principle. Think of x as a black box, give inputs, and observe the outputs. Then figure out what exactly x does by looking at the inputs and outputs. For eg. You can figure out how the entire server side leaderboards works by solely applying this method, this means that there was absolutely no reason to release leaderboards codes at all as there is enough data to reconstruct a working replica of it.

wow this forum really does not support inline code
Last edit: 23 Mar 2021 10:21 by main_gi.
The following user(s) said Thank You: Enigma, Tue27, Nature Freak, Nockess, c0wmanglr

Please Log in or Create an account to join the conversation.

  • main_gi
  • main_gi's Avatar Topic Author
  • Offline
  • Experienced Marbler
  • Experienced Marbler
22 Mar 2021 14:17 #2 by main_gi
Here are some examples of modifications I made. This one is Game Paused: Pause Sounds.

I search "pause" in /platinum/client/ for the first thing. I found this in /platinum/client/scripts/default.bind.cs:
function input_escapeFromGame(%val) {
	if ($Game::State $= "End" || (%val !$= "" && !%val))
	if (ControllerGui.isJoystick()) {
	// We don't want the disconnect DLG for LB peoples
	if ($Server::ServerType $= "SinglePlayer" || $LB::LoggedIn)  {
		// In single player, we'll pause the game while the dialog box is up.
		if ($Server::ServerType $= "Multiplayer") {
			if (!$Server::Lobby && !$Game::Pregame)  {
		} else {
			alxSetChannelVolume(1, 0); // main_gi
	} else {
		MessageBoxYesNo("Disconnect", "Disconnect from the server?",
		                "disconnect(); hideControllerUI();", "hideControllerUI();");
First of all it's quite convenient that "pause" was in comments. But if it wasn't there, there still other keywords, like "Escape" because that's the pause keybind, and "ExitGameDlg" which is the pause GUI.

It starts with 2 if conditions, both unrelated to the pausing. One checks if the gamestate ended (it won't let you pause after finishing a level), and the other is ControllerGui.isJoystick(), pretty obvious stuff, and irrelevant.
There is another if-condition, $Server::ServerType $= "SinglePlayer" || $LB::LoggedIn. It's a singleplayer check or LB check. Then it checks if the server is in multiplayer, which can only happen if $LB::LoggedIn was true. That means the right singleplayer pause function is in the adjacent "else" statement.

The code alxSetChannelVolume(1, 0); comes from menu.cs, which also contains this code that sets the volume back to normal: alxSetChannelVolume(1, $pref::Audio::channelVolume1);. Next, I check ExitGameDlg.gui, because I already know that's the pause screen, where I need to put the reset-volume function back. I see this:
commandSelect = "hideControllerUI(); ExitGameDlg.close(); ExitGameDlg.end();";
commandCancel = "hideControllerUI(); ExitGameDlg.close(); resumeGame();";
commandAlt1 = "hideControllerUI(); ExitGameDlg.close(); resumeGame(); restartLevel(true);";
Great! Let's put alxSetChannelVolume(1, $pref::Audio::channelVolume1); in it!
Okay no it did nothing. I try for like 2 minutes and realize these buttons don't correspond to anything. "commandSelect"? What?

Yeah they're probably for controllers. I considered putting the reset code onto every function, but instead, I can modify the game resume function to reset volumes instead!

I do a filesystem search on function resumeGame, find this:
function resumeGame() {
	// resume game
	alxSetChannelVolume(1, $pref::Audio::channelVolume1); // main_gi: fix volume
	$gamePaused = false;
I put the alxSetChannelVolume command in, and the change is done. Note that I do not even need to have significant knowledge of the things I read in order to do this code, like what "alx" is, or even the convention that the "channels" are using 1 = ingame sounds and 3 = music. I'm just precisely copypasting code, which can be done for most changes.

Much of Programming is Precision Copypasting... Except when it goes wrong. The idea of 'precision copypasting' is that you look at things in the game that already have the exact behavior or similar behavior as what you want. If they are buggy, or are actually unused code, it is gonna fail, so you need to test it.

Madness: Finish Level on OOB: I wanted Gem Madness to finish the level on OOB because I disliked having to restart the level if you fell or something, it meant you could get a higher score than your personal best, but still have the chance to lose it, even though you could wait out the timer to "keep the score", and any player would prefer not to go OOB anyway.

Well, that caused a WR bug. Why? I copypasted from the finish pad code. Turns out I missed that in 2019 the Hydropower finish pad was removed because of bugs. I really thought it was still there. The stupid thing is that it would be easy to fix in code, but the pad was removed instead. The pad code was in onEnterPad, like:
$Game::FinishClient = %object.client;
And I just copied it to onOutOfBounds (I modified shouldRestartOnOOB to say it didn't restart on OOB). But the time expire code says:
%this.gotAllGems = false;
	commandToAll('UseTimeScore', false);
Which means the finish pad code should've had that as well, and the OOB code should've had that, but it didn't. There was also a problem with testing, in that it appeared to work fine, until it turns out that the variables set when finishing a Madness level with a time don't get cleared and whoop, there's the bug. Basically it just shows bad code + lacking testing cause there are no easy Madness levels to complete.


Here's how I'm implementing a Time Travel Timer. The base code for this is pretty easy, but first I will like to point at Dependency as an example of copypasting not working.
package Dependency {
	function RespawningTimeTravelItem_PQ::onPickup(%this, %obj, %user) {
		if (!Parent::onPickup(%this, %obj, %user)) {
			return false;
		if (!MissionInfo.dependency) {
		commandToClient(%user.client, 'StartCountdown', $Time::BonusTime, "timerTimeTravel");
		return true;
First of all I'd like to point out the irony in that this is custom code in an official PQ level and is not coded properly. Specifically that the package deactivates but still executes code, which means if you play Dependency and then another level with respawning PQ TT's (so, a Hunt level), you will get a time travel countdown for the first respawning PQ TT you pick up. Very nice.

But anyway, if you think copypasting this code is a good idea, it's not. Turns out this code is a big hack that only works because Dependency has only that one TT. "Starting a countdown" is a purely visual thing, so it doesn't update with other Time Travels, it doesn't update with timestops, and it overlaps lap UI and overlaps other countdowns. I can fix the "doesn't update with other Time Travels" by putting it in the TT class, but at that point, this is when I know it's a bad idea.

In the end, the only thing I end up precision copypasting is the UI, but I place it somewhere differently and change it. Moving the UI was just messing with the positions and extents until it looked right, modifying each position by the same amount.

Anyway, here is the thing I understood about this code: The timer is still updating every frame in order to animate the countdown properly. That means I can find whatever is updating the countdown, and make it also update the time travel countdown. This structure of defining the variables is very common with timers, where .setNumberColor(%number, %color); has to be used for each digit, so each digit has to be calculated using division and getting the remainder of division with modulo (the % symbol).

Then there is stuff like this PGCountdownTTPoint.setPosition((%secondsLeft >= 10 ? "403" : "388") + %offsetIfThousandths SPC "0");, which might be confusing because it's a chain of things, but remember that .setPosition() just wants two numbers with a space inbetween, so what's happening is checking if there's >=10 seconds (double digits) and offsetting the position of the point.

This code all works, except that there is a convention that green = stopped time, and with just the position code, it doesn't update properly when even the bonus timer is stopped. I really want to put in small touches to make that work. I added this line:

%color = (%this.stopped || $PlayTimerActive == 0) ? $TimeColor["stopped"] : $TimeColor["normal"];

But it didn't have $PlayTimerActive at first, it was just %this.stopped. However, the code didn't work. I tested The Spoils of Serendipity Gardens multiple times, which is a level that has TT's and timestops. The TT timer stopped as usual, but it didn't turn green. I thought %this.stopped was the problem, and wasted a bunch of time with that, but it's actually not the problem. I eventually realized that updateTimeTravelCountdown() is just not being called when the time is stopped, even though updateTimer() makes it look like it due to a misleading function:
if (%this.stopped) {
		// HACK: If inside Time Stop trigger, keep time stopped by adding bonus time
		%this.bonusTime = add64_int(%this.bonusTime, %timeInc);
But this code is not actually triggered (in singleplayer, at least). What causes the time to stop is Time::stop();. I would have realized sooner, if I typed updateTimeTravelCountdown() in the console and did it. So here's how it's fixed. I looked in the TimeStopTrigger code, here, and added a new line: commandToClient(%obj.client, 'UpdateTimeTravelCountdown');.

The commandToClient is used because you don't want to directly call PlayGui in these functions (and if I understand it right, it can also legitimately be buggy - if someone picks up a Time Travel in a co-op level, the bonus timer should update on everyone's GUI's, not just whover picked it up, so you want commandToAll). To define this command, I put this in playGui.cs:
function clientCmdUpdateTimeTravelCountdown() {
Anyway, the new line goes here:
function TimeStopTrigger::onEnterTrigger(%this,%trigger,%obj) {
	//Don't do this in MP
	if ($Server::ServerType $= "MultiPlayer")
	%obj.client.timeStopTriggers ++;
	if (%obj.client.timeStopTriggers == 1) {
		$Game::TimeStoppedClients ++;
		if ($Game::TimeStoppedClients == 1) {
			commandToClient(%obj.client, 'UpdateTimeTravelCountdown'); // main_gi v4.2.3: Update TT timer even in a timestop
It goes after Time::stop(); because it seems to me that it is that function that's causing the issues, not %obj.client.timeStopTriggers ++; or $Game::TimeStoppedClients ++;. I could be reasonably sure about that, even if not certainly.

But doing this lead to inconsistencies with other timestopped TT's, and luckily I was aware of the proper testing locations, The Spoils of Serendipity Gardens (timestop with TT), Gravity Swap (get TT at the start), Time Trial (finish level with TT). These did not take any time at all to change. platinum/server/scripts/game.cs contained endGameSetup(), and platinum/server/scripts/powerups.cs could be used to make Time Travels pickup also call this function. And that is basically it.


Here's how I implemented the Gem Change Trigger. This is incredibly not special. You can see how much is copypasted off the Time Bonus trigger which is also in this code. displayGemMessage is copied, and functions involving gems are easy to find.
datablock TriggerData(GemChangeTrigger) {
	tickPeriodMS = 50;
	greenMessageColor = "99ff99";
	grayMessageColor = "cccccc";
	redMessageColor = "ff9999";
	customField[0, "field"  ] = "gemBonus";
	customField[0, "type"   ] = "numeric";
	customField[0, "name"   ] = "Gem Change Amount";
	customField[0, "desc"   ] = "How many points to add or subtract.";
	customField[0, "default"] = -1;
function GemChangeTrigger::onEnterTrigger(%this,%trigger,%obj) {
	if (%trigger.gemBonus $= "")
		%trigger.gemBonus = -1; // made default, the "-1" here and in the later part of the code, are because -1 is the default
	if (%trigger.gemBonus < 0 && %obj.client.gemCount < mAbs(%trigger.gemBonus)) { // in the case where gems would subtract into the negative
		if (%obj.client.gemCount != 0) { // give no call to the client if they already have 0 gems
			%obj.client.gemCount = 0;   // - works only silently, doesn't update gem counter
			%obj.client.setGemCount(0); // - works only visually, updates only gem counter
	} else {
		%obj.client.gemCount += %trigger.gemBonus;
		%obj.client.setGemCount(%obj.client.gemCount + %trigger.gemBonus);
	%bonus = (%trigger.gemBonus $= "" ? -1 : %trigger.gemBonus);
	%color = (%bonus == 0 ? %this.grayMessageColor : (%bonus < 0 ? %this.redMessageColor : %this.greenMessageColor));
	%sign = (%bonus > 0 ? "+" : "");
	//Show a message
	%obj.client.displayGemMessage(%sign @ %bonus, %color);
datablock TriggerData(TimeTravelTrigger) {
	tickPeriodMS = 50;
	//For the time message
	greenMessageColor = "99ff99";
	grayMessageColor = "cccccc";
	redMessageColor = "ff9999";
	customField[0, "field"  ] = "timeBonus";
	customField[0, "type"   ] = "time";
	customField[0, "name"   ] = "Time Bonus";
	customField[0, "desc"   ] = "How much bonus time to add.";
	customField[0, "default"] = $Game::TimeTravelBonus;
function TimeTravelTrigger::onEnterTrigger(%this,%trigger,%obj) {
	if (%trigger.timeBonus $= "")
		%trigger.timeBonus = $Game::TimeTravelBonus;
	%bonus = (%trigger.timeBonus $= "" ? $Game::TimeTravelBonus : %trigger.timeBonus);
	%color = (%bonus == 0 ? %this.grayMessageColor : (%bonus < 0 ? %this.redMessageColor : %this.greenMessageColor));
	//Hunt maps are reversed
	%sign = (Mode::callback("timeMultiplier", 1) > 0 ? (%bonus < 0 ? "+" : "-") : (%bonus < 0 ? "-" : "+"));
	//Show a message
	%obj.client.displayGemMessage(%sign @ mAbs(%bonus / 1000) @ "s", %color);
The following user(s) said Thank You: Nature Freak, c0wmanglr

Please Log in or Create an account to join the conversation.

  • Weather
  • Weather's Avatar
  • Offline
  • Professional Marbler
  • Professional Marbler
22 Mar 2021 17:43 #3 by Weather
I have a few things to offer:
  • Strictly speaking, arguments are values passed to functions, and parameters are local variables defined in function headers.
  • Local variables work in the console if entered with their references:
    %k = localClientConnection.player.getDataBlock(); %k.cameraDistance = 5;
  • %this is written in method headers because "this" is not a meaningful word in TorqueScript. It can be replaced:
    function LandMine::onCollision(%datablock, %obj, %col) {
    I believe the first parameter of a method always refers to the object/datablock it was defined on, not instances of that. PlayGui is an instance of a more general object but has methods defined specifically for it. In the case of LandMine::onCollision(), LandMine gets the first parameter, the second one is the instance in the level, and the third one is the marble that collided with it.
  • This might not be useful anymore, but dumpConsoleClasses() will flood the console with, as far as I know, a list of every object method accessable.
  • If you make a variable starting with $pref:: it will be saved in mbpPrefs.cs.
    The function for saving variables to a file is export(%name, %fileName, %append). It writes all global variables matching %name to the path %fileName. If %append is true, it will only append and not overwrite or create the file. Only %name is a required parameter.
    So for example: export("$pref::*", "platinum/client/mbpPrefs.cs")
  • PQ provides functions for missions to overwrite. They are redefined in these files before mission load and start with "serverCb" or "clientCb":
  • In PQ, the timer is managed by $Time, not PlayGui. See platinum/server/scripts/mp/time.cs.
The following user(s) said Thank You: Nature Freak

Please Log in or Create an account to join the conversation.

20 Aug 2021 03:58 - 20 Aug 2021 12:45 #4 by EdFanSus-ble
I like the fact that you can now mod the game to add more features because it's now open-source, but I am surprised that there's no video guide on some of these, even at the beginning. I sometimes get coaught up on the "Failed to open "main.cs"" part.

I am not forcing you guys to make a video, but I just thought videos are more helpful because guides can be a bit confusing at times.

UPDATE: I have figured out the "Failed to open "main.cs"" part. Now to figure out how to add the power-up doubler into the mod...
Last edit: 20 Aug 2021 12:45 by EdFanSus-ble. Reason: Updates added to the comment

Please Log in or Create an account to join the conversation.

Moderators: DoomblahGo'way