Add a method to disallow using all global classes in a particular GDScript
fbcosentino opened this issue · 2 comments
Describe the project you are working on
Currently working on a project on space exploration (think "No Man's Sky") where players manage their spaceships very technically, customizing the ship's software by writing real code (e.g. rewriting the autopilot).
Previously published a game where player character was a hacker controlling robots by scripting code.
Also planned a mecha robot building game in a style similar to games like Kerbal Space Program but more inclined to software and circuits, but I gave up on the project due to, among other things, the lack of this specific feature.
Describe the problem or limitation you are having in your project
Some games and apps involve players typing their own code, which is then interpreted/executed as part of the process.
One example is games where the player is expected to write logic as part of the game mechanic (control robots, hack into fictional systems, etc). There is an entire game genre based on this (https://en.wikipedia.org/wiki/Programming_game).
Another example is games which can be customized by players (mods), including writing script code (e.g. enemy AI).
So far, when working with this mechanic, I had to design a scripting language and write my own interpreter (I have seen others trying the same as well). Due to having to write the interpreter, the scripting was rudimentary (sequential) and players complained they wanted usual programming features like branching, loops, functions, local variables, etc, and were frustrated by the lack of.
GDScript could be used to implement in-game programming with a very functional, complex, stable and user friendly language, and it comes with a compiler for free, zero dev effort. However, due to the access to the engine globals, this comes with severe issues. Players could:
- Break the game behaviour
- Call
OS
and damage something in their system - Call
ResourceSaver
and steal assets - In multiplayer games (or with leaderboards), extract server confidential credentials
And if players are meant to share scripts (e.g. mods), hell is unleashed with anything from "delete all hdd" pranks to trojan horse attacks.
Therefore, so far using GDScript to power in-game general purpose scripting is "a nice thing you can't have".
Describe the feature / enhancement and how it helps to overcome the problem or limitation
This proposal (implemented in PR godotengine/godot#61831) adds a feature to the GDScript
class allowing to disable globals, per script, via a simple method call. All game scripts behave as usual by default, but an individual script can have a flag set so that specific script won't have access to any engine globals, including class names, singletons and constants, becoming detached from the game system. It still can access anything if access is explicitly given, via variables or method arguments. Including globals in a script with globals disabled results in the script not compiling or running.
By disabling globals in a script of interest, the GDScript class can be used as a general purpose scripting parser, compiler and interpreter, running inside a safe box and interacting only with what is desired.
Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
The code below demonstrates how to use the new set_globals_disabled
method.
Assuming a file res://user_script.gd
(or a String assigned into GDScript.source_code
), which is meant to be isolated:
var my_var
func my_func(arg):
print("Hello %s!" % arg)
A node can safely run this script with globals disabled:
var new_script = load("res://user_script.gd")
new_script.set_globals_disabled(true)
if new_script.reload():
# Script recompiled successfully
var new_script_instance = new_script.new()
# Any method can be called externally,
# and variables can be externally assigned
new_script_instance.my_var = 42
new_script_instance.my_func("world") # prints "Hello world!"
else:
# Pauses with an error if running from the editor,
# fails silently and continues running in standalone build
print("Failed to compile script")
An example to give explicit access to something, assuming the res://test_script.gd
script:
var input
func do_something():
if input.is_action_pressed("ui_accept"):
print("Hello world!")
The input
local variable can be used to give the script access to the Input
singleton:
var new_script = load("res://test_script.gd")
new_script.set_globals_disabled(true)
if new_script.reload():
var new_script_instance = new_script.new()
new_script_instance.set("input", Input) # Fails silently if user doesn't want to declare or use `var input`
new_script_instance.do_something()
Function arguments also work with external objects. The following script would work even with globals disabled, if ai_search_player
is called externally providing the KinematicBody
arguments:
func ai_search_player(my_kinematic_body, player_kinematic_body):
var player_distance = (player_body.global_transform.origin - my_body.global_transform.origin).length()
If this enhancement will not be used often, can it be worked around with a few lines of script?
No, as this requires changes in the GDScript and GDScriptCompiler classes, which are engine code.
Is there a reason why this should be core and not an add-on in the asset library?
An addon is not possible since this cannot be implemented via scripting.
Related to #389.
PS: You can use gdscript
as a syntax highlighting language on GitHub. No need for that #//
stuff 🙂
@fbcosentino if you're still interested in this issue, I've created a very basic Godot Wasm addon spawned from this proposal (relevant explanatory comment). In short, it should grant reasonable safety when loading arbitrary user-provided scripts. There are many features lacking but if it solves your problem, I'm happy to receive PRs or issues.