Skip to content

Instantly share code, notes, and snippets.

@TrueBrain
Last active January 27, 2022 20:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TrueBrain/58a388d5e396bba3ae3ec721d3d5fca3 to your computer and use it in GitHub Desktop.
Save TrueBrain/58a388d5e396bba3ae3ec721d3d5fca3 to your computer and use it in GitHub Desktop.
TrueGRF custom callback language

TrueGRF uses a custom (RPN-based) language.

RPN (See: https://en.wikipedia.org/wiki/Reverse_Polish_notation), also known as postfix language or stack-based language, is used as NewGRFs have a limit subsets of supported operations. RPN best matches NewGRFs while still being a slightly higher language than NFO is.

Integer-based

As NewGRF is integer-based, so is this language. This is not so much a rule of this language, more a consequence of the rule NewGRF created.

Variables

Variables are normally local scoped (read: uses a temporary register). There is no function-scoping; you can set a variable in one function, and read it in the next.

There are a few namespaces to change the scope of a variable:

  • industry: - get variables from the current industtry.
  • town: - get variables from the related town.
  • func: - function storage (action2 set-ids).
  • cb: - like functions, but specific for callbacks.
  • ctt: - get the id of a cargo label (from the Cargo Translation Table).

result and for some callbacks result: are special, to indicate the result value of a cb:.

Local variables and industry:storage: are dynamically assigned to a register slot. That is to say, as soon as you use a new local variable or a new industry:storage: variable (for example industry:storage:myvariable) it is automatically assigned the next free register slot. You do not have to worry about this, and this is all handled for you.

Dictionaries

Dictionaries are suported, and the keys and values are always integers. The [] operator looks up the index from the dictionary.

 mylist 2 [] 0 =

Adds the key "2" to the list "mylist" with the value "0".

If you use a dictionary in a loop, it will iterate the keys. You can ge the value by using it as key on the dictionary again:

 key mylist loop{ 
   value mylist key [] =
 }

Functions

Functions never return anything. Use a local variable if you want to talk between functions.

The body is created by using def{ and is closed with a }.

Loops

Loops only work on static information; in other words, they work on anything that can be done before the GRF is actually created. To help with loops, there is a "range" operator, that creates a list of a range. It comes in four variants: (..], (..), [..) or [..]. The first and last characters indicate if that side should be inclusive [ / ] or exclusive ( / ). For example:

 1 10 (..)

returns a list with the elements 2, 3, 4, 5, 6, 7, 8, 9.

A loop begins with a variable to change for each iteration, and is followed by a body to iterate over. For example:

 i 1 10 [..] loop{
   /* do something with i here. */
 }

If-statements

If-statements work in a simple way: the first item on the stack is the "true" value, the second is the "false" value. Example:

 10
 11
 1 2 >

Means: if (1 > 2) 10 else 11.

Whitespaces

All newlines and any spaces more than one are fluff, and meant to increase readability. They have absolutely no meaning what-so-ever from a language point of view. This is also the reason // comments are not allowed. /* */ comments however are.

Types

To ensure we are using the same persistent registers every time, order of variable definition is important. Additionally, NewGRFs have both signed and unsigned operations. To solve both issues, we need to define each used variable in a type block first.

Changing the order in this list will most likely break savegames that upgrade your NewGRF (read: don't change the order after initial release). If you want to remove a variable, just add a dummy one in return.

There are several types of type-blocks:

  • const: doesn't consume registers, and all need to resolve on compile-time. These are "write-once" variables, as in: you can write the const to it once.
  • local: variables that can be used locally, and lose their value every time a new callback comes in.
  • industry:storage: variables that need to be stored in the persistent industry storage.
  • town:storage: variables that need to be stored in the persistent town storage of the town related to the current industry.

There are several types available:

  • dict: [const only] a key->value storage.
  • cargodict: a key->value storage, where the key is cargo. This consumes 64 registers if used in NNN:storage.
  • list: a value list. Last item on stack indicates the length of the list.
  • integer: a value.
  • iterator: [local only] used as variable for loops.

Examples:

const type{
  const_dict dict
}

local type{
  local_list 12 list
  local_variable integer
  i iterator
}

industry:storage type{
  persistent_cargo_dict cargodict
  persistent_variable integer
}
/*
Type definition has to be on top.
*/
const type{
level_requirements dict
accept_cargo_types cargodict
supply_requirement integer
prod_cargo_types cargodict
}
local type{
total_delivered integer
i iterator
level iterator
cargo iterator
}
industry:storage type{
base_prod_factor integer
current_production_level integer
supplied_cycles_remaining_cargo cargodict
num_supplies_delivered 27 list
}
/*
These next variables can be templated in TrueGRF, and are generated (by TrueGRF) different for each industry.
These values are from the FIRS Steeltown Bulk Terminal industry, and are meant as example.
*/
level_requirements 16 [] 150 =
level_requirements 16 5 * [] 300 =
accept_cargo_types ctt:FOOD [] 1 =
accept_cargo_types ctt:POTA [] 1 =
accept_cargo_types ctt:CHLO [] 1 =
supply_requirement 8 =
prod_cargo_types ctt:MNO2 [] 19 =
prod_cargo_types ctt:RUBR [] 16 =
prod_cargo_types ctt:PLAS [] 16 =
prod_cargo_types ctt:FECR [] 14 =
prod_cargo_types ctt:ALUM [] 11 =
/*
The following is a replication of what FIRS is doing for a primary industry.
See: https://github.com/andythenorth/firs/blob/4.4.0/src/templates/produce_primary.pynml
*/
/* Get the total amount of supplies delivered in last 27 production cycles. */
func:produce_total_supplies_delivered def{
total_delivered 0 =
i 0 26 [..] loop{
total_delivered total_delivered num_supplies_delivered i [] + =
}
}
/* Calculate current production level, based on delivered supplies. */
func:produce_calculate_current_production_level def{
func:produce_total_supplies_delivered ()
current_production_level 100 =
level level_requirements loop{
current_production_level
level_requirements level []
current_production_level
level total_delivered supply_requirement / >
=
}
}
/* Set the number of supplied cycles remaining per cargo - used to display 'supplied' (or not) in the industry window. */
func:update_supplied_cycles_remaining_per_cargo def{
cargo accept_cargo_types loop{
supplied_cycles_remaining_cargo cargo [] supplied_cycles_remaining_cargo cargo [] 1 - 0 max =
}
}
/* Shift the array of supplies_delivered values one place to the left, and zero the last entry. */
func:produce_256_ticks_shift_supplies_delivered def{
i 0 25 [..] loop{
num_supplies_delivered i [] num_supplies_delivered i 1 + [] =
}
num_supplies_delivered 26 [] 0 =
}
/* On arrival of supplies, push the amount to perm storage, then clear from stockpile. */
cb:production_cargo_arrival def{
cargo accept_cargo_types loop{
num_supplies_delivered 26 [] num_supplies_delivered 26 [] industry:cargo_incoming_waiting cargo [] + =
supplied_cycles_remaining_cargo cargo []
28
supplied_cycles_remaining_cargo cargo []
industry:cargo_incoming_waiting cargo [] 0 >
=
}
/*
Update the production level immediately, so that production level text immediately updates in industry window.
Production won't actually increase until next 256 tick production cycle.
*/
func:produce_calculate_current_production_level ()
/*
Most callbacks expect an integer as return value, but some are more complex. Like this (and the next).
Don't worry about not setting "result:outputs"; if you don't touch it, it remains empty.
*/
cargo accept_cargo_types loop{
result:inputs cargo [] industry:cargo_incoming_waiting cargo [] =
}
}
/* On 256 ticks, if supplied, produce extra output cargo at appropriate multiplier. */
cb:production_every_256_ticks def{
func:produce_calculate_current_production_level ()
func:update_supplied_cycles_remaining_per_cargo ()
func:produce_256_ticks_shift_supplies_delivered ()
cargo prod_cargo_types loop{
result:outputs cargo [] prod_cargo_types cargo [] industry:production_level base_prod_factor current_production_level * * * 16 16 100 * * / =
}
}
cb:production_initial def{
base_prod_factor 16 =
/* Initial production level. Can be changed with cheats. */
result:value 16 =
}
cb:production_change_monthly def{
result:value 0 =
}
cb:production_change_random def{
result:value 0 =
}
/*
Similar to the above, but based on 2TallTyler's Lumbarjack Industries, the Coal Mine.
See: https://github.com/2TallTyler/lumberjack_industries/blob/main/src/coal_mine.nml
*/
func:apply_boost def{
result:inputs ctt:FUEL [] 3 =
result:outputs ctt:COAL [] 16 =
}
func:noop def{
}
cb:production_cargo_arrival def{
/* Ensure we never go over a cap of 4096. */
result:inputs ctt:FUEL [] industry:cargo_incoming_waiting ctt:FUEL [] 4096 - 0 max =
}
cb:production_every_256_ticks def{
func:apply_boost ()
func:noop ()
industry:cargo_incoming_waiting ctt:FUEL [] 3 >=
result:outputs ctt:COAL [] result:outputs ctt:COAL [] 16 + =
}
cb:production_initial def{
result 1 =
}
cb:production_change_monthly def{
result 0 =
}
cb:production_change_random def{
result 0 =
}
return {
//defaultToken: 'invalid',
keywords: [
"const", "local", "industry:storage", "type{", "def{",
],
typeKeywords: [
'cargodict', 'integer', 'iterator', 'list', 'dict'
],
operators: [
'=', '>', '<', '>=', '<=', '==', '!=', "[]", "()"
],
// we include these common regular expressions
symbols: /[=><!~?:&|+\-*\/\^%\[\]\(\)]+/,
// The main tokenizer for our languages
tokenizer: {
root: [
[/[a-z_$][\w:$]*{?/, { cases: { '@typeKeywords': 'keyword',
'@keywords': 'keyword',
'@default': 'identifer' } }],
[/}/, "keyword"],
{ include: '@whitespace' },
[/@symbols/, { cases: { '@operators': 'operator',
'@default' : '' } } ],
[/0[xX][0-9a-fA-F]+/, 'number.hex'],
[/\d+/, 'number'],
],
comment: [
[/[^\/*]+/, 'comment' ],
[/\/\*/, 'comment', '@push' ], // nested comment
["\\*/", 'comment', '@pop' ],
[/[\/*]/, 'comment' ]
],
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/\/\*/, 'comment', '@comment' ],
],
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment