diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/changed_dimension.json b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/changed_dimension.json new file mode 100644 index 0000000..9f03eba --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/changed_dimension.json @@ -0,0 +1,29 @@ +{ + "parent": "rtwrapper:hooks/root", + "display": { + "icon": {"id": "minecraft:ender_pearl"}, + "title": {"text": "RTWrapper: changed dimension hook", "color": "aqua"}, + "description": "Internal - fires the registered changed_dimension hook action for tagged players", + "frame": "task", + "show_toast": false, + "announce_to_chat": false, + "hidden": true + }, + "criteria": { + "requirement": { + "trigger": "minecraft:changed_dimension", + "conditions": { + "player": { + "type": "minecraft:entity_properties", + "entity": "this", + "predicate": { + "minecraft:nbt": "{Tags:[\"rtw.target\"]}" + } + } + } + } + }, + "rewards": { + "function": "rtwrapper:core/hooks/changed_dimension" + } +} diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/consume_item.json b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/consume_item.json new file mode 100644 index 0000000..d87b588 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/consume_item.json @@ -0,0 +1,31 @@ +{ + "parent": "rtwrapper:hooks/root", + "display": { + "icon": {"id": "minecraft:potion"}, + "title": {"text": "RTWrapper: consume item hook", "color": "aqua"}, + "description": "Internal - fires rtwrapper:api/run/context when a tagged item is consumed", + "frame": "task", + "show_toast": false, + "announce_to_chat": false, + "hidden": true + }, + "criteria": { + "requirement": { + "trigger": "minecraft:consume_item", + "conditions": { + "item": { + "components": { + "minecraft:custom_data": { + "rtwrapper": { + "hook": "consume_item" + } + } + } + } + } + } + }, + "rewards": { + "function": "rtwrapper:core/hooks/consume_item" + } +} diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/enter_block.json b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/enter_block.json new file mode 100644 index 0000000..287ed8f --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/enter_block.json @@ -0,0 +1,23 @@ +{ + "parent": "rtwrapper:hooks/root", + "display": { + "icon": {"id": "minecraft:obsidian"}, + "title": {"text": "RTWrapper: enter block hook", "color": "aqua"}, + "description": "Internal - fires the registered enter_block hook action when a player's hitbox enters the configured block", + "frame": "task", + "show_toast": false, + "announce_to_chat": false, + "hidden": true + }, + "criteria": { + "requirement": { + "trigger": "minecraft:enter_block", + "conditions": { + "block": "minecraft:nether_portal" + } + } + }, + "rewards": { + "function": "rtwrapper:core/hooks/enter_block" + } +} diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/entity_killed_player.json b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/entity_killed_player.json new file mode 100644 index 0000000..22a92de --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/entity_killed_player.json @@ -0,0 +1,20 @@ +{ + "parent": "rtwrapper:hooks/root", + "display": { + "icon": {"id": "minecraft:skeleton_skull"}, + "title": {"text": "RTWrapper: entity killed player hook", "color": "aqua"}, + "description": "Internal - fires the registered entity_killed_player hook action when a player is killed by a living entity", + "frame": "task", + "show_toast": false, + "announce_to_chat": false, + "hidden": true + }, + "criteria": { + "requirement": { + "trigger": "minecraft:entity_killed_player" + } + }, + "rewards": { + "function": "rtwrapper:core/hooks/entity_killed_player" + } +} diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/placed_block.json b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/placed_block.json new file mode 100644 index 0000000..7821dbb --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/placed_block.json @@ -0,0 +1,20 @@ +{ + "parent": "rtwrapper:hooks/root", + "display": { + "icon": {"id": "minecraft:grass_block"}, + "title": {"text": "RTWrapper: placed block hook", "color": "aqua"}, + "description": "Internal - fires the registered placed_block hook action when a player places a block", + "frame": "task", + "show_toast": false, + "announce_to_chat": false, + "hidden": true + }, + "criteria": { + "requirement": { + "trigger": "minecraft:placed_block" + } + }, + "rewards": { + "function": "rtwrapper:core/hooks/placed_block" + } +} diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/player_killed_entity.json b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/player_killed_entity.json new file mode 100644 index 0000000..8b00f84 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/player_killed_entity.json @@ -0,0 +1,29 @@ +{ + "parent": "rtwrapper:hooks/root", + "display": { + "icon": {"id": "minecraft:iron_sword"}, + "title": {"text": "RTWrapper: player killed entity hook", "color": "aqua"}, + "description": "Internal - fires the registered player_killed_entity hook action when a player kills a tagged entity", + "frame": "task", + "show_toast": false, + "announce_to_chat": false, + "hidden": true + }, + "criteria": { + "requirement": { + "trigger": "minecraft:player_killed_entity", + "conditions": { + "entity": { + "type": "minecraft:entity_properties", + "entity": "this", + "predicate": { + "minecraft:nbt": "{Tags:[\"rtw.target\"]}" + } + } + } + } + }, + "rewards": { + "function": "rtwrapper:core/hooks/player_killed_entity" + } +} diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/root.json b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/root.json new file mode 100644 index 0000000..db410ba --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/advancement/hooks/root.json @@ -0,0 +1,18 @@ +{ + "display": { + "icon": {"id": "minecraft:command_block"}, + "title": {"text": "RTWrapper Hooks", "color": "dark_gray"}, + "description": "Internal - parent for RTWrapper's advancement-based event hooks", + "background": "minecraft:block/gray_concrete", + "frame": "task", + "show_toast": false, + "announce_to_chat": false, + "hidden": true + }, + "criteria": { + "trigger": { + "trigger": "minecraft:tick" + } + }, + "sends_telemetry_event": false +} diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/enqueue_batch.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/enqueue_batch.mcfunction index 3fcfeac..332d2f1 100644 --- a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/enqueue_batch.mcfunction +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/enqueue_batch.mcfunction @@ -1,4 +1,4 @@ -# Multi-action API entry (v0.1.2, basic form — see note at bottom). +# Multi-action API entry (v1.0.0, basic form — see note at bottom). # # Usage from a dependent pack: # data modify storage rtwrapper:api batch set value [{cmd:"tp",params:{...}},{cmd:"say",params:{...}}] diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/hooks/register.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/hooks/register.mcfunction new file mode 100644 index 0000000..35d53f4 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/hooks/register.mcfunction @@ -0,0 +1,12 @@ +# Register an action to run when a named RTWrapper advancement hook fires +# (e.g. "consume_item", "changed_dimension" - see data/rtwrapper/advancement/hooks/). +# Required storage keys: hook.name (string, must match a hook's registered name), +# hook.action (compound, same shape as rtwrapper:api request, e.g. {cmd:"say",...}). +# No-ops (does not duplicate) if this hook name is already registered - unregister first +# to replace it. +execute unless data storage rtwrapper:api hook.name run return fail +execute unless data storage rtwrapper:api hook.action run return fail +execute unless data storage rtwrapper:hooks registry run data modify storage rtwrapper:hooks registry set value [] + +function rtwrapper:core/hooks/register_check with storage rtwrapper:api hook +data remove storage rtwrapper:api hook diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/hooks/unregister.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/hooks/unregister.mcfunction new file mode 100644 index 0000000..1aefa5f --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/hooks/unregister.mcfunction @@ -0,0 +1,5 @@ +# Unregister a hook action previously set with rtwrapper:api/hooks/register. +# Required storage keys: hook.name (string). Safe no-op if not registered. +execute unless data storage rtwrapper:api hook.name run return fail +function rtwrapper:core/hooks/unregister_apply with storage rtwrapper:api hook +data remove storage rtwrapper:api hook diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/menu/show.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/menu/show.mcfunction new file mode 100644 index 0000000..c202859 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/menu/show.mcfunction @@ -0,0 +1,16 @@ +# Dynamic menu API: show an inline dialog definition without registering it under a +# resource location first. Put the full dialog SNBT/JSON (a "minecraft:notice", +# "minecraft:dialog_list", etc. compound - see the /dialog command's inline-dialog form) +# in storage rtwrapper:api menu.dialog, and the target selector in rtwrapper:api menu.targets +# (defaults to @s if omitted). +# +# Required storage keys: menu.dialog (compound). Optional: menu.targets (defaults to @s). +execute unless data storage rtwrapper:api menu.targets run data modify storage rtwrapper:api menu.targets set value "@s" + +data modify storage rtwrapper:runtime menu_ctx.targets set from storage rtwrapper:api menu.targets +data modify storage rtwrapper:runtime menu_ctx.dialog set from storage rtwrapper:api menu.dialog + +function rtwrapper:core/menu/show_inline with storage rtwrapper:runtime menu_ctx + +data remove storage rtwrapper:runtime menu_ctx +data remove storage rtwrapper:api menu diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/disable.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/disable.mcfunction new file mode 100644 index 0000000..7d5f0d0 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/disable.mcfunction @@ -0,0 +1,6 @@ +# Disable a registered RTWrapper trigger. The objective/registry entry stays, but +# core/trigger/poll will not fire it or reset its score while disabled. +# Required storage keys: trigger.objective (string). +execute unless data storage rtwrapper:api trigger.objective run return fail +function rtwrapper:core/trigger/set_enabled_false with storage rtwrapper:api trigger +data remove storage rtwrapper:api trigger diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/enable.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/enable.mcfunction new file mode 100644 index 0000000..a46f466 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/enable.mcfunction @@ -0,0 +1,5 @@ +# Enable a registered RTWrapper trigger (it will fire again when set). No-op if unregistered. +# Required storage keys: trigger.objective (string). +execute unless data storage rtwrapper:api trigger.objective run return fail +function rtwrapper:core/trigger/set_enabled_true with storage rtwrapper:api trigger +data remove storage rtwrapper:api trigger diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/modify.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/modify.mcfunction new file mode 100644 index 0000000..edff921 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/modify.mcfunction @@ -0,0 +1,8 @@ +# Modify an existing RTWrapper trigger's action and/or reset_mode. +# Required storage keys: trigger.objective (string, must already be registered). +# Optional: trigger.action (compound, replaces the fire action if present), +# trigger.reset_mode ("value"|"keep", replaces the reset mode if present). +# Fails (no-op) if the objective is not registered. +execute unless data storage rtwrapper:api trigger.objective run return fail +function rtwrapper:core/trigger/modify_check with storage rtwrapper:api trigger +data remove storage rtwrapper:api trigger diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/register.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/register.mcfunction new file mode 100644 index 0000000..caf3826 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/register.mcfunction @@ -0,0 +1,16 @@ +# Register a new custom /trigger command backed by RTWrapper. +# Required storage keys: trigger.objective (string, the trigger's scoreboard name), +# trigger.action (RTWrapper request compound to run when the trigger fires - the same +# shape as rtwrapper:api request, e.g. {cmd:"say", params:{message:"hi"}} or {type:"..."}). +# Optional: trigger.reset_mode ("value"|"keep", default "value" - whether the trigger score +# is reset to 0 after firing like vanilla `trigger set 0`, or left as-is for "keep"). +# +# No-ops (does not duplicate) if the objective is already registered - use +# rtwrapper:api/trigger/modify to change an existing registration. + +execute unless data storage rtwrapper:api trigger.objective run return fail +execute unless data storage rtwrapper:api trigger.action run return fail +execute unless data storage rtwrapper:triggers registry run data modify storage rtwrapper:triggers registry set value [] +execute unless data storage rtwrapper:api trigger.reset_mode run data modify storage rtwrapper:api trigger.reset_mode set value "value" + +function rtwrapper:core/trigger/register_check with storage rtwrapper:api trigger diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/unregister.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/unregister.mcfunction new file mode 100644 index 0000000..032ab48 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/api/trigger/unregister.mcfunction @@ -0,0 +1,6 @@ +# Unregister a previously-registered RTWrapper trigger. +# Required storage keys: trigger.objective (string). +# Removes the registry entry and the scoreboard objective. Safe no-op if not registered. +execute unless data storage rtwrapper:api trigger.objective run return fail +function rtwrapper:core/trigger/unregister_apply with storage rtwrapper:api trigger +data remove storage rtwrapper:api trigger diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/changed_dimension.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/changed_dimension.mcfunction new file mode 100644 index 0000000..6f4f778 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/changed_dimension.mcfunction @@ -0,0 +1,8 @@ +# Fired by data/rtwrapper/advancement/hooks/changed_dimension.json when a player tagged +# "rtw.target" changes dimension. +# Looks up the registered action for hook "changed_dimension" (see +# rtwrapper:api/hooks/register) and runs it. No-op if nothing is registered. +# Advancements only grant once per player, so this revokes its own advancement right +# after firing - otherwise the hook would only ever fire the first time per player. +execute if data storage rtwrapper:hooks registry[{name:"changed_dimension"}] run function rtwrapper:core/hooks/dispatch {name:"changed_dimension"} +advancement revoke @s only rtwrapper:hooks/changed_dimension diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/consume_item.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/consume_item.mcfunction new file mode 100644 index 0000000..25d9b00 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/consume_item.mcfunction @@ -0,0 +1,8 @@ +# Fired by data/rtwrapper/advancement/hooks/consume_item.json when a player consumes an +# item tagged {custom_data:{rtwrapper:{hook:"consume_item"}}}. +# Looks up the registered action for hook "consume_item" (see rtwrapper:api/hooks/register) +# and runs it. No-op if nothing is registered for this hook name. +# Advancements only grant once per player, so this revokes its own advancement right +# after firing - otherwise the hook would only ever fire the first time per player. +execute if data storage rtwrapper:hooks registry[{name:"consume_item"}] run function rtwrapper:core/hooks/dispatch {name:"consume_item"} +advancement revoke @s only rtwrapper:hooks/consume_item diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/dispatch.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/dispatch.mcfunction new file mode 100644 index 0000000..5b8565a --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/dispatch.mcfunction @@ -0,0 +1,6 @@ +# Macro: called with an inline compound {name:""}. +# Looks up the registered action for this hook name and runs it via rtwrapper:api/run. +$data modify storage rtwrapper:runtime hook_action set from storage rtwrapper:hooks registry[{name:"$(name)"}].action +data modify storage rtwrapper:api request set from storage rtwrapper:runtime hook_action +data remove storage rtwrapper:runtime hook_action +function rtwrapper:api/run diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/enter_block.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/enter_block.mcfunction new file mode 100644 index 0000000..4a9ec63 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/enter_block.mcfunction @@ -0,0 +1,8 @@ +# Fired by data/rtwrapper/advancement/hooks/enter_block.json when a player's hitbox +# enters the configured block (default: minecraft:nether_portal - edit the "block" field +# in that file to target a different block). +# Advancements only grant once per player, so this revokes its own advancement right +# after firing - otherwise the hook would only ever fire the first time a player enters +# the block, not every time. +execute if data storage rtwrapper:hooks registry[{name:"enter_block"}] run function rtwrapper:core/hooks/dispatch {name:"enter_block"} +advancement revoke @s only rtwrapper:hooks/enter_block diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/entity_killed_player.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/entity_killed_player.mcfunction new file mode 100644 index 0000000..004889e --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/entity_killed_player.mcfunction @@ -0,0 +1,9 @@ +# Fired by data/rtwrapper/advancement/hooks/entity_killed_player.json when a player is +# killed by any living entity (edit the "entity" conditions in that file to filter by +# killer type, e.g. only skeletons). +# Note: the executing player here is the one who died - the advancement/reward system +# still runs as that player even though they are dead when it fires. +# Advancements only grant once per player, so this revokes its own advancement right +# after firing - otherwise the hook would only ever fire the first time per player. +execute if data storage rtwrapper:hooks registry[{name:"entity_killed_player"}] run function rtwrapper:core/hooks/dispatch {name:"entity_killed_player"} +advancement revoke @s only rtwrapper:hooks/entity_killed_player diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/level_up_check.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/level_up_check.mcfunction new file mode 100644 index 0000000..b1bd5a4 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/level_up_check.mcfunction @@ -0,0 +1,11 @@ +# Executor context (@s) is one online player. Compares current xp_level score to the +# stored previous-tick value; fires the level_up hook if it increased. +# A player with no rtw.xp_level_prev entry yet (first tick seen) is seeded from their +# current level so it does not look like a level-up on the very first poll. +# A fully-bounded range (min..max int) is true only when the score entry exists; a missing +# entry never matches any range, so this reliably detects "never seeded" vs "seeded". +# (Note: bare `..` with no bounds is not valid range syntax.) +execute unless score @s rtw.xp_level_prev matches -2147483648..2147483647 run scoreboard players operation @s rtw.xp_level_prev = @s rtw.xp_level + +execute if score @s rtw.xp_level > @s rtw.xp_level_prev run function rtwrapper:core/hooks/level_up_fire +scoreboard players operation @s rtw.xp_level_prev = @s rtw.xp_level diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/level_up_fire.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/level_up_fire.mcfunction new file mode 100644 index 0000000..1f702ff --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/level_up_fire.mcfunction @@ -0,0 +1,3 @@ +# Executor context (@s) is the player whose xp_level just increased. +execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] level_up fired","color":"gray"}] +function rtwrapper:core/hooks/dispatch {name:"level_up"} diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/level_up_poll.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/level_up_poll.mcfunction new file mode 100644 index 0000000..573a82a --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/level_up_poll.mcfunction @@ -0,0 +1,7 @@ +# Polls every online player's xp_level score once per tick and fires the registered +# "level_up" hook (see rtwrapper:api/hooks/register) for anyone whose level increased +# since the last tick. Unlike the advancement-based hooks, there's no vanilla advancement +# trigger for leveling up, so this is tick-polled instead. +# Uses two objectives: rtw.xp_level (criteria "xp_level", auto-tracks the real level) and +# rtw.xp_level_prev (dummy, stores each player's level as of the previous tick). +execute if data storage rtwrapper:hooks registry[{name:"level_up"}] as @a at @s run function rtwrapper:core/hooks/level_up_check diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/placed_block.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/placed_block.mcfunction new file mode 100644 index 0000000..b004f3a --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/placed_block.mcfunction @@ -0,0 +1,6 @@ +# Fired by data/rtwrapper/advancement/hooks/placed_block.json when a player places any +# block (edit the "conditions" in that file to filter by block type, e.g. only stone). +# Advancements only grant once per player, so this revokes its own advancement right +# after firing - otherwise the hook would only ever fire the first time per player. +execute if data storage rtwrapper:hooks registry[{name:"placed_block"}] run function rtwrapper:core/hooks/dispatch {name:"placed_block"} +advancement revoke @s only rtwrapper:hooks/placed_block diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/player_killed_entity.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/player_killed_entity.mcfunction new file mode 100644 index 0000000..5ed732d --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/player_killed_entity.mcfunction @@ -0,0 +1,7 @@ +# Fired by data/rtwrapper/advancement/hooks/player_killed_entity.json when a player kills +# an entity tagged "rtw.target" (edit the "entity" predicate in that file to target +# something else, e.g. a specific mob type). +# Advancements only grant once per player, so this revokes its own advancement right +# after firing - otherwise the hook would only ever fire the first time per player. +execute if data storage rtwrapper:hooks registry[{name:"player_killed_entity"}] run function rtwrapper:core/hooks/dispatch {name:"player_killed_entity"} +advancement revoke @s only rtwrapper:hooks/player_killed_entity diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/register_check.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/register_check.mcfunction new file mode 100644 index 0000000..fdd0487 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/register_check.mcfunction @@ -0,0 +1,5 @@ +# Macro: called with storage rtwrapper:api hook (fields: name, action). +# Fails (no-op) if this hook name is already registered. +$execute if data storage rtwrapper:hooks registry[{name:"$(name)"}] run return fail +$data modify storage rtwrapper:hooks registry append value {name:"$(name)",action:$(action)} +execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] hook registered","color":"gray"}] diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/unregister_apply.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/unregister_apply.mcfunction new file mode 100644 index 0000000..268e707 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/hooks/unregister_apply.mcfunction @@ -0,0 +1,3 @@ +# Macro: called with storage rtwrapper:api hook (field: name). +$data remove storage rtwrapper:hooks registry[{name:"$(name)"}] +execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] hook unregistered","color":"gray"}] diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/load.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/load.mcfunction index 3b8d377..15dc380 100644 --- a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/load.mcfunction +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/load.mcfunction @@ -5,6 +5,8 @@ execute if data storage rtwrapper:meta {loaded:1b} run return 0 scoreboard objectives add rtw.config dummy scoreboard objectives add rtw.status dummy +scoreboard objectives add rtw.xp_level xp_level +scoreboard objectives add rtw.xp_level_prev dummy function rtwrapper:core/meta_init execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] meta gate checked (status/version left untouched if already present)","color":"gray"}] @@ -19,12 +21,14 @@ execute unless score #processed rtw.status matches 0.. run scoreboard players se execute unless score #errors rtw.status matches 0.. run scoreboard players set #errors rtw.status 0 # Bind storage roots if missing. Other packs may write rtwrapper:api request/args. -execute unless data storage rtwrapper:runtime config run data modify storage rtwrapper:runtime config set value {version:"v0.1.2",debug:0b,silent:1b,auto_tick:0b} +execute unless data storage rtwrapper:runtime config run data modify storage rtwrapper:runtime config set value {version:"v1.0.0",debug:0b,silent:1b,auto_tick:0b} execute unless data storage rtwrapper:runtime queue run data modify storage rtwrapper:runtime queue set value [] execute unless data storage rtwrapper:runtime current run data modify storage rtwrapper:runtime current set value {} execute unless data storage rtwrapper:api request run data modify storage rtwrapper:api request set value {} execute unless data storage rtwrapper:api params run data modify storage rtwrapper:api params set value {} execute unless data storage rtwrapper:api batch run data modify storage rtwrapper:api batch set value [] +execute unless data storage rtwrapper:triggers registry run data modify storage rtwrapper:triggers registry set value [] +execute unless data storage rtwrapper:hooks registry run data modify storage rtwrapper:hooks registry set value [] data modify storage rtwrapper:meta loaded set value 1b execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] load complete","color":"gold"}] diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/menu/show_inline.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/menu/show_inline.mcfunction new file mode 100644 index 0000000..add15c4 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/menu/show_inline.mcfunction @@ -0,0 +1,5 @@ +# Sends an inline dialog (no resource-location registration needed) via macro. +# Expects storage keys: targets (string selector), dialog (compound dialog definition, +# e.g. {type:"minecraft:notice", title:{...}, body:[...]}). +$execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] menu/show_inline targets=$(targets)","color":"gray"}] +$dialog show $(targets) $(dialog) diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/meta_init.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/meta_init.mcfunction index 27d2d8d..88e8cf3 100644 --- a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/meta_init.mcfunction +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/meta_init.mcfunction @@ -1 +1 @@ -execute unless data storage rtwrapper:meta loaded run data merge storage rtwrapper:meta {version:"v0.1.2",id:"RTWrapper",type:"datapack",datapack_type:"library",status:"enabled"} \ No newline at end of file +execute unless data storage rtwrapper:meta loaded run data merge storage rtwrapper:meta {version:"v1.0.0",id:"RTWrapper",type:"datapack",datapack_type:"library",status:"enabled"} \ No newline at end of file diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/tick.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/tick.mcfunction index cb60be0..2877611 100644 --- a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/tick.mcfunction +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/tick.mcfunction @@ -2,3 +2,13 @@ # Autotick intentionally processes only one action per tick. Use rtwrapper:api/run # when you explicitly want to drain the whole queue immediately. execute if score #auto_tick rtw.config matches 1.. run function rtwrapper:core/run/run_next + +# Trigger registry polling always runs (independent of auto_tick): registered triggers +# are player-driven (/trigger ), not queue-driven, so they must be checked +# every tick regardless of the auto_tick setting. +function rtwrapper:core/trigger/poll + +# Level-up hook polling: no vanilla advancement trigger exists for leveling up, so this +# is tick-polled instead. Cheap no-op when no level_up hook is registered (see +# core/hooks/level_up_poll.mcfunction). +function rtwrapper:core/hooks/level_up_poll diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/modify_action.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/modify_action.mcfunction new file mode 100644 index 0000000..8301c81 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/modify_action.mcfunction @@ -0,0 +1,2 @@ +# Macro: called with storage rtwrapper:api trigger (fields: objective, action). +$data modify storage rtwrapper:triggers registry[{objective:"$(objective)"}].action set value $(action) diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/modify_check.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/modify_check.mcfunction new file mode 100644 index 0000000..a907207 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/modify_check.mcfunction @@ -0,0 +1,7 @@ +# Macro: called with storage rtwrapper:api trigger (fields: objective, optional action, +# optional reset_mode). Fails (no-op) if the objective is not registered. +$execute unless data storage rtwrapper:triggers registry[{objective:"$(objective)"}] run return fail + +execute if data storage rtwrapper:api trigger.action run function rtwrapper:core/trigger/modify_action with storage rtwrapper:api trigger +execute if data storage rtwrapper:api trigger.reset_mode run function rtwrapper:core/trigger/modify_reset_mode with storage rtwrapper:api trigger +execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] trigger modified","color":"gray"}] diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/modify_reset_mode.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/modify_reset_mode.mcfunction new file mode 100644 index 0000000..9b849a2 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/modify_reset_mode.mcfunction @@ -0,0 +1,2 @@ +# Macro: called with storage rtwrapper:api trigger (fields: objective, reset_mode). +$data modify storage rtwrapper:triggers registry[{objective:"$(objective)"}].reset_mode set value "$(reset_mode)" diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll.mcfunction new file mode 100644 index 0000000..78d2d69 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll.mcfunction @@ -0,0 +1,6 @@ +# Polls every registered trigger objective for every online player once per tick, firing +# enabled ones whose score is non-zero for that player, then resetting (or keeping, per +# reset_mode) the score. Recurses over the registry by index since mcfunction has no +# native list iteration. Trigger scores are per-player, so this runs `as @a`. +execute if data storage rtwrapper:triggers registry[0] as @a at @s run data modify storage rtwrapper:runtime trigger_poll set value {index:0} +execute if data storage rtwrapper:triggers registry[0] as @a at @s run function rtwrapper:core/trigger/poll_step with storage rtwrapper:runtime trigger_poll diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_check.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_check.mcfunction new file mode 100644 index 0000000..b6bbbd3 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_check.mcfunction @@ -0,0 +1,9 @@ +# Macro: called with storage rtwrapper:runtime trigger_cur (a single registry entry: +# objective, enabled, reset_mode, action). Executor context (@s) is the polled player. +# Only proceeds if enabled and the executing player HAS a score on that trigger AND it is +# non-zero (i.e. they ran /trigger ). A missing score must not match here - +# `unless score @s matches 0` would be true both when the score is absent and when +# it's genuinely non-zero, so absence and "not zero" are checked as two separate branches. +execute unless data storage rtwrapper:runtime trigger_cur{enabled:1b} run return 0 +$execute if score @s $(objective) matches 1.. run function rtwrapper:core/trigger/poll_fire with storage rtwrapper:runtime trigger_cur +$execute if score @s $(objective) matches ..-1 run function rtwrapper:core/trigger/poll_fire with storage rtwrapper:runtime trigger_cur diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_fire.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_fire.mcfunction new file mode 100644 index 0000000..7eb380c --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_fire.mcfunction @@ -0,0 +1,8 @@ +# Macro: called with storage rtwrapper:runtime trigger_cur (objective, reset_mode, action). +# Executor context (@s) is the player whose trigger fired. +execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] trigger fired","color":"gray"}] + +data modify storage rtwrapper:api request set from storage rtwrapper:runtime trigger_cur.action +function rtwrapper:api/run + +$execute if data storage rtwrapper:runtime trigger_cur {reset_mode:"value"} run scoreboard players set @s $(objective) 0 diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_next.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_next.mcfunction new file mode 100644 index 0000000..942c28e --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_next.mcfunction @@ -0,0 +1,3 @@ +# Macro: called with storage rtwrapper:runtime trigger_poll (field: index, an int, already +# incremented by poll_step). Recurses into poll_step if registry[index] exists. +$execute if data storage rtwrapper:triggers registry[$(index)] run function rtwrapper:core/trigger/poll_step with storage rtwrapper:runtime trigger_poll diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_step.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_step.mcfunction new file mode 100644 index 0000000..bb95269 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/poll_step.mcfunction @@ -0,0 +1,10 @@ +# Macro: called with storage rtwrapper:runtime trigger_poll (field: index, an int). +# Processes registry[index], then hands off to poll_next to check/recurse on index+1. +$data modify storage rtwrapper:runtime trigger_cur set from storage rtwrapper:triggers registry[$(index)] +function rtwrapper:core/trigger/poll_check with storage rtwrapper:runtime trigger_cur +data remove storage rtwrapper:runtime trigger_cur + +$scoreboard players set #idx rtw.status $(index) +scoreboard players add #idx rtw.status 1 +execute store result storage rtwrapper:runtime trigger_poll.index int 1 run scoreboard players get #idx rtw.status +function rtwrapper:core/trigger/poll_next with storage rtwrapper:runtime trigger_poll diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/register_apply.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/register_apply.mcfunction new file mode 100644 index 0000000..d68ae0a --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/register_apply.mcfunction @@ -0,0 +1,5 @@ +# Macro: called with storage rtwrapper:api trigger (fields: objective, action, reset_mode). +# Creates the trigger-type scoreboard objective and appends the registration entry. +$scoreboard objectives add $(objective) trigger +$data modify storage rtwrapper:triggers registry append value {objective:"$(objective)",enabled:1b,reset_mode:"$(reset_mode)",action:$(action)} +execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] trigger registered","color":"gray"}] diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/register_check.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/register_check.mcfunction new file mode 100644 index 0000000..cfc6694 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/register_check.mcfunction @@ -0,0 +1,4 @@ +# Macro: called with storage rtwrapper:api trigger (fields: objective, action, reset_mode). +# Fails (no-op) if the objective is already registered. +$execute if data storage rtwrapper:triggers registry[{objective:"$(objective)"}] run return fail +function rtwrapper:core/trigger/register_apply with storage rtwrapper:api trigger diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/set_enabled_false.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/set_enabled_false.mcfunction new file mode 100644 index 0000000..df1e9d2 --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/set_enabled_false.mcfunction @@ -0,0 +1,2 @@ +# Macro: called with storage rtwrapper:api trigger (field: objective). +$data modify storage rtwrapper:triggers registry[{objective:"$(objective)"}].enabled set value 0b diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/set_enabled_true.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/set_enabled_true.mcfunction new file mode 100644 index 0000000..51c375c --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/set_enabled_true.mcfunction @@ -0,0 +1,2 @@ +# Macro: called with storage rtwrapper:api trigger (field: objective). +$data modify storage rtwrapper:triggers registry[{objective:"$(objective)"}].enabled set value 1b diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/unregister_apply.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/unregister_apply.mcfunction new file mode 100644 index 0000000..877bd8b --- /dev/null +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/core/trigger/unregister_apply.mcfunction @@ -0,0 +1,4 @@ +# Macro: called with storage rtwrapper:api trigger (field: objective). +$data remove storage rtwrapper:triggers registry[{objective:"$(objective)"}] +$scoreboard objectives remove $(objective) +execute if score #debug rtw.config matches 1.. run tellraw @a[tag=rtwrapper.debug] [{"text":"[RTWrapper] trigger unregistered","color":"gray"}] diff --git a/datapack/RTWrapper-Datapack/data/rtwrapper/function/uninstall.mcfunction b/datapack/RTWrapper-Datapack/data/rtwrapper/function/uninstall.mcfunction index 2891084..38587e0 100644 --- a/datapack/RTWrapper-Datapack/data/rtwrapper/function/uninstall.mcfunction +++ b/datapack/RTWrapper-Datapack/data/rtwrapper/function/uninstall.mcfunction @@ -21,7 +21,15 @@ data remove storage rtwrapper:runtime current data remove storage rtwrapper:api request data remove storage rtwrapper:api params data remove storage rtwrapper:api batch +data remove storage rtwrapper:triggers registry +data remove storage rtwrapper:hooks registry +# NOTE: registered trigger scoreboard objectives (added dynamically via +# rtwrapper:api/trigger/register) are NOT removed here, since their names are unknown +# ahead of time. Call rtwrapper:api/trigger/unregister for each one before uninstalling, +# or remove them manually with /scoreboard objectives remove . # Drop scoreboard objectives (also removes all #debug/#silent/#auto_tick/#processed/#errors entries). scoreboard objectives remove rtw.config -scoreboard objectives remove rtw.status \ No newline at end of file +scoreboard objectives remove rtw.status +scoreboard objectives remove rtw.xp_level +scoreboard objectives remove rtw.xp_level_prev \ No newline at end of file diff --git a/datapack/RTWrapper-Datapack/data/runtoolkit/advancement/packs/rtwrapper.json b/datapack/RTWrapper-Datapack/data/runtoolkit/advancement/packs/rtwrapper.json index d0ac835..bfc718a 100644 --- a/datapack/RTWrapper-Datapack/data/runtoolkit/advancement/packs/rtwrapper.json +++ b/datapack/RTWrapper-Datapack/data/runtoolkit/advancement/packs/rtwrapper.json @@ -15,7 +15,7 @@ "color": "gray" }, { - "text": "RTWrapper v0.1.2", + "text": "RTWrapper v1.0.0", "color": "green" }, { diff --git a/datapack/commands-26.2.json b/datapack/commands-26.2.json index d0b00de..62008b9 100644 --- a/datapack/commands-26.2.json +++ b/datapack/commands-26.2.json @@ -6,7 +6,7 @@ "namespace": "rtwrapper", "links": ["https://github.com/runtoolkit/RTWrapper"], "variant_dispatch": true, - "version": "v0.1.2", + "version": "v1.0.0", "command_params": { "advancement": [ "action", diff --git a/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/RTWrapper.java b/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/RTWrapper.java index 3ca2876..92abec7 100644 --- a/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/RTWrapper.java +++ b/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/RTWrapper.java @@ -1,6 +1,7 @@ package com.runtoolkit.rtwrapper; import com.runtoolkit.rtwrapper.api.RTWrapperAPI; +import com.runtoolkit.rtwrapper.api.RTWrapperConfig; import com.runtoolkit.rtwrapper.command.RTWrapperCommand; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; @@ -18,12 +19,20 @@ public void onInitialize() { CommandRegistrationCallback.EVENT.register( (dispatcher, registryAccess, environment) -> RTWrapperCommand.register(dispatcher)); - // Queue is only drained once per server tick, and draining is a - // plain list iteration over whatever was enqueued that tick — no - // polling, no per-request scheduling, no busy-wait. This keeps - // the mod well inside the TPS 18-19 floor requirement even if a - // caller enqueues the full 256-request cap in one go. - ServerTickEvents.END_SERVER_TICK.register(server -> RTWrapperAPI.runQueued()); + // Datapack parity: autotick is OFF by default (RTWrapperConfig + // default matches load.mcfunction's #auto_tick=0). When enabled, + // exactly one queued request is processed per tick — never a full + // drain — mirroring core/tick.mcfunction's + // "if #auto_tick matches 1.. run function core/run/run_next". + // Draining the whole queue every tick was the wrong behavior here: + // a caller that queues e.g. 200 requests in one go would blow past + // the TPS 18-19 floor requirement in a single tick if autotick + // drained everything instead of throttling to one-per-tick. + ServerTickEvents.END_SERVER_TICK.register(server -> { + if (RTWrapperConfig.isAutoTick()) { + RTWrapperAPI.runNext(); + } + }); LOGGER.info("RTWrapper loaded (native allowlist dispatch, {} commands registered)", com.runtoolkit.rtwrapper.command.RTCommand.values().length); diff --git a/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/api/RTWrapperAPI.java b/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/api/RTWrapperAPI.java index 917709b..4d1661c 100644 --- a/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/api/RTWrapperAPI.java +++ b/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/api/RTWrapperAPI.java @@ -56,6 +56,7 @@ public static RTDispatchResult execute(RTRequest request) { int required = command.minPermissionLevel(); if (!source.hasPermissionLevel(required)) { + RTWrapperConfig.incrementErrors(); return RTDispatchResult.permissionDenied(required, effectivePermissionLevel(source)); } @@ -64,11 +65,15 @@ public static RTDispatchResult execute(RTRequest request) { try { int result = server.getCommandManager().getDispatcher().execute(fullCommand, source); + RTWrapperConfig.incrementProcessed(); if (result <= 0) { + RTWrapperConfig.incrementErrors(); return RTDispatchResult.commandFailed("Command returned non-positive result: " + fullCommand); } return RTDispatchResult.success(); } catch (CommandSyntaxException e) { + RTWrapperConfig.incrementProcessed(); + RTWrapperConfig.incrementErrors(); return RTDispatchResult.commandFailed(e.getMessage()); } } @@ -117,6 +122,63 @@ public static List runQueued() { return results; } + /** + * Process exactly one queued request, if any. This is what the + * autotick tick handler calls — mirrors the datapack's + * core/run/run_next, which intentionally processes only one action + * per tick so a long queue can't blow the TPS floor in one go. + * Returns null if the queue was empty. + */ + public static RTDispatchResult runNext() { + RTRequest request; + synchronized (QUEUE) { + request = QUEUE.poll(); + } + if (request == null) { + return null; + } + return execute(request); + } + + /** + * Discard everything currently queued without executing it. + * Mirrors api/clear_queue.mcfunction. Returns how many requests were + * dropped. + */ + public static int clearQueue() { + synchronized (QUEUE) { + int size = QUEUE.size(); + QUEUE.clear(); + return size; + } + } + + /** + * Enqueue several requests as one logical batch, in order. Mirrors + * api/enqueue_batch: every element goes through the same queue and + * dispatch path as a single enqueue would, no atomic rollback if one + * element fails — a failed element just counts as an error and the + * rest of the batch still runs. + * Returns how many of the requests were actually enqueued (fewer than + * requests.size() if the bounded queue filled up partway through). + */ + public static int enqueueBatch(List requests) { + int enqueued = 0; + for (RTRequest r : requests) { + if (!enqueue(r)) { + break; + } + enqueued++; + } + return enqueued; + } + + public static int queueSize() { + synchronized (QUEUE) { + return QUEUE.size(); + } + } + private static String buildCommandString(RTCommand command, List args) { StringBuilder sb = new StringBuilder(command.literal()); for (String arg : args) { diff --git a/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/api/RTWrapperConfig.java b/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/api/RTWrapperConfig.java new file mode 100644 index 0000000..1102b43 --- /dev/null +++ b/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/api/RTWrapperConfig.java @@ -0,0 +1,63 @@ +package com.runtoolkit.rtwrapper.api; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Runtime configuration mirroring the datapack's rtw.config scoreboard / + * rtwrapper:runtime config storage (#debug, #silent, #auto_tick). + * + * Datapack defaults were: debug=0, silent=1, auto_tick=0 (see load.mcfunction). + * Same defaults kept here for behavioral parity. + */ +public final class RTWrapperConfig { + + private RTWrapperConfig() {} + + private static final AtomicBoolean DEBUG = new AtomicBoolean(false); + private static final AtomicBoolean SILENT = new AtomicBoolean(true); + private static final AtomicBoolean AUTO_TICK = new AtomicBoolean(false); + + private static final AtomicInteger PROCESSED = new AtomicInteger(0); + private static final AtomicInteger ERRORS = new AtomicInteger(0); + + public static boolean isDebug() { + return DEBUG.get(); + } + + public static void setDebug(boolean value) { + DEBUG.set(value); + } + + public static boolean isSilent() { + return SILENT.get(); + } + + public static void setSilent(boolean value) { + SILENT.set(value); + } + + public static boolean isAutoTick() { + return AUTO_TICK.get(); + } + + public static void setAutoTick(boolean value) { + AUTO_TICK.set(value); + } + + public static int incrementProcessed() { + return PROCESSED.incrementAndGet(); + } + + public static int incrementErrors() { + return ERRORS.incrementAndGet(); + } + + public static int processedCount() { + return PROCESSED.get(); + } + + public static int errorCount() { + return ERRORS.get(); + } +} diff --git a/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/command/RTWrapperCommand.java b/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/command/RTWrapperCommand.java index 4d7869a..cbe3c4f 100644 --- a/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/command/RTWrapperCommand.java +++ b/rtwrapper-fabric/src/main/java/com/runtoolkit/rtwrapper/command/RTWrapperCommand.java @@ -4,9 +4,11 @@ import com.runtoolkit.rtwrapper.api.RTDispatchResult; import com.runtoolkit.rtwrapper.api.RTRequest; import com.runtoolkit.rtwrapper.api.RTWrapperAPI; +import com.runtoolkit.rtwrapper.api.RTWrapperConfig; import com.mojang.brigadier.arguments.StringArgumentType; import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; import java.util.Arrays; @@ -14,21 +16,40 @@ /** * /rtwrapper run [args...] + * /rtwrapper autotick + * /rtwrapper silent + * /rtwrapper debug + * /rtwrapper status + * /rtwrapper clear_queue + * /rtwrapper enqueue [args...] + * /rtwrapper run_next + * /rtwrapper drain * * Command-source entry point. The base command itself requires permission * level 2 (matches vanilla's own convention for most admin-adjacent * commands), and RTWrapperAPI.execute() applies the *per-command* level on * top of that — so a level-2 op typing "/rtwrapper run op Steve" still * gets rejected because OP requires level 4. + * + * The config/queue subcommands (autotick, silent, debug, status, + * clear_queue, enqueue, run_next, drain) are direct parity with the + * datapack's api/ functions of the same name — see RTWrapperConfig and + * RTWrapperAPI for the actual behavior each one wraps. */ public final class RTWrapperCommand { private RTWrapperCommand() {} + // Scoreboard-tag equivalent of the datapack's "rtwrapper.debug" player + // tag used by debug/listen and debug/unlisten to decide who receives + // debug tellraw messages. + private static final String DEBUG_LISTENER_TAG = "rtwrapper.debug"; + public static void register(CommandDispatcher dispatcher) { dispatcher.register( CommandManager.literal("rtwrapper") .requires(source -> source.hasPermissionLevel(2)) + .then(CommandManager.literal("run") .then(CommandManager.argument("cmd", StringArgumentType.word()) .then(CommandManager.argument("args", StringArgumentType.greedyString()) @@ -43,16 +64,155 @@ public static void register(CommandDispatcher dispatcher) { "")) ) ) + + .then(CommandManager.literal("autotick") + .then(CommandManager.literal("on").executes(ctx -> setAutoTick(ctx.getSource(), true))) + .then(CommandManager.literal("off").executes(ctx -> setAutoTick(ctx.getSource(), false))) + ) + + .then(CommandManager.literal("silent") + .then(CommandManager.literal("on").executes(ctx -> setSilent(ctx.getSource(), true))) + .then(CommandManager.literal("off").executes(ctx -> setSilent(ctx.getSource(), false))) + ) + + .then(CommandManager.literal("debug") + .then(CommandManager.literal("on").executes(ctx -> setDebug(ctx.getSource(), true))) + .then(CommandManager.literal("off").executes(ctx -> setDebug(ctx.getSource(), false))) + .then(CommandManager.literal("listen").executes(ctx -> setDebugListener(ctx.getSource(), true))) + .then(CommandManager.literal("unlisten").executes(ctx -> setDebugListener(ctx.getSource(), false))) + ) + + .then(CommandManager.literal("status").executes(ctx -> status(ctx.getSource()))) + + .then(CommandManager.literal("clear_queue").executes(ctx -> clearQueue(ctx.getSource()))) + + .then(CommandManager.literal("enqueue") + .then(CommandManager.argument("cmd", StringArgumentType.word()) + .then(CommandManager.argument("args", StringArgumentType.greedyString()) + .executes(ctx -> enqueueWithArgs( + ctx.getSource(), + StringArgumentType.getString(ctx, "cmd"), + StringArgumentType.getString(ctx, "args"))) + ) + .executes(ctx -> enqueueWithArgs( + ctx.getSource(), + StringArgumentType.getString(ctx, "cmd"), + "")) + ) + ) + + // Processes exactly one queued request. Mirrors + // api/run_next.mcfunction / core/run/run_next.mcfunction. + .then(CommandManager.literal("run_next").executes(ctx -> runNext(ctx.getSource()))) + + // Drains the whole queue immediately (same-tick where + // possible). Mirrors api/run.mcfunction's + // core/run/run_actions drain — NOT the same as autotick, + // which throttles to one request per tick. + .then(CommandManager.literal("drain").executes(ctx -> drainQueue(ctx.getSource()))) ); } private static int runWithArgs(ServerCommandSource source, String cmdLiteral, String rawArgs) { - List args = rawArgs.isBlank() - ? List.of() - : Arrays.asList(rawArgs.split(" +")); - + List args = splitArgs(rawArgs); RTDispatchResult result = RTWrapperAPI.execute(cmdLiteral, args, source); + return report(source, cmdLiteral, result); + } + private static int enqueueWithArgs(ServerCommandSource source, String cmdLiteral, String rawArgs) { + RTCommand command = RTCommand.fromLiteral(cmdLiteral); + if (command == null) { + source.sendError(Text.literal("[RTWrapper] Unknown or disallowed command: " + cmdLiteral)); + return 0; + } + List args = splitArgs(rawArgs); + boolean queued = RTWrapperAPI.enqueue(RTRequest.of(command, args, source)); + if (!queued) { + source.sendError(Text.literal("[RTWrapper] Queue is full, request dropped: " + cmdLiteral)); + return 0; + } + maybeDebugFeedback(source, "enqueued: " + cmdLiteral + " (queue size now " + RTWrapperAPI.queueSize() + ")"); + return 1; + } + + private static int runNext(ServerCommandSource source) { + maybeDebugFeedback(source, "run_next"); + RTDispatchResult result = RTWrapperAPI.runNext(); + if (result == null) { + maybeFeedback(source, "queue empty, nothing to run"); + return 1; + } + return report(source, "run_next", result); + } + + private static int drainQueue(ServerCommandSource source) { + maybeDebugFeedback(source, "drain (run_actions)"); + List results = RTWrapperAPI.runQueued(); + long failed = results.stream().filter(r -> !r.isSuccess()).count(); + maybeFeedback(source, "drained " + results.size() + " request(s), " + failed + " failed"); + return 1; + } + + private static int setAutoTick(ServerCommandSource source, boolean value) { + RTWrapperConfig.setAutoTick(value); + maybeFeedback(source, "auto tick " + (value ? "on" : "off")); + return 1; + } + + private static int setSilent(ServerCommandSource source, boolean value) { + RTWrapperConfig.setSilent(value); + // Deliberately still sends this one confirmation even when turning + // silent on — matches the datapack, whose silent/on.mcfunction + // also always tellraws its own confirmation regardless of the + // silent flag it's setting. + source.sendFeedback(() -> Text.literal("[RTWrapper] silent " + (value ? "on" : "off")), false); + return 1; + } + + private static int setDebug(ServerCommandSource source, boolean value) { + RTWrapperConfig.setDebug(value); + source.sendFeedback(() -> Text.literal("[RTWrapper] debug " + (value ? "on" : "off")), false); + return 1; + } + + private static int setDebugListener(ServerCommandSource source, boolean listen) { + if (!(source.getEntity() instanceof ServerPlayerEntity player)) { + source.sendError(Text.literal("[RTWrapper] debug listen/unlisten requires a player source")); + return 0; + } + if (listen) { + player.addCommandTag(DEBUG_LISTENER_TAG); + source.sendFeedback(() -> Text.literal("[RTWrapper] you will receive debug messages"), false); + } else { + player.removeCommandTag(DEBUG_LISTENER_TAG); + source.sendFeedback(() -> Text.literal("[RTWrapper] debug listener removed"), false); + } + return 1; + } + + private static int status(ServerCommandSource source) { + source.sendFeedback(() -> Text.literal( + "[RTWrapper] processed=" + RTWrapperConfig.processedCount() + + " errors=" + RTWrapperConfig.errorCount() + + " queue=" + RTWrapperAPI.queueSize() + + " auto_tick=" + RTWrapperConfig.isAutoTick() + + " silent=" + RTWrapperConfig.isSilent() + + " debug=" + RTWrapperConfig.isDebug() + ), false); + return 1; + } + + private static int clearQueue(ServerCommandSource source) { + int dropped = RTWrapperAPI.clearQueue(); + source.sendFeedback(() -> Text.literal("[RTWrapper] cleared " + dropped + " queued request(s)"), false); + return 1; + } + + private static List splitArgs(String rawArgs) { + return rawArgs.isBlank() ? List.of() : Arrays.asList(rawArgs.split(" +")); + } + + private static int report(ServerCommandSource source, String cmdLiteral, RTDispatchResult result) { switch (result.status()) { case SUCCESS -> { source.sendFeedback(() -> Text.literal("[RTWrapper] OK: " + cmdLiteral), false); @@ -75,4 +235,22 @@ private static int runWithArgs(ServerCommandSource source, String cmdLiteral, St } } } + + // Only sends if debug is on — matches the datapack's pattern of gating + // its verbose "[RTWrapper] ..." tellraws behind #debug rtw.config. + private static void maybeDebugFeedback(ServerCommandSource source, String message) { + if (RTWrapperConfig.isDebug()) { + source.sendFeedback(() -> Text.literal("[RTWrapper] " + message), false); + } + } + + // Sends unless silent is on — matches the datapack's #silent gate on + // its own status/confirmation tellraws (distinct from the #debug gate + // above, same as the two separate scoreboards in load.mcfunction). + private static void maybeFeedback(ServerCommandSource source, String message) { + if (!RTWrapperConfig.isSilent()) { + source.sendFeedback(() -> Text.literal("[RTWrapper] " + message), false); + } + } } +