diff --git a/docs/scarpet/api/Events.md b/docs/scarpet/api/Events.md index b1baba69..6367e3c2 100644 --- a/docs/scarpet/api/Events.md +++ b/docs/scarpet/api/Events.md @@ -1,39 +1,48 @@ # Scarpet events system -Provides the ability to execute specific function whenever an event occurs. The functions to be registered need to -conform with the arguments to the event specification. When loading module functions, each function that starts -with `__on_...` and has the required arguments, will be bound automatically. In case of player specific modules, -all player action events will be directed to the appropriate player space, and all tick events will be executed in -the global context, so its not a good idea to mix these two, so use either of these, or use commands to call tick -events directly, or handle player specific data inside an app. +Scarpet provides the ability to execute specific function whenever an event occurs. The functions to be subscribed for an event +need to conform with the arguments to the event specification. There are several built-in events triggered when certain in-game +events occur, but app designers can create their own events and trigger them across all loaded apps. -Most events strive to report right before they take an effect in the game. The purpose of that is that this give a choice +When loading the app, each function that starts +with `__on_` and has the required arguments, will be bound automatically to a corresponding built-in event. '`undef`'ying +of such function would result in unbinding the app from this event. + +In case of `player` scoped apps, +all player action events will be directed to the appropriate player hosts. Global events, like `'tick'`, cannot be handled +in `'player'` scoped app. + +`'global'` scoped apps will receive both global and player built-in events. + +Most built-in events strive to report right before they take an effect in the game. The purpose of that is that this give a choice for the programmer to handle them right away (as it happens, potentially affect the course of action by changing the environment right before it), or decide to handle it after by scheduling another call for the end of the tick. Or both - partially handle the event before it happens and handle the rest after. While in some cases this may lead to programmers confusion (like handling the respawn event still referring to player's original position and dimension), but gives much more control over these events. +Programmers can also define their own events and signal other events, including built-in events, and across all loaded apps. + ## Event list -Here is a list of events that are handled by scarpet. This list includes prefixes for function names, allowing apps +Here is a list of events that are handled by default in scarpet. This list includes prefixes for function names, allowing apps to autoload them, but you can always add any function to any event (using `/script event` command) if it accepts required number of parameters. -## Global events +## Built-in global events Handling global events is only allowed in apps with `'global'` scope. With the default scope (`'player'`) you -simply wouldn't know which player to hook up this event to. All other events can be handled by any app type. +simply wouldn't know which player to hook up this event to. ### `__on_tick()` Event triggers at the beginning of each tick, located in the overworld. You can use `in_dimension()` to access other dimensions from there. ### `__on_tick_nether()` (Deprecated) -Duplicate of `tick`, just automatically located in the nether. +Duplicate of `tick`, just automatically located in the nether. Use `__on_tick() -> in_dimension('nether', ... ` instead. ### `__on_tick_ender()` (Deprecated) -Duplicate of `tick`, just automatically located in the end. +Duplicate of `tick`, just automatically located in the end. Use `__on_tick() -> in_dimension('end', ... ` instead. ### `__on_chunk_generated(x,z)` Called right after a chunk at a given coordinate is full generated. `x` and `z` correspond @@ -45,12 +54,18 @@ Triggered right after a lightning strikes. Lightning entity as well as potential already be spawned at that point. `mode` is `true` if the lightning did cause a trap to spawn. ### `__on_carpet_rule_changes(rule, new_value)` -Triggered when a Carpet rule is changed. It includes extension rules, which will be namespaced as `namespace:rule`. - -## Player events +Triggered when a carpet mod rule is changed. It includes extension rules, not using default `/carpet` command, +which will then be namespaced as `namespace:rule`. -These are triggered with a player context. For apps with a 'player' scope, they trigger once for the appropriate -player. In apps with `global` scope they trigger once for all players. +### entity load event -> check `entity_load_handler()` + +These will trigger every time an entity of a given type is loaded into the game: spawned, added with a chunks, +spawned from commands, anything really. Check `entity_load_handler()` in the entity section for details. + +## Built-in player events + +These are triggered with a player context. For apps with a `'player'` scope, they trigger once for the appropriate +player. In apps with `global` scope they trigger once as well as a global event. ### `__on_player_uses_item(player, item_tuple, hand)` Triggers with a right click action. Event is triggered right after a server receives the packet, before the @@ -182,7 +197,170 @@ Triggered when a player statistic changes. Doesn't notify on periodic an rhythmi `time_since_death`, `time_since_rest`, and `played_one_minute` since these are triggered every tick. Event is handled before scoreboard values for these statistics are changed. -### `/script event` command +## Custom events and hacking into scarpet event system + +App programmers can define and trigger their own custom events. Unlike built-in events, all custom events pass a single value +as an argument, but this doesn't mean that they cannot pass a complex list, map, or nbt tag as a message. Each event signal is +either targetting all global context apps, if no target player has been identified, or only player scoped apps, if the target player +is specified. + +Programmers can also handle built-in events the same way as custom events, as well as triggering built-in events, which I have +have no idea why you would need that. The following snippets have the same effect: + +``` +__on_player_breaks_block(player, block) -> print(player+' broke '+block); +``` +and +``` +handle_event('player_breaks_block', _(player, block) -> print(player+' broke '+block)); +``` + +as well as +``` +undef('__on_player_breaks_block'); +``` +and +``` +handle_event('player_breaks_block', null); +``` +And `signal_event` can be used as a trigger, called twice for player based built-in events +``` +signal_event('player_breaks_block', player, player, block); // to target all player scoped apps +signal_event('player_breaks_block', null , player, block); // to target all global scoped apps +``` +or (for global only events) +``` +signal_event('tick') // trigger only global scoped apps with no extra arguments +``` + +### `handle_event(event, callback ...)` + +Provides a handler for an event identified by the '`event`' argument. If the event doesn't exist yet, it will be created. +All loaded apps globally can trigger that event, when they call corresponding `signal_event(event, ...)`. Callback can be +defined as a function name, function value (or a lambda function), along with optional extra arguments that will be passed +to it when the event is triggered. All custom events expect a function that takes one free argument, passed by the event trigger. +If extra arguments are provided, they will be appended to the argument list of the callback function. + +Returns `true` if subscription to the event was successful, or `false` if it failed (for instance wrong scope for built-in event, +or incorect number of parameters for the event). + +
+foo(a) -> print(a);
+handle_event('boohoo', 'foo');
+
+bar(a, b, c) -> print([a, b, c]);
+handle_event('boohoo', 'bar', 2, 3) // using b = 2, c = 3, a - passed by the caller
+
+handle_event('tick', _() -> foo('tick happened')); // built-in event
+
+handle_event('tick', null)  // nah, ima good, kthxbai
+
+ +### `signal_event(event, target_player?, ... args?)` + +Fires a specific event. If the event does not exist (only `handle_event` creates missing mising new events), or provided argument list +was not matching the callee expected arguments, returns `null`, +otherwise returns number of apps notified. If `target_player` is specified and not `null` triggers a player specific event, targetting +only `player` scoped apps for that player. Apps with globals scope will not be notified even if they handle this event. +If the `target_player` is omitted or `null`, it will instead only target `global` scoped apps and skip `player` scoped. +Therefore each custom event trigger can trigger each app once, but calls to '`signal_event`' can be repeated to cover +all app scopes. Note that all built-in player events have a player as a first argument, so to trigger these events, you need to +provide them twice - once to specify the target player scope and second - to provide as an argument to the handler function. + +
+signal_event('player_breaks_block', player, player, block); // to target all player scoped apps
+signal_event('player_breaks_block', null  , player, block); // to target all global scoped apps
+signal_event('tick') // triggering tick event in all loaded apps
+
+ +## Custom events example + +The following example contains 4 apps, 2 of them defining a new event, `"player_farts"` triggered after 40 ticks of continuous +squatting. Note that this could call the recipient event handlers twice, since `signal_event` will be called twice, but +in this case the event is once called as a global event, and once as a player event, targeting each app once. Custom events is +a good idea to share common event detection code with one app, and using the outcome of these events in multiple places. +Obviously you would define each event handler once not twice in your app system. + +In this +case here we have two ways of detecting all players squatting for more than 40 ticks - one of those apps will produce cloud particles +in an apprpriate location, and the other will produce a sound when it happens, and they both will proceed to signal the same event, +thankfully for different scopes, otherwise other apps would trigger twice. + +``` +// detector1.sc +// detecting event in a tick loop +__config() -> {'scope' -> 'global'}; + +global_sneaks = {}; +__on_tick() -> for(player('all'), + if (_~'sneaking', + sneak_time = if (has(global_sneaks:_), tick_time()-global_sneaks:_ , global_sneaks:_=tick_time(); 0); + if (sneak_time > 20 && sneak_time < 40, + particle('cloud', pos(_) + [0, _~'height'/2, 0] - _~'look', 1, 0.1) + ); + if (sneak_time == 40, + signal_event('player_farts', _, _) // triggering all player scoped apps from a global app + ) + , // else + delete(global_sneaks:_) + ) +) +``` + +``` +// detector2.sc +// detecting events in a more appropriate way - player based and event based app +__on_player_starts_sneaking(p) -> +( + global_sneaking = tick_time(); + schedule(40, 'check', p); +); + +check(p) -> +if( global_sneaking && tick_time() - global_sneaking == 40, + sound('entity.shulker.shoot', pos(p), 1, 0.6, 'player'); + signal_event('player_farts', null, p) // trigerring all global apps from a player scoped app +); + +__on_player_stops_sneaking(p) -> (global_sneaking = false); +``` + +And then two other apps that meant to do something with this event. One mimics an explosion, and the other - grows crops around +the player. + +``` +client1.sc +__config() -> {'scope' -> 'global'}; + +on_fart(p, msg) -> +( + print(p+msg); + particle('explosion', pos(p)); + sound('entity.generic.explode', pos(p), 1, 0.5); +); + +handle_event('player_farts', 'on_fart', ' goes kaboom'); +``` + +``` +client2.sc +longsneak(p) -> +( + ppos = pos(p); + loop ( 500, + target = ppos+[rand(12),rand(8),rand(12)]-[rand(12),rand(8),rand(12)]; + if (material(target) == 'plant' && ticks_randomly(target), + particle('happy_villager', target, 2, 0.4) + ); + random_tick(target) + ) +); + +handle_event('player_farts', 'longsneak'); +``` + + +## `/script event` command used to display current events and bounded functions. use `add_to` ro register new event, or `remove_from` to unbind a specific function from an event. Function to be bounded to an event needs to have the same number of diff --git a/docs/scarpet/language/FunctionsAndControlFlow.md b/docs/scarpet/language/FunctionsAndControlFlow.md index 3fc9ded4..2d6a8d00 100644 --- a/docs/scarpet/language/FunctionsAndControlFlow.md +++ b/docs/scarpet/language/FunctionsAndControlFlow.md @@ -33,16 +33,22 @@ expr1 ; expr2 => expr2 // with expr1 as a side-effect ## Global variables -All defined functions are compiled, stored persistently, and available globally - accessible to all other scripts. +All defined functions are compiled, stored persistently, and available globally within the app. Functions can only be undefined via call to `undef('fun')`, which would erase global entry for function `fun`. -Since all variables have local scope inside each function, one way to share large objects is via global variables +Since all variables have local scope inside each function, or each command script, + global variables is a way to share the global state. + +Any variable that is used with a name that starts with `'global_'` will be stored and accessible globally, not only +inside the current scope. If used directly in the chat window with the default app, it will persist across calls to `/script` +function. Like functions, which are global, global variables can only be undefined via `undef`. + +For apps running in `'global'` scope - all players will share the same global variables and defined functions, +and with `player` scope, each player hosts its own state for each app, so function and global_variables are distinct. -Any variable that is used with a name that starts with `'global_'` will be stored and accessible globally, not, -inside current scope. It will also persist across scripts, so if a procedure needs to use its own construct, it -needs to define it, or initialize it explicitly, or undefine it via `undef`
-a() -> global_list+=1; global_list = l(1,2,3); a(); a(); global_list  // => [1,2,3,1,1]
+/script run a() -> global_list+=1; global_list = l(1,2,3); a(); a(); global_list  // => [1, 2, 3, 1, 1]
+/script run a(); a(); global_list  // => [1, 2, 3, 1, 1, 1, 1]
 
