diff --git a/base/data/gm4/function/environment_check/score_on_non_player_entity.mcfunction b/base/data/gm4/function/environment_check/score_on_non_player_entity.mcfunction new file mode 100644 index 0000000000..94763d00a0 --- /dev/null +++ b/base/data/gm4/function/environment_check/score_on_non_player_entity.mcfunction @@ -0,0 +1,10 @@ +# Tests if non-player entities can hold scores. Default-config Spigot fails this test. +# @s = unspecified +# at unspecified +# run from modules which require this environment check + +# if there is no recent echeck result, re-run echeck +execute unless data storage gm4:environment_checks result.score_on_non_player_entity summon marker run function gm4:environment_check/score_on_non_player_entity/assign_score + +# return result +return run data get storage gm4:environment_checks result.score_on_non_player_entity.passed diff --git a/base/data/gm4/function/environment_check/score_on_non_player_entity/assign_score.mcfunction b/base/data/gm4/function/environment_check/score_on_non_player_entity/assign_score.mcfunction new file mode 100644 index 0000000000..3eba964f04 --- /dev/null +++ b/base/data/gm4/function/environment_check/score_on_non_player_entity/assign_score.mcfunction @@ -0,0 +1,17 @@ +# Assigns a score to this marker and reads it back. Kills the marker afterwards +# @s = test marker, just summoned +# at @s +# run from gm4:environment_check/score_on_non_player_entity + +# set up marker & run test +scoreboard players set @s gm4_data 1 + +# if the score is present, mark the check as passed +execute if score @s gm4_data matches 1 run data modify storage gm4:environment_checks result set value {score_on_non_player_entity: {passed: 1b}} + +# (optional) if no score is present, provide a probable cause message to the user +execute unless score @s gm4_data matches 1 run data modify storage gm4:environment_checks result set value {score_on_non_player_entity: {probable_cause: "This may be caused by the Paper/Spigot setting 'scoreboards.allow-non-player-entities-on-scoreboards=false'."}} + +# clean up marker +scoreboard players reset @s gm4_data +kill @s diff --git a/base/data/gm4/function/log.mcfunction b/base/data/gm4/function/log.mcfunction index 37194dc37c..3ec3e90268 100644 --- a/base/data/gm4/function/log.mcfunction +++ b/base/data/gm4/function/log.mcfunction @@ -4,6 +4,7 @@ execute if data storage gm4:log log{type:"install"} run tellraw @a[tag=gm4_show_ execute if data storage gm4:log log{type:"missing"} run tellraw @a[tag=gm4_show_log] [{"nbt":"log.module","storage":"gm4:log","color":"red"},{"text":" is disabled because ","color":"red"},{"nbt":"log.require","storage":"gm4:log","color":"red"},{"text":" is not installed."}] execute if data storage gm4:log log{type:"outdated"} run function gm4:outdated_logs/outdated_start execute if data storage gm4:log log{type:"version_conflict"} run function gm4:conflict_logs/version_conflict_start +execute if data storage gm4:log log{type:"environment_check_failed"} run tellraw @a[tag=gm4_show_log] [{"nbt":"log.module","storage":"gm4:log","color":"red"},{"text":" is disabled because the required environment check '","color":"red"},{"nbt":"log.environment_check","storage":"gm4:log","color":"red"},{"text":"' failed! ","color":"red"}, {"nbt":"log.probable_cause","storage":"gm4:log","color":"red", "interpret": true}] data remove storage gm4:log queue[0] execute store result score #log_size gm4_data run data get storage gm4:log queue diff --git a/base/data/gm4/function/post_load.mcfunction b/base/data/gm4/function/post_load.mcfunction index 750958366a..2a96d25527 100644 --- a/base/data/gm4/function/post_load.mcfunction +++ b/base/data/gm4/function/post_load.mcfunction @@ -2,3 +2,6 @@ execute unless data storage gm4:log queue[{type:"install"}] run data modify stor execute if data storage gm4:log queue[{type:"install"}] run data modify storage gm4:log queue append value {type:"text",message:'{"text":"[GM4]: Updates completed.","color":"#4AA0C7"}'} function gm4:log_wait + +# discard environment check test results (done post-load so players can overwrite echecks pre-load) +data remove storage gm4:environment_checks result diff --git a/docs/making-a-module.md b/docs/making-a-module.md index 124cfcee2f..aee45c6239 100644 --- a/docs/making-a-module.md +++ b/docs/making-a-module.md @@ -68,6 +68,9 @@ meta: - main # namespace assumed to be the module id - gm4_bat_grenades:tick # but one can be manually specified + # A list of checks to run when installing the module. May be omitted if not needed + environment_checks: [gm4:score_on_non_player_entity] + website: # A description. This should be a good summary of what this module adds or achieves, to get someone interested in this module description: Break apart gold and iron tools and weapons for materials. Attach this to a mobfarm to finally make use of those extra armour sets! diff --git a/docs/utilities.md b/docs/utilities.md new file mode 100644 index 0000000000..cc0e42f319 --- /dev/null +++ b/docs/utilities.md @@ -0,0 +1,149 @@ +# Utilities +Chances are, the tough problem you are trying to tackle has been solved by someone else working on Gamemode 4 long before you did. +This document will give you an overview of utilities and tools that have been developed over the years and which may help you when making a module. + +## Table of contents +* [Common Tags](#common-tags) + * [Blocks Tags](#block-tags) + * [Entity Tags](#entity-tags) +* [Environment Checks]() + * [Base Checks]() + * [Other Notable Checks]() + * [Creating New Checks]() +* [Upgrade Paths]() + +## Common Tags +Selecting blocks or entities with common properties is an error prone task, and subsequent updates often miss module-specific, hardcoded tags. +As such, `base` provides various tags that are maintained through updates and which should be prioritized over module-specific solutions. + +### Block Tags +Gamemode 4's default block tags are located at `base/data/gm4/tags/block/`. + +| Tag Name | Source | Description | +|---------------------|---------------------|------------------------------------------------------------------------------------------------------| +| #gm4:air | air.json | All air types. | +| #gm4:foliage | foliage.json | Naturally generating decoration on surfaces, which are easily broken, i.e. are washed away by water. | +| #gm4:full_collision | full_collision.json | All blocks that have a full-block collision box. | +| #gm4:no_collision | no_collision.json | All blocks without any collision, including air. | +| #gm4:replaceable | replaceable.json | Blocks that can be replaced by placing another block inside it, including air. | +| #gm4:water | water.json | Blocks that act as a water source. | +| #gm4:waterloggable | waterloggable.json | Blocks which can be water-logged. | + +### Entity Tags +Gamemode 4's default entity tags are located at `base/data/gm4/tags/block/`. + +| Tag Name | Source | Description | +|----------------------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| #gm4:boats | boats.json | All boat variations, including rafts. | +| #gm4:boss | boss.json | Bosses, namely the Ender Dragon and the Wither. | +| #gm4:chest_boats | chest_boats.json | All boat variations with a chest. | +| #gm4:hostile | hostile.json | Living entities that are hostile towards player by default. | +| #gm4:minecarts | minecarts.json | All minecart variations. | +| #gm4:neutral_hostile | neutral_hostile.json | Hostile living entities that may be, given the right conditions, neutral towards the player by default but turn hostile if provoked. | +| #gm4:neutral_passive | neutral_passive.json | Entities that are normally neutral, but turn hostile if provoked. | +| #gm4:neutral | neutral.json | Entities that may be neutral given the right conditions. | +| #gm4:non-living | non-living.json | Entities that are not considered living. | +| #gm4:passive | passive.json | Entities that are normally friendly and do not turn hostile, even if provoked. | + +## Environment Checks +The environment a data pack is installed into can affect its performance. +Servers may have command blocks disabled, or mods may change the way the game reacts to changes made by commands. +Not all users are aware of this, which can lead to rather frustrating debugging experiences. +To counteract this, modules can include environment checks which warn the user and block installation of the data pack if certain conditions are not met. + +Environment checks are included by specifying them by name (including namespace) inside a module's `beet.yaml`, e.g. +```yaml +id: gm4_double_doors +name: Double Doors +version: 1.2.X + +data_pack: + load: . + +require: + - bolt + +pipeline: + - gm4_double_doors.generate + - gm4.plugins.extend.module + +meta: + gm4: + versioning: + schedule_loops: [] + + # List of environment checks to include + environment_checks: [gm4:score_on_non_player_entity] + website: + description: Tired of clicking twice to open a double door? Annoyed by the fact that doors are only two blocks tall? This data pack automatically opens adjacent doors, making double doors fully functional! Additionally, bottom trapdoors of matching wood type placed above a door are opened alongside the door when it is opened by a player. + recommended: [] + notes: [] + modrinth: + project_id: Vx4zJ1Np + smithed: + pack_id: gm4_double_doors + video: null + wiki: https://wiki.gm4.co/wiki/Double_Doors + credits: + Creator: + - Bloo + Icon Design: + - venomousbirds +``` + +Multiple checks may be included and are executed in-order: +```yaml +environment_checks: [gm4:score_on_non_player_entity, gm4_double_doors:bloo_is_not_online, lib_forceload:command_blocks_enabled] +``` + +As shown above, environment checks are namespaced. If the namespace is omitted, the namespace of the parent module is used. + +Environment checks are run on every reload, with each check only running once per reload even if multiple modules require it. +For testing purposes, environment checks may be bypassed by setting a positive test result before reloading data packs. +To do this run +```mcfunction +/data modify storage {namespace}:environment_checks result.{check name}.passed set value 1b +``` +and run `/reload`. +This has to be repeated before each reload. + +### Base Environment Checks +`base` comes with some fundamental environment checks that can be referenced by the `gm4:` namespace. + +| Check Name | Description | +|--------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| gm4:score_on_non_player_entity | Checks if non-player entities can be added to a scoreboard. Fails if a test marker's score can not be set and read back. | + +### Other Notable Environment Checks +Some libraries also implement checks, which are inherited by all modules requiring said libraries, as a failed library install will prevent the dependent module from installing. +Hence, you probably don't need to add these checks to your module if you already depend on the library. + +| Library | Check Name | Description | +|---------------|----------------------------|---------------------------------------------------------------------------------------------------------------------| +| lib_forceload | gm4:command_blocks_enabled | Checks if command blocks are enabled. Fails if the command block in the forceloaded chunk can not execute commands. | + +### Creating New Environment Checks +Modules may also introduce their own environment checks. +Any functions contained directly within the `function/environment_check/` folder of a module are treated as environment checks and may be mentioned within 'beet.yaml`. +Functions in subfolders within `function/environment_check/` are ignored and can therefore be used as helper functions. + +To indicate a successful environment check include +```mcfunction +data modify storage :environment_checks result set value {: {passed: 1b}} +``` +within your check, replacing `` and `` with the module's namespace and the new check's name (identical to the file name without the `.mcfunction` suffix) respectively. +Optionally, a failed environment check can display a message to the user by including +```mcfunction +data modify storage :environment_checks result set value {: {probable_cause: "This may be caused by the programmer not expecting you to run this on a potato."}} +``` +within your check, once again replacing the `` and `` with the module's and the new check's name respectively, as well as adding a more adequate message. + +Multiple modules may require the same environment check during a single reload. +To cut down on lag, you should ensure your environment check tries to use a previous test result -- from the same `/reload` period -- before deciding to re-run the test. +This can be done using +```mcfunction +execute unless data storage :environment_checks result. +``` +You **must also clear all check results** in post-load. + +For a textbook example of an environment check, inspect `gm4:score_on_non_player_entity` in `base`. diff --git a/gm4/plugins/versioning.py b/gm4/plugins/versioning.py index 38c43904df..96d43eed56 100644 --- a/gm4/plugins/versioning.py +++ b/gm4/plugins/versioning.py @@ -11,6 +11,7 @@ class VersionInjectionConfig(PluginOptions): advancements: list[str] = [] class VersioningConfig(PluginOptions, extra=Extra.ignore): + environment_checks: list[str] = [] schedule_loops: list[str] = [] required: dict[str, str] = {} extra_version_injections: VersionInjectionConfig = Field(default=VersionInjectionConfig()) @@ -56,6 +57,26 @@ def modules(ctx: Context, opts: VersioningConfig): lines.append(f"execute if score {dep_id} load.status matches 1.. unless score {dep_id} load.status matches {dep_ver.major} run data modify storage gm4:log queue append value {log_data}") lines.append(f"execute if score {dep_id} load.status matches {dep_ver.major} unless score {dep_id}_minor load.status matches {dep_ver.minor}.. run data modify storage gm4:log queue append value {log_data}") + # add required environment checks + for namespaced_environment_check in opts.environment_checks: + match namespaced_environment_check.split(":"): + case [check]: # if no namespace is given, assume current project's namespace + namespace = ctx.project_id + case [namespace, check]: + pass + case _: + raise ValueError(f"{namespaced_environment_check} is not a valid environment check name") + if namespace == "gm4" or namespace.startswith("lib_"): # base and libraries need versioned namespaces + parsed_version = Version(dependencies[namespace]) + line = f"if function {namespace}-{parsed_version.major}.{parsed_version.minor}:environment_check/{check} " + else: + line = f"if function {namespace}:environment_check/{check} " + + lines[0] += line + lines[1] += line + log_data = f"{{type:\"environment_check_failed\",module:\"{ctx.project_name}\",id:\"{ctx.project_id}\",environment_check:\"{namespace}:{check}\",probable_cause:{{\"nbt\":\"result.{check}.probable_cause\",\"storage\":\"{namespace}:environment_checks\"}}}}" + lines.append(f"execute unless data storage {namespace}:environment_checks result.{check}.passed run data modify storage gm4:log queue append value {log_data}") + # finalize startup check module_ver = Version(ctx.project_version) lines[1] = lines[0] + f"run scoreboard players set {ctx.project_id}_minor load.status {module_ver.minor}" diff --git a/gm4_double_doors/beet.yaml b/gm4_double_doors/beet.yaml index 85afc6e5d0..b5a3536690 100644 --- a/gm4_double_doors/beet.yaml +++ b/gm4_double_doors/beet.yaml @@ -16,6 +16,7 @@ meta: gm4: versioning: schedule_loops: [] + environment_checks: [gm4:score_on_non_player_entity] website: description: Tired of clicking twice to open a double door? Annoyed by the fact that doors are only two blocks tall? This data pack automatically opens adjacent doors, making double doors fully functional! Additionally, bottom trapdoors of matching wood type placed above a door are opened alongside the door when it is opened by a player. recommended: []