Skip to content

Environment Checks #1093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions base/data/gm4/function/log.mcfunction
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions base/data/gm4/function/post_load.mcfunction
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions docs/making-a-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
149 changes: 149 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
@@ -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 <namespace>:environment_checks result set value {<check name>: {passed: 1b}}
```
within your check, replacing `<namespace>` and `<check name>` 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 <namespace>:environment_checks result set value {<check name>: {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 `<namespace>` and `<check name>` 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 <namespace>:environment_checks result.<check name>
```
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`.
21 changes: 21 additions & 0 deletions gm4/plugins/versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost think we should only run environment checks on first install, instead of every reload? Also, we only need to perform each check once. Putting them into load makes each module run the same check again. Maybe we can finally utilize pre_load for this kind of setup check?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would miss the edge case of importing an existing world to a new server.

Copy link
Member Author

@Bloo-dev Bloo-dev Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting them into load makes each module run the same check again

That's why these checks leave a score behind which is only reset post load. If the check was already done this reload, it won't need to check again and just uses the last result. That result is invalidated in post load, making sure the check is run again once next reload.

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}"
Expand Down
1 change: 1 addition & 0 deletions gm4_double_doors/beet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
Expand Down
Loading