NOTE: The reference here is in progress and may expand later as rulesets become more powerful.
Spirit Drop holds a versus engine that can refer to ruleset objects. You can create these rulesets from JSON files that are formatted appropriately. An in-game editor will come for v0.2.1.0 if you don't like writing in JSON. The parser interprets JSONC (JSON with Comments), but if your IDE is against comments, you can use keys starting with "_c" anywhere.
To get started, create a .json file with this template (IDE with JSON Schema support recommended!):
{ "name": "Your Ruleset Name", "author": "your_username", "identifier": "unique_ruleset_id", "versionNumber": "1", "$schema": "https://rayblastgames.com/spiritdrop/ruleset-schema.json" // Insert rule events here }
After this, you can append event names with array values. Each event contains multiple rule objects. Every rule is identified by "type"
.
Jump into the Discord server if you have any questions
To include a variable for synchronization in multiplayer and replays, append one of these to the ruleset:
"syncedModeVariables": [ // Array of mode variables (don't include "mode.") ]
"syncedGroupVariables": [ // Array of group variables (don't include "group.") ]
"syncedBoardVariables": [ // Array of board variables (don't include "board.") ]
Each time you set these variables (read: each time) the values require networking. Setting them every frame will cause several problems such as restricting the max number of players, slowing down the framerate, occupying gigabytes in replay size, or a combination of such. Worth noting is that these variables are only updated when time starts passing, so you may get unexpected behavior if you were to treat them as normal compute variables (using a variable immediately after changing it results in the variable returning its old value). Generally, you want to use the variables within the events that are called for them. These variables should be saved for values that do not change often, such as points rewarded to winners of versus rounds.
Name | Who can write | Event called | Safe to control timings |
---|---|---|---|
syncedModeVariables | The current host | syncedModeVariableRules | No |
syncedGroupVariables | The player with the lowest boardIndex in the group | syncedGroupVariableRules | No |
syncedBoardVariables | The player controlling the board | syncedBoardVariableRules | Yes |
To include a new event, simply append the name of the event, and then give it an array value. Note that "buttonPressRules"
and "userDefinedRules"
are structured differently.
Whenever one of these events start, the scope variables are cleared (except for userDefinedRules).
Array of rules that are ran when the mode starts.
Context:
"modeStartRules": [ // Array of rules ]
Array of rules that are ran whenever a board is created.
Context:
"boardStartRules": [ // Array of rules ]
Array of rules that are ran every frame (delta time varies, do not assume 60 fps).
Context:
"modePassiveRules": [ // Array of rules ]
Array of rules that are ran every frame for each board (delta time varies, do not assume 60 fps).
Context:
"boardPassiveRules": [ // Array of rules ]
Array of rules that are ran whenever a synced mode variable changes. NOTE: Need to supply info about how to check what variable changed.
Context:
"syncedModeVariableRules": [ // Array of rules ]
Array of rules that are ran whenever a synced group variable changes. NOTE: Need to supply info about how to check what variable changed.
Context:
"syncedGroupVariableRules": [ // Array of rules ]
Array of rules that are ran whenever a synced board variable changes. This event provides a synchronization point for board events. NOTE: Need to supply info about how to check what variable changed.
Context:
"syncedBoardVariableRules": [ // Array of rules ]
Array of rules that are ran whenever a piece is placed, right before attack and cancel is used. This event allows calculating resultAttackPower, resultBaseAttack, and resultGarbageCancel.
Context:
"attackCommitRules": [ // Array of rules ]
Array of rules that are ran after attackCommitRules.
Context:
"placementRules": [ // Array of rules ]
Array of rules that are ran whenever an attack is created by a board.
Context:
"attackPushRules": [ // Array of rules ]
Array of rules that are ran immediately after a board receives an attack.
Context:
"attackReceiveRules": [ // Array of rules ]
Array of rules that are ran when an attack arrives at a board.
Context:
"beforeGarbageAcceptRules": [ // Array of rules ]
Array of rules that are ran after the attack pushes garbage onto a board's garbage meter.
Context:
"garbageAcceptRules": [ // Array of rules ]
Array of rules that are ran whenever the player allows garbage to enter into the Rise Meter.
Context:
"beforeGarbageIntakeRules": [ // Array of rules ]
Array of rules that are ran after garbage has been put into the Rise Meter.
Context:
"garbageIntakeRules": [ // Array of rules ]
Array of rules that are ran after each line enters the board from the Rise Meter.
Context:
"riseLineRules": [ // Array of rules ]
Array of rules that are ran before a player declares themselves as topped out. Useful to avert normal lose conditions by unblocking the spawn window.
Context:
"beforeTopOutRules": [ // Array of rules ]
Array of rules that are ran whenever a player locks out or blocks out.
Context:
"playerBreakdownRules": [ // Array of rules ]
Two-dimensional array of rules that are ran whenever the player presses one of the General buttons.
The number of arrays in here should range between 1 and 4. You cannot have more than 4 because only 4 General buttons can be used in the game.
Context:
"buttonPressRules": [ // General1 Rules [ // Array of rules ], // General2 Rules [ // Array of rules ] ]
Custom events that can be triggered with the userDefined rule, gives the ability to create functions.
Instead of being an array, this one is an object. Every userDefined event you create will be a key here.
Note that invoking one of these events does not clear the context, so you could treat these as mode functions, board functions, etc.
"userDefinedRules": { /* userFunction1 - An example event to demonstrate syntax */ "userFunction1": [ // Array of rules ], /* userFunction2 - Another example event to demonstrate syntax Variables used: variable1, board.variable2 */ "userFunction2": [ // Array of rules ] }
Adds a value to a variable.
{ // Add 10 to the board's variable1 "type": "add", "variable": "board.variable1", "expression": "10" }
Applies rules to all alive groups.
{ // Multiply the points of each alive group by 2 "type": "aliveGroupsRelative", "rules": [ { "type": "multiply" "variable": "group.points" "expression": "2" } ] }
Applies rules to all alive players; be careful not to use this multiple times in a row.
{ // Give a point to each alive player "type": "alivePlayersRelative", "rules": [ { "type": "add" "variable": "board.points" "expression": "1" } ] }
Causes a large message to appear on the right side of the screen. Use the s+ operator to print variable values inside your messages.
{ // Tell everyone that this player has x.xx seconds remaining "type": "announce", "expression": "$board.playerName; ' has '; $board.timeLeft; :0.00; ' seconds left!'; s+; s+; s+" // 4 strings in the stack, so do 3 concatenations }
Gives a variable a value.
{ // Set the board's variable1 to (variable2 + 6) * 3 "type": "assign", "variable": "board.variable1", "expression": "$variable2; 6; +; 3; *" }
The same as assign, but if the variable is not assigned yet it will create a scope variable instead of throwing an exception
Note that mode variables, group variables, and board variables will be automatically initialized as null; number type operations will treat this as 0 (zero)
{ // Create variable3 set to the larger of board.variable or half of variable2 "type": "assignNew", "variable": "variable3", "luaPath": "myLuaScript::max", "luaArguments": [ "$board.variable1" "$variable2; 2; /" // Pass in 1/2 of variable2 ] }
-- returns the larger of the two numbers function max(num1, num2) if(num1 > num2) then result = num1 else result = num2 end return result end
Creates an attack object and pushes it through the current board's targeting style; due to limitations, Lua calls must be handled beforehand.
{ "type": "createAttack", "lineCount": "$resultAttackSent", "mutateChance": "$mutateChance", "cheeseRate": "$cheeseRate", "travelTime": "0.5", }
Allows the mode to end in 3 seconds
{ "type": "endMode" }
Applies rules to all boards of the current group. This event cannot be called from a mode event or a button press event unless such an event uses another relative rule.
{ // Increase the gravity of all the boards of the group "type": "groupBoardsRelative", "rules": [ { "type": "multiply" "variable": "board.gravityInterval" "expression": "0.9" } ] }
Applies rules to one group.
{ // Increase the gravity of all the boards of the second group "type": "groupIndexRelative", "expression": "1", "rules": [ { "type": "multiply" "variable": "board.gravityInterval" "expression": "0.5" } ] }
Controls flow based on a statement
For complex expression ternary, you should use this rule instead, as ternary requires evaluating the values to pick from, while these rules are skippable.
If you want to use Lua to compute logic, use an assign rule beforehand
{ // Check lines cleared "type": "if", "statement": "$result.linesCleared >= 4", "onTrue": [ { // Add 4 to the attack total "type": "add" "variable": "result.attackPower" "expression": "4" } ], "onFalse": [ { // Throw away the attack "type": "assign" "variable": "result.attackPower" "expression": "0" } ] }
Calls a Lua script with a specific name and a specific function.
If you want to use this function to compute values (likely), then you should use assign, add, multiply, etc.
{ "type": "lua", "path": "myLuaScript::testFunction", "arguments": [ "$board.statistics[Lines]" ] }
Multiplies a number to a variable.
{ // Multiplies the board's variable1 by 2.5 "type": "multiply", "variable": "board.variable1", "expression": "2.5" }
Applies rules to one player.
{ // Makes the first player receive garbage one at a time "type": "playerIndexRelative", "expression": "0", "rules": [ { "type": "assign" "variable": "board.garbageIntake" "expression": "'individual'" } ] }
Plays a sound effect in the Sounds folder.
{ // Plays the level up sound "type": "playSound", "expression": "'level up'" }
Causes the current board to halt and be put in the finished state
{ "type": "stopPlaying" }
Causes all players to restart their board states in 2 seconds; in multiplayer, syncing occurs
{ "type": "startRound" }
Subtracts a value from a variable.
{ // Subtract 10 to the board's variable1 "type": "subtract", "variable": "board.variable1", "expression": "10" }
Causes the current board to change target styles.
{ // Sets the player's target style to all attackers and lets them face their wrath "type": "target", "style": "AllAttackers", "attributes": [ "'noDistribution'" ] }
Updates a collection with a key-value pair.
{ // Set key 10 in a dictionary to 'You're too slow' "type": "update", "variable": "board.variable5", "key": "10", "value": "'You\\'re too slow" }
Triggers a user defined event.
Note that the context does not change and the scope variables are not wiped.
{ "type": "userDefined" "name": "updateBoardStage" }
"userDefinedRules": { "updateBoardStage": [ { "type": "assign" "variable": "board.stage" "expression": "$gameTime; 60; //" } ] }
Performs rules while a statement remains true
If you want to use Lua to compute logic, use an assign rule beforehand and after each loop
{ "type": "assignNew" "variable": "a" "expression": "$board.level" }, { // Add ceiling(log2(level)) to attack power "type": "while", "statement": "$a > 1", "rules": [ { // Add 1 to the attack total "type": "add" "variable": "result.attackPower" "expression": "1" }, { // Divide a by 2 "type": "multiply" "variable": "a" "expression": "0.5" } ] }
Variables with get instead of read are considered more expensive to fetch. Variables with set instead of write have side effects.
Name | CLR Type | Permissions | Description |
---|---|---|---|
changedVariable | string | Read | The name of the variable that was changed in the syncedModeVariableRules, syncedGroupVariableRules, and syncedBoardVariableRules events |
gameTime | double | Read | Number of seconds that the current board has been active; stops progressing once the player tops out or when the stop playing rule is ran |
modeTime | double | Read | Number of seconds since the mode started the first round |
roundTime | double | Read | Number of seconds since the round started |
roundNumber | int16 | Read | The current round in the game session, starts at 1 |
playerCount | uint16 | Read | Number of player definitions there are |
alivePlayerCount | uint16 | Read | Number of boards that have not topped out |
aliveGroupCount | uint16 | Read | Number of groups that have at least one board that hasn't topped out |
targetCount | uint16 | Read | Number of boards targeted by the current board |
targets | List<uint16> | Get/Set | A list of board indexes that corresponding to who the board is targeting |
attackerCount | uint16 | Read | Number of boards that are targeting the current board |
attackerCount | List<uint16> | Get | A list of board indexes that correspond to who is targeting the board |
boardIsLocal | bool | Read | If true, the current board is controlled by a user of the system |
boardIsLead | bool | Read | If true, the current board is in focus in the view |
localPlayerIsAlive | bool | Get | Returns true when a local player is alive; should be used only for visuals and not for logic |
resultLinesCleared result.linesCleared |
byte | Read | Number of line clears the piece placement formed |
resultSpecialType result.specialType |
enum | Read | Is one of these values:
|
resultIsMinorTwist result.isMinorTwist |
bool | Read | Denotes if a minor twist was performed |
resultIsMajorTwist result.isMajorTwist |
bool | Read | Denotes if a major twist was performed |
resultGarbageCleared result.garbageCleared |
byte | Read | Number of line clears the piece placement formed involving rows that came from the Rise Meter |
resultIsAerial result.isAerial |
bool | Read | Denotes if this was a line clear that was performed over a bunch of space right underneath |
resultIsReaper result.isReaper |
bool | Read | Denotes if this was a piece placement that was saved from block out or involved a line clear at the top |
resultAttackPower result.attackPower |
uint16 | Read/Write (situational) |
A number that can be used to create attacks; can only be written to in the attack commit rules |
resultBaseAttack result.baseAttack |
uint16 | Read/Write (situational) |
A number that is used for the base attack statistic which appears outside of 2-player, not used otherwise; can only be written to in the attack commit rules. There is no way to validate base attack calculation so do not forget to set this or it will show as "0.00 base atk/min" |
resultAttackSent result.attackSent |
uint16 | Read | A number that is equal to attack power minus garbage burned; cancel amount is added to this number when it is used |
resultGarbageCancel result.garbageCancel |
uint16 | Read/Write (situational) |
A number that can be used to bypass garbage to send attack; can only be written to in the attack commit rules |
allClearCount | uint32 | Read | Number of all clears or all clear minis that were performed |
combo | int32 | Read | Current combo count; is -1 when no line clears occurred |
backToBack | int32 | Read | Current Back-to-Back chain; is -1 when there is no chain |
previousBackToBack | int32 | Read | The Back-to-Back chain that existed before the placement; when this is greater than backToBack, it means a chain just ended |
thereIsAPlayerInDanger | bool | Get | Checks if any alive player is near the top |
musicLevel | int32 | Read/Set | When assigned to, the music will change. Set to -1 to turn off the music. If duel is set to true, it will be automatically set and setting this variable has no effect. The following levels can be used:
|
gravityInterval | double | Read/Write | Board timing for how long it takes for the current piece to move down one row; writing to this will reschedule the Gravity event |
spawnDelayMin spawnDelayMinimum |
double | Read/Write | Minimum time to summon the next piece; writing to this will reschedule the Spawn events |
spawnDelayMax spawnDelayMaximum |
double | Read/Write | Maximum time to summon the next piece; writing to this will reschedule the Spawn events |
lineClearDelayMin lineClearDelayMinimum |
double | Read/Write | Board timing for how long it takes for the stack to compress after line clears; writing to this will reschedule the LineFall and Spawn events |
lineClearDelayMax lineClearDelayMaximum |
double | Read/Write | Board timing for how long it takes for the stack to compress after line clears; writing to this will reschedule the LineFall and Spawn events |
asdMin asdMinimum |
double | Read/Write | Minimum allowed Auto-Shift Delay; default is 0; is applied after the placement event |
asdMax asdMaximum |
double | Read/Write | Maximum allowed Auto-Shift Delay; default is infinity; is applied after the placement event |
aspMin aspMinimum |
double | Read/Write | Minimum allowed Auto-Shift Period; default is 0; is applied after the placement event |
aspMax aspMaximum |
double | Read/Write | Maximum allowed Auto-Shift Period; default is infinity; is applied after the placement event |
lockDelay | double | Read/Write | Board timing for how long it takes for the current piece to automatically lock down; writing to this will reschedule the Lock event |
garbageIntake | string | Read/Write | Set to 'individual' to have the Rise Meter take in one garbage at a time or the garbageIntakeLimit, whichever is lower Set to 'conglomerate' to have the Rise Meter take in as much as the garbageIntakeLimit |
attackIntakeDelay attack.intakeDelay garbageIntakeDelay garbage.intakeDelay |
double | Read/Write | Determines how long it sits in the intake section of the Garbage Meter before it can be taken in by the Rise Meter |
attackTravelTime attack.travelTime |
double | Read/Write | Determines how long it takes for the attack to travel to its destination; writing to this will reschedule the GarbageAccept event |
garbageLimit | int32 | Read/Set | Determines how many lines of garbage the Garbage Meter can hold; setting this will immediately toss any excess, so do not set to 0 if you're recalculating |
garbageIntakeLimit | byte | Read/Set | Determines how many lines of garbage can enter in every piece placement; setting this will immediately "unprime" pending garbage, so do not set to 0 if you're recalculating |
garbageSpawnDelay | double | Read/Write | Determines how long the Rise Meter waits before applying rows from the Garbage Meter; has no effect if the Rise Meter already has rows |
groupCount | uint16 | Read | Number of groups |
aliveEnemyCount | uint16 | Read | Number of opponents that have not topped out |
koHistory | List<List<object>> | Get | This is an expensive variable to translate; use the below variable if you are only interested in the latest KO to increase performance, otherwise use this to get a full list of the entries across all rounds in the below format |
latestKO | List<object> | Get | This translates the last KO that occurred into data you can use; if no one has been KO'd yet, this will be null, otherwise you will get a tuple that gives you the board index that was KO'd, the board index of the killer (or the KO'd if suicide), the round it occurred in, and the roundTime it occurred |
stack boardStack board.stack |
List<byte> Dictionary<uint16, byte> |
Get/Set | Returns a list of bytes that represent the stack of the current board; this is an expensive variable to retrieve and overwrite; data is read from bottom to top, left to right (for example, the 7th cell in the 3rd row would be index 26 (2 * 10 + 6) or Lua index 27) To apply changes, you must set it back; when providing a dictionary, the keys represent cell positions; this allows you to overwrite parts of the stack without having to retrieve the current data Refer to this data format for each byte |
previewCount | byte | Read/Write | Controls the number of next piece previews visible to the player; currently the limit is 7 |
nextPieces | List<string> | Get/Set | Returns a list representing the order of the pieces as their name and orientations in the Next Queue (formatted as "name:orientation"); to apply changes, you must set it back; if the queue is too short, it will be filled up with the current bag |
bag | List<string> | Get/Set | A list representing the piece and orientation combinations the piece generator can use; to apply changes, you must set it back; WARNING: This will be empty in replays |
attackLineCount attack.lineCount garbageLineCount garbage.lineCount |
uint16 | Read/Write | The number of lines in the attack or garbage |
boardWidth board.width |
byte | Read/Set | Number of columns in the grid |
boardHeight board.height |
byte | Read/Set | Number of rows in the grid, or more specifically, the top out height |
attackMutateChance attack.mutateChance garbageMutateChance garbage.mutateChance |
byte | Read/Write | Determines the chance the garbage will change columns from the previous garbage; 0 is guaranteed to match column, 200 is guaranteed to change column; for a 10-wide board, 180 will give each column equal chance |
attackCheeseRate attack.cheeseRate garbageCheeseRate garbage.cheeseRate |
byte | Read/Write | Determines the chance the garbage will change columns for each generated row; 0 is guaranteed to match column, 200 is guaranteed to change column; for a 10-wide board, 180 will give each column equal chance |
garbageAsynchronousIntake | bool | Read/Write | If true, the player will be able to play while the Rise Meter is pushing rows |
garbageLineClearIntake | bool | Read/Write | If true, the Garbage Meter will push garbage regardless of if the player cleared a line |
currentPieceName | string | Read | Returns the name of the piece currently in control |
rotationState | int32 | Read/Write | The orientation index of the current piece; note that you can no-clip by setting it |
pieceX | sbyte | Read/Write | The X coordinate of the origin of the current piece; note that you can no-clip by setting it |
pieceY | sbyte | Read/Write | The Y coordinate of the origin of the current piece; note that you can no-clip by setting it |
reaperState | bool | Read | Is true when the player has escaped block out |
resultPieceName result.pieceName |
string | Read | The name of the piece that was placed |
resultBuildIdentifier result.buildIdentifier |
enum | Read | Is one of the named builds:
|
risePeriod | double | Read/Write | Board timing for how long it takes for each line in the Rise Meter to appear; writing to this will reschedule the GarbageIntake event |
moveResets | uint16 | Read/Write | Number of times the player can move a piece on the stack before it auto-locks; set to 0 to disable lock resets for movement and 10000+ to give infinite lock resets |
rotationResets | uint16 | Read/Write | Number of times the player can kick a piece before it auto-locks; set to 0 to disable lock resets for rotation and 10000+ to give infinite lock resets |
resettableGravity | bool | Read/Write | When enabled, moving a piece off a ledge will let it hover for the full gravityInterval |
holdLimit | uint16 | Read/Write | Maximum number of times the player can move pieces in and out of the Hold queue |
allowHardDrop | bool | Read/Write | Allows the use of the Hard Drop input; if not allowed, the Hard Drop Fallback is used |
allowSonicDrop | bool | Read/Write | Allows the use of the Sonic Drop input; if not allowed, Sonic Drop inputs will become Soft Drop inputs |
allowPassthrough | bool | Read/Write | When disabled, attacks will not only burn lines in the garbage meter, but lines that will be inserted in the garbage meter as well; note that if you send 0 line attacks to opponents that send attacks with lines when passthrough is disabled, the attack receive event will NOT trigger for the 0 line attacks |
enableGhost | bool | Read/Write | Reveals or hides the location a piece will end up when Hard Drop is used |
groupIndex group.index |
uint16 | Read | Returns the index of the group |
boardIndex board.index |
uint16 | Read | Returns the index of the board |
Mode settings are global constants. Mode settings are readonly and are initialized as a number set by the host before the mode begins. To expose this in the lobbies and in the AI Versus setup screen, you need a block of JSON like this in the settings portion. Supported types for min and max are numbers only. If you want the UI to display something other than numbers, you can include an enumValues attribute.
name
is the ruleset variable name, accessible through mode.settings.name
or simply settings.name
uiName
is what appears in the menus
min
is the minimum value it can be; can be a integer (up to 64-bit signed or unsigned), or a double-precision floating-point number
max
is the maximum value it can be
default
is the default value it will start with in the menus; this is also the value the AI use
enumValues
is an array of enum strings; these strings can be referred to in the rules
"settings": [ { "name": "goalPoints" "uiName": "Goal Points" "min": 1 "max": 255 "default": 3 } ]
Board preferences are unique to each player. Board preferences are readonly and are initialized as a number set by the player before the mode begins. They are structured in JSON the same way as mode settings. All board preferences must be prefixed with board.preferences.
or simply preferences.
.
"preferences": [ { "name": "targetStyle" "uiName": "Target Style" "description": "(explanation on how this works)" "enumValues": [ "quickAccess", "cycle" ] } ]
{ // Check target style "type": "if", "statement": "$board.preferences.targetStyle == quickAccess", "onTrue": [ { "type": "target", "style": "AllAttackers" } ] }
Mode variables are global variables and can be accessed anywhere. Mode variables are initialized as null before the mode begins. All mode variables must be prefixed with mode.
.
Name | CLR Type | Initial Value | Description |
---|---|---|---|
goalPoints | byte | 1 | A number that is used to determine the winner(s). When it is greater than 1, a bar at the bottom of the screen will describe which groups are winning (if anyone is by themselves, they are technically their own group). You will need to check if any group's points is at least this before using the endMode rule. |
goalLead | byte | 1 | A number that is used to describe how much of a lead a group needs to be winning by before they are declared the winner. This makes the middle section of the points bar wider, and the goal points will appear to be increasing if a lead is not maintained. Refer to the playerBreakdownRules in the Eliminations Ruleset to see how to implement. |
duel | bool | false | Changes the behavior of musicLevel. |
button1Text | string | null | Text that says what the General1 input does |
button2Text | string | null | Text that says what the General2 input does |
button3Text | string | null | Text that says what the General3 input does |
button4Text | string | null | Text that says what the General4 input does |
Group variables are unique to each group and can be accessed by member boards. They persist across rounds. Group variables are initialized as null before the mode begins. All group variables must be prefixed with group.
.
Name | CLR Type | Initial Value | Description |
---|---|---|---|
points | byte | 0 | A number that is used to determine who is winning. This also rewards spirit points at the end, based on goalPoints. |
koCount | uint32 | 0 | Number of KOs this group has accrued across all its members. |
Board variables are unique to each player. They persist across rounds. Board variables are initialized as null before the mode begins. All board variables must be prefixed with board.
.
Name | CLR Type | Initial Value | Description |
---|---|---|---|
displayText | List<string> | null | Text that appears on the right side of the board; if you provide anything other than a list it will be formatted as one line of text; set to null to not add any text; for performance, only create the list when the number of lines change, and use the update rule or operator to change the text inside |
modeBar1 | float | 0f | A number that fills up the first meter on the right. -infinity is hidden, 0.0 is empty, 1.0 is filled once, 2.0 is filled twice, etc. |
modeBar1Colors | Color[] | <the 12 rainbow colors but with rose replaced with white> | An array of colors used for each fill. It is recommended not to set this very often. When a list of strings is provided it will be parsed as hex values on the next frame, and you cannot modify it afterward. If the value is null it will just be a single white. |
modeBar2 | float | -infinity | A number that fills up the second meter on the right. -infinity is hidden, 0.0 is empty, 1.0 is filled once, 2.0 is filled twice, etc. |
modeBar2Colors | Color[] | <the 12 rainbow colors but with rose replaced with white> | An array of colors used for each fill. It is recommended not to set this very often. When a list of strings is provided it will be parsed as hex values on the next frame, and you cannot modify it afterward. If the value is null it will just be a single white. |
attackMultiplier | double | 1.0 | A number shown at the top of the mode bars (if it's not a number, it will be used as a string). |
level | int32 | 0 | This number is used to control the typeface of the attackMultiplier shown. If it's 0, the multiplier is shown as gray, otherwise it's one of modeBar1Colors. It gets bigger when level is at least 10. |
koCount | uint32 | 0 | Number of KOs since the beginning of the first round. |
Scope variables are temporary variables that are used to assist in computing. Scope variables are cleared on every event except for the user defined events. Due to how easy it is to accidentally create scope variables, the game chooses to throw an exception if you try to use the assign rule instead of the assignNew rule when the variable is uninitialized.
Statistics are unique to each board. They are automatically managed by the game and are readonly. To access them, either do $board.statistics[Lines]
or $board.statistics;Lines;[]
. You can also refer to them by ID. They are all of type uint32. If you try to add to, remove from, or update $board.statistics
itself, it will clone itself and turn into a dictionary.
Enum Name | ID | Description |
---|---|---|
Attack | 0 | The total number of attack power created from the attack commit event |
AttackSent | 1 | The total number of attack created from the attack commit event that did not get cancelled by the garbage meter |
BaseAttack | 12 | The total number of base attack created from the attack commit event |
GarbageClears | 6 | The total number of line clears that came from the Rise Meter |
Inputs | 4 | The total number of key presses accepted by the game |
Lines | 5 | The total number of line clears |
Pieces | 7 | The total number of pieces placed |
Power | 8 | The current power level of the board; currently unused |
AccumulatedPower | 9 | The total power ever gained, not including losses |
PeakPower | 10 | The highest power level ever achieved in the round |
NOTE: Need to jot down this section.
Three types of collections are supported. The add, subtract and update rules as well as the add, subtract and update operators can modify them.
All expressions are in postfix. You can use a mix of constants, variables, and operators, all separated by semicolons (use \\; to use actual semicolons). All variable references must be appended with $. All strings must be wrapped in single-quotes. The game will check for illegal expressions, such as when you pop more than you push.
At some point, infix expressions will be supported, but currently there's too many operators at the moment to make it worth implementing at the moment.
NOTE: Will need to work on operators involving collections
A;B;+
Adds A and B and pushes the result.
If A is a list, B will be appended to it instead. If A is a dictionary, B will be used as a key-value pair if it's a list of 2 elements, or the entirety of B will be inserted if it's a dictionary. This is an operator that can modify the original value, so be careful with this operator.
For performance, the update operator is more efficient than creating a temporary list or dictionary, but if B has been created beforehand there's no difference.
A;B;-
Subtracts B from A and pushes the result
When doing this on two strings, a lexical comparison is performed. The result will be positive if A follows B, negative if A precedes B, and zero if they are in the same position in the sort.
If A is a list, B will be removed from it instead. If A is a dictionary, B will be used as a key to remove, or all the keys of B will be removed from A if B is a dictionary. This is an operator that can modify the original value, so be careful with this operator.
If you were looking to remove portions of a string, use the string remove operator instead.
A;B;*
A;B;•
Multiplies A and B and pushes the result
A;B;/
A;B;÷
Divides A by B and pushes the result
A;B;//
Divides A by B, truncates off the remainder, and pushes the result
A;B;%
Divides A by B and pushes the remainder
A;B;**
A;B;pow
A;B;ⁿ
Raises A to the B power and pushes the result
A;B;&
Pushes an integer that shares the same 1's as A and B
A;B;|
Pushes an integer that has 1's in A or B
A;B;^
Pushes an integer that has 1's in A or B but not in both
A;~
Inverts all the bits in A and pushes the result
For performance, A;~;~;~
will be automatically converted to A;~
A;B;<<
Moves all the bits in A B times to the left, making the number larger
The behavior is dependent on integer type. All integers below 32-bit are converted to 32-bit. For signed integers, an arithmetic shift is performed (sign is preserved), and for unsigned integers a logical shift is performed.
A;B;>>
Moves all the bits in A B times to the right, making the number smaller
The behavior is dependent on integer type. All integers below 32-bit are converted to 32-bit. For signed integers, an arithmetic shift is performed (sign is preserved), and for unsigned integers a logical shift is performed.
A;B;&&
A;B;and
Pushes true if both A and B are truthy, else pushes false
A;B;||
A;B;or
Pushes true if A or B are truthy, else pushes false
A;B;^^
A;B;xor
Pushes true if A or B are truthy but not both, else pushes false
A;!
A;not
For performance, A;!;!
will be automatically converted to A;!!
and A;!!;!
will be automatically converted to A;!
Pushes false if A is truthy, else pushes true
A;!!
A;‼
Pushes true if A is truthy, else pushes false
For performance, A;!!;!!
will be automatically converted to A;!!
A value is considered truthy if it is not null/nil/undefined, not zero, not false, not an empty string, and not an empty collection
A;B;==
Pushes true if A and B are considered equal, else pushes false
When comparing a string and a number, the length of the string is used
A;B;!=
Pushes false if A and B are considered equal, else pushes true
When comparing a string and a number, the length of the string is used
A;B;>
Pushes true if A is considered greater than B, else pushes false
When comparing strings, this returns true if A comes after B lexically
When comparing a string and a number, the length of the string is used
A;B;<
Pushes true if A is considered less than B, else pushes false
When comparing strings, this returns true if A comes before B lexically
When comparing a string and a number, the length of the string is used
A;B;>=
A;B;≥
Pushes true if A is considered greater than or equal to B, else pushes false
When comparing strings, this returns true if A comes after B lexically or is in the same position lexically
When comparing a string and a number, the length of the string is used
A;B;<=
A;B;≤
Pushes true if A is considered less than or equal to B, else pushes false
When comparing strings, this returns true if A comes before B lexically or is in the same position lexically
When comparing a string and a number, the length of the string is used
A;B;s+
Puts A in front of B and pushes the combined string
Numbers are given default format
A;B;s-
Removes any occurrences of B in A and pushes the resulting string
Numbers are given default format
A;:0.00
Turns A into a string and pushes it; everything after the colon is considered the format
Refer to .NET formatting for valid formats; they all use Current Culture (the user's system settings), so do not use the result in comparisons.
There is also a special format A;:time
, which will display the number as a timer "0:00.000", displaying hours only when needed. It also does not display decimal digits if the number is an integer.
A;B;[]
Pushes the value from A at index or with key B
A;B;C;@
Modifies A by assigning key B with value C, then pushes A
If A is a dictionary, key B will be set to C. If A is a list, index B will be set to C if B is a valid integer index.
l[]
Pushes a new list
d[]
t[]
Pushes a new dictionary/table
A;countof
A;countOf
Gets the number of elements in A if A is a list, or the number of key-value pairs in A if A is a dictionary/table, then pushes that number to the stack
A;B;C;?
Pushes B if A is truthy, else pushes C
Due to the nature of expressions, B and C are fully evaluated, so for complex ternary, use the if rule for performance
Statements are in infix and produce booleans. At first glance, they seem to have the same operators as expressions, but they operate fundamentally differently. No assignment or manipulation can be performed, and thus no Lua is allowed. You can control evaluation order using parentheses.
If you want to use postfix expressions, surround the postfix in curly braces (not technically always required but highly recommended).
$variable1 < {$board.variable2;5;+}
A=B
A==B
Returns true if A and B are considered equal, else returns false
When comparing a string and a number, the length of the string is used
A!=B
Returns false if A and B are considered equal, else returns true
When comparing a string and a number, the length of the string is used
A>B
Returns true if A is considered greater than B, else returns false
When comparing strings, this returns true if A comes after B lexically
When comparing a string and a number, the length of the string is used
A<B
Returns true if A is considered greater than B, else returns false
When comparing strings, this returns true if A comes before B lexically
When comparing a string and a number, the length of the string is used
A>=B
A≥B
Returns true if A is considered greater than or equal to B, else returns false
When comparing strings, this returns true if A comes after B lexically or is in the same position lexically
When comparing a string and a number, the length of the string is used
A<=B
A≤B
Returns true if A is considered less than or equal to B, else returns false
When comparing strings, this returns true if A comes before B lexically or is in the same position lexically
When comparing a string and a number, the length of the string is used
A&B
A and B
Returns true if both A and B are truthy, else returns false
A|B
A or B
Returns true if A or B are truthy, else returns false
A^B
A xor B
Returns true if A or B are truthy but not both, else returns false
!A
not A
Returns false if A is truthy, else returns true
A
Returns true if A is truthy, else returns false
A value is considered truthy if it is not null/nil/undefined, not zero, not false, not an empty string, and not an empty collection
The event context is a special state that is shared across rules. The nature of the values set depends on the events and rules being ran.
The board stack is a highly specialized collection of bytes. When getting and setting board data, it will be in this format, from bottom to top, left to right:
00 05 00 00 00 00 03 03 00 00
05 05 05 00 00 03 03 09 09 09
2E 2E 2E 10 2E 2E 2E 2E 2E 2E
2E 2E 2E 2E 2E 2E 2E 2E 2E 10
The right digit is the mino color. When it is 0, the cell is interpreted as being empty. 1 is red, 12 is rose, 13 is white, 15 is black, etc.
The left digit is the metadata, which is a set of 4 flags. Add numbers together to get desired cell state:
2E,2E,2E,2E,2E,2E,2E,2E,2E,10,2E,2E,2E,10,2E,2E,2E,2E,2E,2E,
05,05,05,00,00,03,03,09,09,09,00,05,00,00,00,00,03,03,00,00
[46,46,46,46,46,46,46,46,46,16,46,46,46,16,46,46,46,46,46,46,5,5,5,0,0,3,3,9,9,9,0,5,0,0,0,0,3,3,0,0]
The above array is what you'll receive when getting the board stack.
You can also send attacks that contain specialized rows. Here's an example postfix that creates a row:
l[];l[];1;+;2;+;3;+;4;+;5;+;6;+;7;+;8;+;9;+;10;+;+
There are several rules that can call Lua 5.2 functions. These reside in Lua scripts that exist in the Lua folder in the game directory.
Lua support is powered by the MoonSharp plugin. You can define multiple functions in a Lua script and be able to reference them directly in rulesets.
Supported CLR Types (used by the game):
Supported Lua Types:
NOTE: Will need to experiment more with Lua scripting to give more information.
There are some differences in this interpretation of Lua, as it is sandboxed:
Because the game is not framerate-based, it is important that everyone is in sync. All board events are time-driven, so if you change a variable that reschedules an event, a desync might occur and can potentially cause a "Time cannot go backwards" exception (shouldn't happen by the way so report it if it does). The game will try its best to prevent these situations but there's a lot of places to plug up. Your best opportunities to change board timings are by changing them in:
If performance is low, or players are disconnecting in the middle of your custom ruleset, consider any synchronized variables that are constantly being assigned to. If synchronized board variables are used to control board timings, check if the value is actually different, since the ruleset is allowed to use synchronized variables as a sort of event-type value (think of a switch statement). The reason frequent assignment causes performance issues is because of the replay protocol causing all boards to savestate for playback. You can get a visual indicator of how bad it is by viewing a replay and seeing how many markers appear on the seek bar.
Adding a list to another list will make a multi-dimensional jagged list. If you want to append the contents of a list to another you must add each element individually.
When declaring the winner in an eliminations style battle, you need to account for latency. First you check if alivePlayerCount is no more than 1. If only 1 person has not topped out, wait until their gameTime exceeds everyone else's. If everyone is topped out, loop through all the boards and solve for the one with the longest gameTime.
Discord - AgStarRay
Click here for AgStarRay's YouTube channel
Twitter - @AgStarRay