### `Operator ->` diff --git a/docs/scarpet/resources/editors/idea/2.txt b/docs/scarpet/resources/editors/idea/2.txt index 71f6e287..db277164 100644 --- a/docs/scarpet/resources/editors/idea/2.txt +++ b/docs/scarpet/resources/editors/idea/2.txt @@ -34,6 +34,7 @@ flammable format game_tick generation_status +handle_event hardness harvest in_dimension @@ -89,6 +90,7 @@ set set_biome set_poi set_structure +signal_event sky_light solid sound diff --git a/docs/scarpet/resources/editors/npp/scarpet.xml b/docs/scarpet/resources/editors/npp/scarpet.xml index 8bd5d2b5..2a04dbef 100644 --- a/docs/scarpet/resources/editors/npp/scarpet.xml +++ b/docs/scarpet/resources/editors/npp/scarpet.xml @@ -70,6 +70,8 @@ format logger sound particle particle_line particle_rect system_info + handle_event signal_event + run save tick_time world_time day_time last_tick_times game_tick current_dimension in_dimension view_distance plop harvest destroy schedule nbt draw_shape create_marker remove_all_markers diff --git a/src/main/java/carpet/commands/ScriptCommand.java b/src/main/java/carpet/commands/ScriptCommand.java index 6b8e9a1a..24be5130 100644 --- a/src/main/java/carpet/commands/ScriptCommand.java +++ b/src/main/java/carpet/commands/ScriptCommand.java @@ -74,8 +74,8 @@ public class ScriptCommand scarpetMatches.addAll(APIFunctions.stream(). filter(s -> s.startsWith(prefix) && s.length() <= maxLen).map(s -> s+"(").collect(Collectors.toList())); // not that useful in commandline, more so in external scripts, so skipping here - if (eventPrefix != null) scarpetMatches.addAll(CarpetEventServer.Event.publicEvents().stream(). - filter(s -> s.startsWith(eventPrefix)).map(s -> "__on_"+s+"(").collect(Collectors.toList())); + if (eventPrefix != null) scarpetMatches.addAll(CarpetEventServer.Event.publicEvents(null).stream(). + filter(e -> e.name.startsWith(eventPrefix)).map(s -> "__on_"+s+"(").collect(Collectors.toList())); scarpetMatches.addAll(host.globaFunctionNames(host.main, s -> s.startsWith(prefix)).map(s -> s+"(").collect(Collectors.toList())); scarpetMatches.addAll(host.globaVariableNames(host.main, s -> s.startsWith(prefix)).collect(Collectors.toList())); return scarpetMatches; @@ -269,10 +269,10 @@ public class ScriptCommand executes( (cc) -> listEvents(cc.getSource())). then(literal("add_to"). then(argument("event", StringArgumentType.word()). - suggests( (cc, bb) -> suggestMatching(CarpetEventServer.Event.publicEvents() ,bb)). + suggests( (cc, bb) -> suggestMatching(CarpetEventServer.Event.publicEvents(CarpetServer.scriptServer).stream().map(ev -> ev.name).collect(Collectors.toList()),bb)). then(argument("call", StringArgumentType.word()). suggests( (cc, bb) -> suggestMatching(suggestFunctionCalls(cc), bb)). - executes( (cc) -> CarpetServer.scriptServer.events.addEvent( + executes( (cc) -> CarpetServer.scriptServer.events.addEventFromCommand( cc.getSource(), StringArgumentType.getString(cc, "event"), null, @@ -283,7 +283,7 @@ public class ScriptCommand suggests( (cc, bb) -> suggestMatching(CarpetServer.scriptServer.modules.keySet(), bb)). then(argument("call", StringArgumentType.word()). suggests( (cc, bb) -> suggestMatching(suggestFunctionCalls(cc), bb)). - executes( (cc) -> CarpetServer.scriptServer.events.addEvent( + executes( (cc) -> CarpetServer.scriptServer.events.addEventFromCommand( cc.getSource(), StringArgumentType.getString(cc, "event"), StringArgumentType.getString(cc, "app"), @@ -291,10 +291,10 @@ public class ScriptCommand )?1:0)))))). then(literal("remove_from"). then(argument("event", StringArgumentType.word()). - suggests( (cc, bb) -> suggestMatching(CarpetEventServer.Event.publicEvents() ,bb)). + suggests( (cc, bb) -> suggestMatching(CarpetEventServer.Event.publicEvents(CarpetServer.scriptServer).stream().map(ev -> ev.name).collect(Collectors.toList()) ,bb)). then(argument("call", StringArgumentType.greedyString()). - suggests( (cc, bb) -> suggestMatching(CarpetEventServer.Event.byName.get(StringArgumentType.getString(cc, "event")).handler.callList.stream().map(CarpetEventServer.Callback::toString), bb)). - executes( (cc) -> CarpetServer.scriptServer.events.removeEvent( + suggests( (cc, bb) -> suggestMatching(CarpetEventServer.Event.getEvent(StringArgumentType.getString(cc, "event"), CarpetServer.scriptServer).handler.callList.stream().map(CarpetEventServer.Callback::toString), bb)). + executes( (cc) -> CarpetServer.scriptServer.events.removeEventFromCommand( cc.getSource(), StringArgumentType.getString(cc, "event"), StringArgumentType.getString(cc, "call") @@ -335,7 +335,7 @@ public class ScriptCommand private static int listEvents(ServerCommandSource source) { Messenger.m(source, "w Lists ALL event handlers:"); - for (CarpetEventServer.Event event: CarpetEventServer.Event.byName.values()) + for (CarpetEventServer.Event event: CarpetEventServer.Event.getAllEvents(CarpetServer.scriptServer, null)) { boolean shownEvent = false; for (CarpetEventServer.Callback c: event.handler.callList) diff --git a/src/main/java/carpet/script/CarpetEventServer.java b/src/main/java/carpet/script/CarpetEventServer.java index 81d886f7..9280acd0 100644 --- a/src/main/java/carpet/script/CarpetEventServer.java +++ b/src/main/java/carpet/script/CarpetEventServer.java @@ -11,6 +11,7 @@ import carpet.script.value.NBTSerializableValue; import carpet.script.value.NumericValue; import carpet.script.value.StringValue; import carpet.script.value.Value; +import carpet.script.value.ValueConversions; import carpet.settings.ParsedRule; import carpet.utils.Messenger; import net.minecraft.block.BlockState; @@ -48,6 +49,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -58,6 +60,7 @@ public class CarpetEventServer public final List scheduledCalls = new LinkedList<>(); public final MinecraftServer server; private static final List NOARGS = Collections.emptyList(); + public final Map customEvents = new HashMap<>(); public static class Callback { @@ -94,6 +97,23 @@ public class CarpetEventServer } } + /** + * Used also in entity events + * @param recipient - optional target player argument + * @param runtimeArgs = options + */ + public Boolean targetPlayer(ServerPlayerEntity recipient, List runtimeArgs) + { + List args = runtimeArgs; + if (!this.parametrizedArgs.isEmpty()) + { + args = new ArrayList<>(runtimeArgs); + args.addAll(this.parametrizedArgs); + } + return CarpetServer.scriptServer.runForPlayer(recipient, host, function, args); + } + + @Override public String toString() { @@ -152,6 +172,23 @@ public class CarpetEventServer for (Callback call : fails) callList.remove(call); } } + + public int callCustom(ServerPlayerEntity receipient, List callArg) + { + if (callList.isEmpty()) return 0; + //List fails = new ArrayList<>(); + // skipping fails on purpose - its a player induced call. + int successes = 0; + for (Callback call: callList) + { + Boolean result = call.targetPlayer(receipient, callArg); + //if (result == Boolean.FALSE) fails.add(call); + if (result == Boolean.TRUE) successes++; + } + //for (Callback call : fails) callList.remove(call); + return successes; + } + public boolean addEventCall(ServerCommandSource source, String hostName, String funName, Function verifier) { ScriptHost host = CarpetServer.scriptServer.getHostByName(hostName); @@ -198,9 +235,9 @@ public class CarpetEventServer callList.removeIf((c)-> c.function.getString().equals(funName) && ( hostName == c.host || (c.host != null && c.host.equalsIgnoreCase(hostName) )) ); } - public void removeAllCalls(String hostName) + public void removeAllCalls(CarpetScriptHost host) { - callList.removeIf((c)-> c.host != null && c.host.equals(hostName)); + callList.removeIf((c)-> c.host != null && c.host.equals(host.getName())); } public void clearEverything() { @@ -211,9 +248,11 @@ public class CarpetEventServer public static class Event { public static final Map byName = new HashMap<>(); - public static List publicEvents() + public static List publicEvents(CarpetScriptServer server) { - return byName.entrySet().stream().filter(e -> e.getValue().isPublic).map(Map.Entry::getKey).collect(Collectors.toList()); + List events = byName.values().stream().filter(e -> e.isPublic).collect(Collectors.toList()); + if (server != null) events.addAll(server.events.customEvents.values()); + return events; } public static final Event TICK = new Event("tick", 0, true) @@ -674,15 +713,15 @@ public class CarpetEventServer } }; - public static String getLoadEvent(EntityType et) + public static String getEntityLoadEventName(EntityType et) { - return "entity_loaded_" + Registry.ENTITY_TYPE.getId(et); + return "entity_loaded_" + ValueConversions.of(Registry.ENTITY_TYPE.getId(et)).getString(); } public static final Map, Event> ENTITY_LOAD= new HashMap, Event>() {{ EntityType.get("zombie"); Registry.ENTITY_TYPE.forEach(et -> { - put(et, new Event(getLoadEvent(et), 1, true, false) + put(et, new Event(getEntityLoadEventName(et), 1, true, false) { @Override public void onEntityAction(Entity entity) @@ -715,6 +754,52 @@ public class CarpetEventServer this.isPublic = isPublic; byName.put(name, this); } + + public static List getAllEvents(CarpetScriptServer server, Predicate predicate) + { + List eventList = new ArrayList<>(CarpetEventServer.Event.byName.values()); + eventList.addAll(server.events.customEvents.values()); + if (predicate == null) return eventList; + return eventList.stream().filter(predicate).collect(Collectors.toList()); + } + + public static Event getEvent(String name, CarpetScriptServer server) + { + if (byName.containsKey(name)) return byName.get(name); + return server.events.customEvents.get(name); + } + + public static Event getOrCreateCustom(String name, CarpetScriptServer server) + { + Event event = getEvent(name, server); + if (event != null) return event; + return new Event(name, server); + } + + public static void removeAllHostEvents(CarpetScriptHost host) + { + byName.values().forEach((e) -> e.handler.removeAllCalls(host)); + host.getScriptServer().events.customEvents.values().forEach((e) -> e.handler.removeAllCalls(host)); + } + + public static void clearAllBuiltinEvents() + { + byName.values().forEach(e -> e.handler.clearEverything()); + } + + // custom event constructor + private Event(String name, CarpetScriptServer server) + { + this.name = name; + this.handler = new CallbackList(1); + this.globalOnly = false; + this.isPublic = true; + server.events.customEvents.put(name, this); + } + + //handle_event('event', function...) + //signal_event('event', player or null, args.... ) -> number of apps notified + public boolean isNeeded() { return handler.callList.size() > 0; @@ -748,8 +833,7 @@ public class CarpetEventServer public CarpetEventServer(MinecraftServer server) { this.server = server; - for (Event e: Event.byName.values()) - e.handler.callList.clear(); + Event.clearAllBuiltinEvents(); } public void tick() @@ -777,20 +861,21 @@ public class CarpetEventServer scheduledCalls.add(new ScheduledCall(context, function, args, due)); } - public boolean addEvent(ServerCommandSource source, String event, String host, String funName) + public boolean addEventFromCommand(ServerCommandSource source, String event, String host, String funName) { - if (!Event.byName.containsKey(event)) + Event ev = Event.getEvent(event, CarpetServer.scriptServer); + if (ev == null) { return false; } - Event ev = Event.byName.get(event); boolean added = ev.handler.addEventCall(source, host, funName, h -> canAddEvent(ev, h)); if (added) Messenger.m(source, "gi Added " + funName + " to " + event); return added; } - public void addEventDirectly(String event, ScriptHost host, FunctionValue function, List args) + public void addBuiltInEvent(String event, ScriptHost host, FunctionValue function, List args) { + // this is globals only Event ev = Event.byName.get(event); if (!canAddEvent(ev, host)) throw new InternalExpressionException("Global event "+event+" can only be added to apps with global scope"); @@ -798,51 +883,61 @@ public class CarpetEventServer if (!success) throw new InternalExpressionException("Global event "+event+" requires "+ev.handler.reqArgs+", not "+(function.getNumParams()-((args==null)?0:args.size()))); } + public boolean handleCustomEvent(String event, CarpetScriptHost host, FunctionValue function, List args) + { + Event ev = Event.getOrCreateCustom(event, host.getScriptServer()); + if (!canAddEvent(ev, host)) + throw new InternalExpressionException("Global built-in event "+event+" can only be handled in apps with global scope"); + return ev.handler.addEventCallInternal(host, function, args==null?NOARGS:args); + } + + public int signalEvent(String event, CarpetScriptHost host, ServerPlayerEntity player, List callArgs) + { + Event ev = Event.getEvent(event, host.getScriptServer()); + if (ev == null) return -1; + return ev.handler.callCustom(player, callArgs); + } + private boolean canAddEvent(Event event, ScriptHost host) { return !(event.globalOnly && (host.perUser || host.parent != null)); } - public boolean removeEvent(ServerCommandSource source, String event, String funName) + public boolean removeEventFromCommand(ServerCommandSource source, String event, String funName) { - - if (!Event.byName.containsKey(event)) + Event ev = Event.getEvent(event, CarpetServer.scriptServer); + if (ev == null) { Messenger.m(source, "r Unknown event: " + event); return false; } Pair call = decodeCallback(funName); - Event.byName.get(event).handler.removeEventCall(call.getLeft(), call.getRight()); + ev.handler.removeEventCall(call.getLeft(), call.getRight()); // could verified if actually removed Messenger.m(source, "gi Removed event: " + funName + " from "+event); return true; } - public boolean removeEventDirectly(String event, ScriptHost host) + public boolean removeBuiltInEvent(String event, CarpetScriptHost host) { - - if (!Event.byName.containsKey(event)) - { - return false; - } - Event.byName.get(event).handler.removeAllCalls(host.getName()); + Event ev = Event.getEvent(event, host.getScriptServer()); + if (ev == null) return false; + ev.handler.removeAllCalls(host); return true; } - public void removeEventDirectly(String event, ScriptHost host, String funName) + public void removeBuiltInEvent(String event, CarpetScriptHost host, String funName) { - Event.byName.get(event).handler.removeEventCall(host.getName(), funName); + Event ev = Event.getEvent(event, host.getScriptServer()); + if (ev != null) ev.handler.removeEventCall(host.getName(), funName); } - public void removeAllHostEvents(String hostName) + public void removeAllHostEvents(CarpetScriptHost host) { - Event.byName.values().forEach((e) -> e.handler.removeAllCalls(hostName)); - scheduledCalls.removeIf(sc -> sc.host != null && sc.host.equals(hostName)); - } - - public void clearAll() - { - Event.byName.values().forEach(e -> e.handler.clearEverything()); + // remove event handlers + Event.removeAllHostEvents(host); + // remove scheduled calls + scheduledCalls.removeIf(sc -> sc.host != null && sc.host.equals(host.getName())); } private Pair decodeCallback(String funName) diff --git a/src/main/java/carpet/script/CarpetScriptHost.java b/src/main/java/carpet/script/CarpetScriptHost.java index 15901d95..ff25d1cf 100644 --- a/src/main/java/carpet/script/CarpetScriptHost.java +++ b/src/main/java/carpet/script/CarpetScriptHost.java @@ -117,7 +117,7 @@ public class CarpetScriptHost extends ScriptHost // this is nasty, we have the host and function, yet we add it via names, but hey - works for now String event = funName.replaceFirst("__on_", ""); if (CarpetEventServer.Event.byName.containsKey(event)) - scriptServer.events.addEventDirectly(event, this, function, null); + scriptServer.events.addBuiltInEvent(event, this, function, null); } else if (funName.equals("__config")) { @@ -188,8 +188,7 @@ public class CarpetScriptHost extends ScriptHost { // this is nasty, we have the host and function, yet we add it via names, but hey - works for now String event = funName.replaceFirst("__on_",""); - if (CarpetEventServer.Event.byName.containsKey(event)) - scriptServer.events.removeEventDirectly(event, this, funName); + scriptServer.events.removeBuiltInEvent(event, this, funName); } } @@ -212,6 +211,22 @@ public class CarpetScriptHost extends ScriptHost return host; } + public CarpetScriptHost retrieveForExecution(ServerPlayerEntity target) + { + // allowing only run + if (perUser == (target == null)) return null; + + if (!perUser) + { + if (errorSnooper == null) setChatErrorSnooper(this.scriptServer.server.getCommandSource()); + return this; + } + // user based + CarpetScriptHost userHost = (CarpetScriptHost)retrieveForExecution(target.getName().getString()); + if (userHost.errorSnooper == null) userHost.setChatErrorSnooper(target.getCommandSource()); + return userHost; + } + public Value handleCommandLegacy(ServerCommandSource source, String call, List coords, String arg) { try diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java index dfe5ed28..0a299aba 100644 --- a/src/main/java/carpet/script/CarpetScriptServer.java +++ b/src/main/java/carpet/script/CarpetScriptServer.java @@ -20,6 +20,7 @@ import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.tree.CommandNode; import net.minecraft.server.MinecraftServer; import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.LiteralText; import net.minecraft.util.WorldSavePath; import net.minecraft.util.math.BlockPos; @@ -372,7 +373,7 @@ public class CarpetScriptServer return false; } // stop all events associated with name - events.removeAllHostEvents(name); + events.removeAllHostEvents(modules.get(name)); modules.get(name).onClose(); modules.remove(name); if (!isRuleApp) unloadableModules.remove(name); @@ -402,6 +403,35 @@ public class CarpetScriptServer return true; } + public Boolean runForPlayer(ServerPlayerEntity recipient, String hostname, FunctionValue udf, List argv) + { + CarpetScriptHost host; + ServerCommandSource source; + + if (hostname == null) + { + if (recipient != null) return null; + host = globalHost; + source = server.getCommandSource(); + } + else + { + host = modules.get(hostname).retrieveForExecution(recipient); + if (host == null) // not applicable + return null; + source = (recipient == null)? server.getCommandSource() : recipient.getCommandSource(); + } + try + { + host.callUDF(BlockPos.ORIGIN, source.withLevel(CarpetSettings.runPermissionLevel), udf, argv); + } + catch (NullPointerException | InvalidCallbackException npe) + { + return false; + } + return true; + } + public void tick() { events.tick(); @@ -424,7 +454,7 @@ public class CarpetScriptServer Map apps = new HashMap<>(); modules.forEach((s, h) -> apps.put(s, h.perUser)); apps.keySet().forEach(s -> removeScriptHost(server.getCommandSource(), s, false, false)); - events.clearAll(); + CarpetEventServer.Event.clearAllBuiltinEvents(); init(); apps.forEach((s, pp) -> addScriptHost(server.getCommandSource(), s, pp, false, false)); } diff --git a/src/main/java/carpet/script/api/Auxiliary.java b/src/main/java/carpet/script/api/Auxiliary.java index 2a38bfd1..a2b8ec9a 100644 --- a/src/main/java/carpet/script/api/Auxiliary.java +++ b/src/main/java/carpet/script/api/Auxiliary.java @@ -7,7 +7,9 @@ import carpet.fakes.StatTypeInterface; import carpet.fakes.ThreadedAnvilChunkStorageInterface; import carpet.helpers.FeatureGenerator; import carpet.script.CarpetContext; +import carpet.script.CarpetEventServer; import carpet.script.CarpetScriptHost; +import carpet.script.CarpetScriptServer; import carpet.script.Context; import carpet.script.Expression; import carpet.script.LazyValue; @@ -20,6 +22,7 @@ import carpet.script.utils.ShapeDispatcher; import carpet.script.utils.WorldTools; import carpet.script.value.EntityValue; import carpet.script.value.FormattedTextValue; +import carpet.script.value.FunctionValue; import carpet.script.value.ListValue; import carpet.script.value.NBTSerializableValue; import carpet.script.value.NullValue; @@ -639,12 +642,12 @@ public class Auxiliary { throw new InternalExpressionException("'schedule' should have at least 2 arguments, delay and call name"); long delay = NumericValue.asNumber(lv.get(0).evalValue(c)).getLong(); - FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 1, true, false, true); + FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 1, false, true); CarpetServer.scriptServer.events.scheduleCall( (CarpetContext) c, functionArgument.function, - functionArgument.resolveArgs(c, Context.NONE), + functionArgument.resolveArgs(c, t), delay ); return (c_, t_) -> Value.TRUE; @@ -843,6 +846,50 @@ public class Auxiliary { return LazyValue.NULL; return (_c, _t) -> new NumericValue(player.getStatHandler().getStat(stat)); }); + + //handle_event('event', function...) + expression.addLazyFunction("handle_event", -1, (c, t, lv) -> + { + if (lv.size() < 2) + throw new InternalExpressionException("'handle_event' requires at least two arguments, event name, and a callback"); + String event = lv.get(0).evalValue(c).getString(); + FunctionArgument callback = FunctionArgument.findIn(c, expression.module, lv, 1, true, false); + CarpetScriptHost host = ((CarpetScriptHost)c.host); + Value success; + if (callback.function == null) + { + success = host.getScriptServer().events.removeBuiltInEvent(event, host)?Value.TRUE:Value.FALSE; + } + else + { + success = host.getScriptServer().events.handleCustomEvent(event, host, callback.function, FunctionValue.resolveArgs(callback.args, c, t) )?Value.TRUE:Value.FALSE; + } + return (cc, tt) -> success; + }); + //signal_event('event', player or null, args.... ) -> number of apps notified + expression.addLazyFunction("signal_event", -1, (c, t, lv) -> + { + if (lv.size() == 0) + throw new InternalExpressionException("'signal' requires at least one argument"); + CarpetScriptServer server = ((CarpetScriptHost)c.host).getScriptServer(); + String eventName = lv.get(0).evalValue(c).getString(); + // no such event yet + if (CarpetEventServer.Event.getEvent(eventName, server) == null) return LazyValue.NULL; + ServerPlayerEntity player = null; + List args = Collections.emptyList(); + if (lv.size() > 1) + { + player = EntityValue.getPlayerByValue(server.server, lv.get(1).evalValue(c)); + if (lv.size() > 2) + { + args = FunctionValue.resolveArgs(lv.subList(2, lv.size()), c, t); + } + } + int counts = ((CarpetScriptHost)c.host).getScriptServer().events.signalEvent(eventName, (CarpetScriptHost) c.host, player, args); + if (counts < 0) return LazyValue.NULL; + Value ret = new NumericValue(counts); + return (cc, tt) -> ret; + }); } private static Stat getStat(StatType type, Identifier id) diff --git a/src/main/java/carpet/script/api/Entities.java b/src/main/java/carpet/script/api/Entities.java index e666f950..2edd7600 100644 --- a/src/main/java/carpet/script/api/Entities.java +++ b/src/main/java/carpet/script/api/Entities.java @@ -276,15 +276,15 @@ public class Entities { : Collections.singletonList(entityValue.getString()); Set> types = new HashSet<>(); descriptors.forEach(s -> types.addAll(EntityValue.getEntityDescriptor(s).typeList)); - FunctionArgument funArg = FunctionArgument.findIn(c, expression.module, lv, 1, false, true, false); + FunctionArgument funArg = FunctionArgument.findIn(c, expression.module, lv, 1, true, false); CarpetEventServer events = ((CarpetScriptHost)c.host).getScriptServer().events; if (funArg.function == null) { - types.forEach(et -> events.removeEventDirectly(CarpetEventServer.Event.getLoadEvent(et), c.host)); + types.forEach(et -> events.removeBuiltInEvent(CarpetEventServer.Event.getEntityLoadEventName(et), (CarpetScriptHost) c.host)); } else { - types.forEach(et -> events.addEventDirectly(CarpetEventServer.Event.getLoadEvent(et), c.host, funArg.function, FunctionValue.resolveArgs(funArg.args, c, t))); + types.forEach(et -> events.addBuiltInEvent(CarpetEventServer.Event.getEntityLoadEventName(et), c.host, funArg.function, FunctionValue.resolveArgs(funArg.args, c, t))); } Value ret = new NumericValue(types.size()); return (cc, tt) -> ret; @@ -300,7 +300,7 @@ public class Entities { throw new InternalExpressionException("First argument to entity_event should be an entity"); String what = lv.get(1).evalValue(c).getString(); - FunctionArgument funArg = FunctionArgument.findIn(c, expression.module, lv, 2, false, true, false); + FunctionArgument funArg = FunctionArgument.findIn(c, expression.module, lv, 2, true, false); ((EntityValue) v).setEvent((CarpetContext)c, what, funArg.function, FunctionValue.resolveArgs(funArg.args, c, t)); diff --git a/src/main/java/carpet/script/argument/FunctionArgument.java b/src/main/java/carpet/script/argument/FunctionArgument.java index c34fdec2..0a0a5a28 100644 --- a/src/main/java/carpet/script/argument/FunctionArgument.java +++ b/src/main/java/carpet/script/argument/FunctionArgument.java @@ -28,7 +28,6 @@ public class FunctionArgument extends Argument * @param module module * @param params list of lazy params * @param offset offset where to start looking for functional argument - * @param prematureEvaluation evaluates eagerly arguments in current context before returning * @param allowNone none indicates no function present, otherwise it will croak * @param checkArgs whether the caller expects trailing parameters to fully resolve function argument list * if not - argument count check will not be performed and its up to the caller to verify @@ -40,7 +39,6 @@ public class FunctionArgument extends Argument Module module, List params, int offset, - boolean prematureEvaluation, boolean allowNone, boolean checkArgs) { @@ -66,18 +64,7 @@ public class FunctionArgument extends Argument throw new InternalExpressionException("Function " + fun.getPrettyString() + " requires " + fun.getArguments().size() + " arguments"); } List lvargs = new ArrayList<>(); - if (prematureEvaluation) - { - for (int i = offset+1, mx = params.size(); i < mx; i++) - { - Value arg = params.get(i).evalValue(c); - lvargs.add((cc, tt) -> arg); - } - } - else - { - for (int i = offset+1, mx = params.size(); i < mx; i++) lvargs.add(params.get(i)); - } + for (int i = offset+1, mx = params.size(); i < mx; i++) lvargs.add(params.get(i)); return new FunctionArgument(fun, offset+1+argsize, lvargs); } diff --git a/src/main/java/carpet/script/language/Functions.java b/src/main/java/carpet/script/language/Functions.java index 24eaf124..41a9c3a3 100644 --- a/src/main/java/carpet/script/language/Functions.java +++ b/src/main/java/carpet/script/language/Functions.java @@ -43,7 +43,7 @@ public class Functions { //lv.remove(lv.size()-1); // aint gonna cut it // maybe it will because of the eager eval changes if (t != Context.SIGNATURE) // just call the function { - FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 0, false, false, true); + FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 0, false, true); FunctionValue fun = functionArgument.function; Value retval = fun.callInContext(expr, c, t, fun.getExpression(), fun.getToken(), functionArgument.args).evalValue(c); return (cc, tt) -> retval; ///!!!! dono might need to store expr and token in statics? (e? t?) diff --git a/src/main/java/carpet/script/language/Threading.java b/src/main/java/carpet/script/language/Threading.java index b317a3d8..23cfc878 100644 --- a/src/main/java/carpet/script/language/Threading.java +++ b/src/main/java/carpet/script/language/Threading.java @@ -4,6 +4,7 @@ import carpet.script.Expression; import carpet.script.argument.FunctionArgument; import carpet.script.exception.ExitStatement; import carpet.script.exception.InternalExpressionException; +import carpet.script.value.FunctionValue; import carpet.script.value.NumericValue; import carpet.script.value.ThreadValue; import carpet.script.value.Value; @@ -16,8 +17,8 @@ public class Threading { if (lv.size() == 0) throw new InternalExpressionException("'task' requires at least function to call as a parameter"); - FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 0, true, false, true); - ThreadValue thread = new ThreadValue(Value.NULL, functionArgument.function, expr, tok, c, functionArgument.args); + FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 0, false, true); + ThreadValue thread = new ThreadValue(Value.NULL, functionArgument.function, expr, tok, c, FunctionValue.resolveArgs(functionArgument.args, c, t)); Thread.yield(); return (cc, tt) -> thread; }); @@ -27,8 +28,8 @@ public class Threading if (lv.size() < 2) throw new InternalExpressionException("'task' requires at least function to call as a parameter"); Value queue = lv.get(0).evalValue(c); - FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 1, true, false, true); - ThreadValue thread = new ThreadValue(queue, functionArgument.function, expr, tok, c, functionArgument.args); + FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 1, false, true); + ThreadValue thread = new ThreadValue(queue, functionArgument.function, expr, tok, c, FunctionValue.resolveArgs(functionArgument.args, c, t)); Thread.yield(); return (cc, tt) -> thread; }); diff --git a/src/main/java/carpet/script/value/EntityValue.java b/src/main/java/carpet/script/value/EntityValue.java index 49978474..932499e9 100644 --- a/src/main/java/carpet/script/value/EntityValue.java +++ b/src/main/java/carpet/script/value/EntityValue.java @@ -131,6 +131,10 @@ public class EntityValue extends Value player = (ServerPlayerEntity) e; } } + else if (value.isNull()) + { + return null; + } else { String playerName = value.getString(); diff --git a/src/main/java/carpet/script/value/ThreadValue.java b/src/main/java/carpet/script/value/ThreadValue.java index 11be5b93..d49a4d32 100644 --- a/src/main/java/carpet/script/value/ThreadValue.java +++ b/src/main/java/carpet/script/value/ThreadValue.java @@ -20,7 +20,7 @@ public class ThreadValue extends Value private final long id; private static long sequence = 0L; - public ThreadValue(Value pool, FunctionValue function, Expression expr, Tokenizer.Token token, Context ctx, List args) + public ThreadValue(Value pool, FunctionValue function, Expression expr, Tokenizer.Token token, Context ctx, List args) { id = sequence++; ExecutorService executor = ctx.host.getExecutor(pool); @@ -36,7 +36,7 @@ public class ThreadValue extends Value { try { - return function.lazyEval(ctx, Context.NONE, expr, token, args).evalValue(ctx); + return function.lazyEval(ctx, Context.NONE, expr, token, FunctionValue.lazify(args)).evalValue(ctx); } catch (ExitStatement exit) {