Ghoulboy, gnembon, Firigion, BisUmTo, replaceitem, altrisi.
If you have questions, these are the people to bother first of all.
This applies to everyone, including admins. The PR needs at least 2 reviews before it can be merged.
If you're gonna add a new function that manipulates blocks in the world with a player - utilisable function, you should first familiarize yourself with the existing code, so you know what to do right off the bat, but here is the tl;dr, in case it was too confusing:
-
Define the function below all the other defined functions (i.e, underneath the comment which says
//Command functions
). This helps with legibility of your code later on. -
Add the essential boilerplate code. This is necessary for the undo command to work properly with all commands:
-
Get the player as a variable.
se.sc
is a player_scoped app, but this is necessary for printing messages to that specific player.player=player()
-
Your function probably needs to access the player's selected points. This function will throw the appropriate errors if the player doesn't have the positions defined. NB: This is useful, as you can use these positions however you want.
[pos1,pos2]=__get_current_selection(player);
-
If you're setting blocks, you need to be able to undo that. Here's how:
-
When running your function, just use
set_block(pos, block, replacement, flags, extras)
to set the block in the world. -
Secondly, you have to save your command to the player history, so they can undo it. This is an O(1) operation, so don't worry about lag (If you don't understand that sentence, then don't worry about it either). To do this, just run the function
add_to_history('action_'+your_function_name, player)
and add that to the lang as a translation key, mapped to the en_us translation, which will be displayed to the player when they run/undo history
. This is so people can translate the function name when converting to their language. The rest of the undo business will be handled behind the scenes as long as you follow these steps.
-
-
-
If you need to set special blockstates or nbt values, you can do that with the
extra
argument ofset_block
. You can input a map with a key of'state'
with the value of the map of blockstates, or a key of'nbt'
, mapped to a map of the nbt. NB: You can simply input a map as the nbt, theencode_nbt
function is called inset_block
. -
If it's the first time you are adding commands, you need to know how the commands preprocessor work. You have detailed instructions for it in Command System.
-
Your additions should take flags into account. If you want your command to set blocks, there needs to be two versions of each command you add. One with flags, one with out them. For example, the fill command would look like this:
'fill <block>' 'fill <block> <replacement>'
then you need to copy those, and add the flags at the end:
'fill <block>' 'fill <block> <replacement>' 'fill <block> f <flag>' 'fill <block> <replacement> f <flag>'
Make sure your command processing function accepts a flags argument as the last parameter, and add that in the command syntax. For the versions without the flags, pass use
null
as theflags
argument.To access the flags as a map, run:
flags = _parse_flags(flags)
. Whenever you use theset_block()
function, put the flags from your command processing function as the second to last argument. The following flags currently exist for you to use:u no blockupdates (handled by set_block) w waterlog blocks that get placed inside water (handled by set_block) p only replace air blocks (handled by set_block) e copy/move entities as well b copy/move biomes as well (handled by set_block) a don't paste air (handled by set_block) h create hollow shapes d "dry" out the placed set of blocks (remove water and waterlogged, handled by set_block) s keep block states of replaced block, if new block matches (handled by set_block) g when replacing air or water, some greenery gets repalced too (handled by set_block) l when registering a brush with this flag, the brush will trace for liquids as well as blocks
Biomes are handled by the
set_block
function, but you need to input the previous biome as a map in theextra
argument:{'biome' -> biome}
, where the variablebiome
is the biome at the position you copied from. No need to handle undoing,set_block
does that on its own.
If you are adding a new brush function, there is a simple syntax to follow to ensure it is compatible with the existing brush function utility. You must add your new function in the following fashion:
Add your function as an entry to the global_brush_shapes
map variable, with the key being the string name of your function,
and the value being a lambda function with (pos, args, flags)
as the arguments. This is where you can manipulate the
world in whatever way you see fit, and must call the add_to_history()
function (cos not all brush functions set blocks).
You can take whichever arguments you need from the args
variable as long as you specify them in the input command.
You must also add a translation key for the action to the lang file. Finally, it's encouraged that devs add a list of parameters
to global_brushes_parameters_map
to pretify the brush info
command output.
If this was too bulky and confusing too understand, here is a full example of the cube
function, which simply places
cubes:
-
First, we add an entry to the
global_brush_shapes
map variable which consists of the string'cube'
mapped to the block setting function:global_brush_shapes={ ... //brush entries 'cube'->_(pos, args, flags)->(//this can be like a template for new brushes as it's the simplest [block, size, replacement] = args; scan(pos,[size,size,size]/2, set_block(_, block, replacement, flags, {})); add_to_history('action_cube',player()) ), ... //more entries }
Here, we can see the lambda (
_(pos, args, flags)->
) which is called when you right click with the brush or use the/shape
command. -
We can see that we got three arguments:
[block, size, replacement] = args;
. What arguments the brush uses are defined in the argument signature. Here we have the default signature that includes thehelp
info (see (#command-system)) and the duplicate command accepting flags. In this example<replacement>
is a mandatory argument.base_commands_map = [ ... //commands ['brush cube <block> <size> <replacement>', _(block, size_int, replacement) -> brush('cube', null, block, size_int, replacement), [2, 'help_cmd_brush_cube', 'help_cmd_brush_generic', null]], ['brush cube <block> <size> <replacement> f <flag>', _(block, size_int, replacement, flags) -> brush('cube', flags, block, size_int, replacement), false], ... //more commands ]
If your brush can also be called as a shape, like it's the case for
cube
, add the corresponding set of commands by copying the lines above and replacingbrush
withshape
. -
We can also see that we ran the
add_to_history
function with'action_cube'
, not'cube'
. This is because we need to then add an entry to the translations map (global_default_lang
). This is so the action name can be translated to other languages when running the/undo history
command. Add your translation key to the map, mapped to the en_us translation:global_default_lang = { ... //other entries 'action_cube' -> 'cube', ... //more entries }
-
We finally add a list of the parameters the brush uses to
global_brushes_parameters_map
to be the display names when callingbrush info
:
global_brushes_parameters_map = {
... //other brushes
'cube'-> ['block', 'size', 'replace'],
... //more brushes
}
If you want to print a message as an output to the player, the easiest way is using the _print(player, message_id, ...extra_args)
function. This is important to be able to translate the message into other languages. Don't worry if you can't translate
your strings to other languages, but make sure that you follow these steps so that someone else can translate it. Input
the string in the format:'message_id' -> 'Your message',
into the map under the //Translations
comment, under all
the other output messages. Try to group your messages if all relate to the same function/command, so they are easier to
find when translating or using them. If the message requires variables to be put in (like a number, or player, etc), just
use %s
in the message to stand for that value, it will be taken care of by the rest of the app.
While the _print()
function is useful in most of the cases, there are some situations where you can't work by directly
printing the message to the player. For those cases, there are two auxiliary functions for languages that can be useful
in those situations:
_translate(message_id, ...extra_args)
Will give you the translated string directly, without even applying theformat()
to it, so you have full freedom over what you use it for_error(player, message_id, ...extra_args
Will send the player the message and then immediately exit the function as errored.
A message added with this method will appear in the default US English translation. If you want to translate to other
languages, you just need to create a JSON file with your language's id (e.g. it_it.json
), and add it into the se.data/langs
folder. When the file is in there, you'll be able to load it by using /se lang [lang id]
ingame. The app will
warn to the console if you try to load an incomplete language, including all missing keys so it's easier to translate.
While those are not available, it will use the default US English values for those.
In order to partially automate the help creation process, the command system in the app is different than the regular
Carpet's command system. It is generated in a separate map and pre-proccessed before being passed to Carpet. You have to
add your commands into the base_command_map
instead with the following format:
[command_for_carpet, interpretation_for_carpet, false] (will hide it from help menu)
[command_for_carpet, interpretation_for_carpet, [optional_arguments_since, description, description_tooltip, description_action]]
In words: the first two arguments are the usual key and value arguments in the __config():'commands'
map when defining custom commands
with scarpet the regular way. For example, they can be 'fill <block>'
and 'fill_with_blocks'
, where the latter is a user-defined
function. The third element in the list is what defines the representation of the command in the help menu. Use false
to hide the command
from the help menu (not every variation of a command needs it's individual entry), or see below for more info.
command_for_carpet
: it's the command "syntax" that will be passed to Carpet, the equivalent to the key in regular commands mapinterpretation_for_carpet
: it's how Carpet will process that command, the equivalent to value argument of a regular commands mapoptional_arguments_since
: the index of the first optional argument in the command out of all arguments given. optional arguemtns will be printed as[arg]
, and mandaroty ones as<arg>
For isntance, setting it to0
will make all arguments optional, while setting it to2
means the first two arguments are mandatory. Set it to-1
if all arguments are mandatory. This can be used to merge multiple commands into one help entry (for isntance,help [page]
represents both thehelp
andhelp <page>
commands).description
: The description of the command in the help menu. Must be a translation string (see Messages) or a lambda (if you need arguments in the translation,_()->_translate('string', ...args)
). (it can technically benull
, but the idea is to add a description)description_tooltip
: An optional tooltip to show when the user is hovering the description. Can benull
. If present, it must be a translation string or a lambda (if you need arguments in the translation)description_action
An optional action to run when the player clicks the description. Can benull
. If present, it must start with either!
(to specify it will run the action) or?
(to specify it will suggest the action). The command is automatically prefixed with/world-edit
(and a space)
The command suggestion will be derived from command_for_carpet
: everything before the first <
.
You should try to fit each entry in a single line (when viewed in the help menu) for proper pagination (until something is done).
Some items have built-in functionality, like the wand, but the user has the ability to add more items with actions associated with them (like brushes and angel block, for instance). If you want to add a new item functionality (say, building wand, for example), then you need to take care of a few things to prevent one item from having multiple actions associated with it:
- When registering an item for a new action, call
new_action_item(item, action)
. This will check if the item is already registered for another action and call_error()
if it is. Remember, thatexit
s, so there no need for an extrareturn()
call or anything, that function does the check for you. - When unregistering an item (not replacing it with a new one, like the wand does, actually deleting it), call
remove_action_item(item, action)
, which will check that the requested item indeed has that action registered to it. It alsoexit
s if it fails. For isntance,brush
uses it like this:
if(
action=='clear',
remove_action_item(held_item, 'brush'); // delete old item from global_items_with_actions
delete(global_brushes, held_item); // deregister item as brush
...
- For these two functions to work properly, any new action you add needs a few things:
- add your action's name to the
global_lang_keys
so it can be translated - add your action's code name to
global_item_action_translation_key
so it can be requested fromglobal_lang_keys
(for example, the angel block's code name is'angel'
, with a translation key'action_item_angel'
, which gives the text'angel block item'
) - if your new action has a default tool or item (like the wand does), add it to
global_items_with_actions
along with its action
- add your action's name to the
- If registering a new item to your action just replaces the previous one, rather than adding a brand new one (think wand vs brush: one
replaces the old wand with a new one, while the other adds a new brush, leaving the old ones untouched), take good care to manually
delete the old item from
global_items_with_actions
just before adding the new one. Angel block for example does it like this:
_set_angel_block_item() -> (
new_action_item(item = player()~'holds':0, 'angel'); // add new item
delete(global_items_with_actions, global_angel_block); // delete old one
global_angel_block = item; // register new item as angel block
...
This is extremely important so that old deregistered items can be reused for new actions by the player.
If you're doing something that changes the player's stored data, please ensure that:
- It doesn't break anything that was there, ensure you test it all. This includes setting blocks, undoing, redoing, etc.
- If you have questions about the code, ask the main contributors.