d2k_scripting
Sat Feb 7, 2015Scripting in D2K has come a long way. The interface is coming along nicely, largely because Lua makes it so easy to write bindings. For example, this is the function used to match key events to key presses:
static int XI_InputEventIsKeyPress(lua_State *L) {
event_t *ev = (event_t *)luaL_checkudata(L, 1, "InputEvent");
int key = luaL_checkint(L, 2);
if (ev->type == ev_key && ev->pressed && ev->key == key)
lua_pushboolean(L, true);
else
lua_pushboolean(L, false);
return 1;
}
XI_InputEventIsKeyPress
is a method inside the InputEvent class, which is
registered like this:
X_RegisterType("InputEvent", 21,
"new", XI_InputEventNew,
"reset", XI_InputEventReset,
"__tostring", XI_InputEventToString,
"is_key", XI_InputEventIsKey,
"is_mouse", XI_InputEventIsMouse,
"is_joystick", XI_InputEventIsJoystick,
"is_mouse_movement", XI_InputEventIsMouseMovement,
"is_joystick_movement", XI_InputEventIsJoystickMovement,
"is_joystick_axis", XI_InputEventIsJoystickAxis,
"is_joystick_ball", XI_InputEventIsJoystickBall,
"is_joystick_hat", XI_InputEventIsJoystickHat,
"is_movement", XI_InputEventIsMovement,
"is_press", XI_InputEventIsPress,
"is_release", XI_InputEventIsRelease,
"get_key", XI_InputEventGetKey,
"get_key_name", XI_InputEventGetKeyName,
"get_value", XI_InputEventGetValue,
"get_xmove", XI_InputEventGetXMove,
"get_ymove", XI_InputEventGetYMove,
"get_char", XI_InputEventGetChar,
"is_key_press", XI_InputEventIsKeyPress
);
The implementation of X_RegisterObjects
is a little long, but it’s not
terribly complex.
All this scaffolding enables a simple, but powerful scripting interface. For example, here’s the console’s event handler:
function Console:handle_event(event)
if event:is_key_press(d2k.Key.BACKQUOTE) then
self:toggle_scroll()
end
if self.scroll_rate < 0 or self.height == 0 then
return
end
if d2k.KeyStates.shift_is_down() then
if event:is_key_press(d2k.Key.UP) then
self.output:scroll_up(Console.HORIZONTAL_SCROLL_AMOUNT)
return true
elseif event:is_key_press(d2k.Key.PAGE_UP) then
self.output:scroll_up(Console.HORIZONTAL_SCROLL_AMOUNT * 10)
return true
elseif event:is_key_press(d2k.Key.DOWN) then
self.output:scroll_down(Console.HORIZONTAL_SCROLL_AMOUNT)
return true
elseif event:is_key_press(d2k.Key.PAGE_DOWN) then
self.output:scroll_down(Console.HORIZONTAL_SCROLL_AMOUNT * 10)
return true
end
elseif event:is_key_press(d2k.Key.UP) then
self.input:show_previous_command()
elseif event:is_key_press(d2k.Key.DOWN) then
self.input:show_next_command()
elseif event:is_key_press(d2k.Key.LEFT) then
self.input:move_cursor_left()
elseif event:is_key_press(d2k.Key.RIGHT) then
self.input:move_cursor_right()
elseif event:is_key() and event:is_press() then
self.input:insert_text(event:get_char())
end
return false
end
Never mind the temporary placeholder constants
(Console.HORIZONTAL_SCROLL_AMOUNT
, etc.) ;). The point is that it was very
easy to build an entirely new object-oriented event handling interface.
I did have to refactor nearly all the SDL event stuff in C, but it was in need anyway. As a result, after events are pulled from SDL in C, event dispatch and handling happens in Lua now. This means I can slowly start to move things like the menu and screen drawing (NOT the renderer) into Lua, and delete reams of junk code in the process (man, I dream of the day the C menu code dies).
Now that event handling happens in Lua, interfaces can be implemented using only scripting. The console is my test case; as I develop it, I’m learning to use Lua GObject Introspection and fixing the scripting interface I’ve built so far. It’s not a perfect test (there’s not a ton of interaction with D2K-proper), but it’s pretty good for a first test.
Speaking of Lua GObject Introspection, I’m making progress building D2K’s dependencies on Windows. Mingw-builds provides a native, MinGW-w64-built Python which can be used for GObject Introspection, so I’ve been going through the process of writing a bunch of scripts to automatically build everything D2K needs on Windows. This has been a huge roadblock. My secret dream is that peopel can use my work as a kind of cross-platform shim for C/C++ projects, but given the relative unpopularity of similar projects (win-builds, MSYS2, MXE), I’m not optimistic, haha.
Having developed a fair amount of code in Lua, I feel qualified – nay, compelled – to give an opinion!
Lua as an API is fantastic. I wish the documentation were slightly better, but
honestly it’s pretty good. The Lua site itself is hideous; I remember better
pages from 1998. It sounds like I’m kidding, but I am not. I wish there were
better errors in Lua; while they technically make sense, you only realize
why they make sense once you guess correctly as to what the problem is.
Lua the language is overall a fairly positive experience. The “different just
to be different” stuff like ~=
meaning ‘not equal to’, elseif
instead of
literally anything else (else if
, elif
), and arrays starting at 1 are all
extremely bothersome. Especially the array index thing, my God what an
abomination. For loop syntax is a little free-form to me; I really don’t
understand why
for i=0,10,2 do
print(string.format('i is %d', i))
end
is better than:
``c
for (int i = 0; i <= 10, i += 2) {
printf("i is %d\n", i);
}
I do understand why it’s worse though: it’s less explicit.
local
is a big mistake too. Lua argues that “Local by default is wrong.
Maybe global by default is also wrong, [but] the solution is not local by
default”, but come on:
- I am typing
local
everywhere - In Python, I type
global
only when I have to, and it serves as documentation - If you forget to type
local
, you can destroy your program in a very hard-to-discover way - If you forget to type
global
, the damage is limited to that function and whatever depends on it. Which, admittedly, is still pretty bad, but what is better, more damage that’s hard to reason about, or less damage that’s easier to reason about? - If you meant “declarations should look like declarations”,
var
would have sufficed, and wouldn’t get conflated with the scoping issue. - If you meant “scope should be explicit”, how much more explicit than “I defined this variable in this scope, therefore that’s where it lives” can you get? Do you want to define variables for other scopes? MADNESS
The other annoying thing in Lua is that while syntactic sugar abounds for
things that really aren’t that big a deal (using :
instead of .
to pass the
implicit self
to object method calls, omitting parentheses when only passing
a single string/table argument, etc.), there is no syntactic sugar for
implementing a class or building a module. You have to wade into this morass
of metatables and the implicit workings of require
in order to access basic
functionality. You can’t argue that Lua isn’t object-oriented because it has
:
and has a whole section on “Object-Oriented Programming” in the book
(complete with helpful APIs for defining userdata types). By the way,
“userdata” is really poorly named. Regular userdata is a blob managed by Lua,
light userdata is a blob – tracked by a pointer – managed in native code.
Boom, that’s it. Anyway, there ought to be an easier way to define classes and
modules without understanding the deep workings of require
and Lua’s method
dispatch.
Lua is also a little too flexible. Leaving off parentheses is just a huge problem for readability, and in a language that makes you type so much (“function”, “then”, “end”, “do”, “repeat”, “until”) it doesn’t make a lot of sense. I guess maybe they were worried about weird symbols confusing users, but how much does dropping the parentheses really buy you when 99% of the time what follows is a “{” ? Plus, while I pretty much chose Lua over Python because it isn’t whitespace-sensitive, there are times I really wish it were. If I read another piece of code with all the if blocks on the same line, I’m gonna burn Github to the ground. Whew!
In fact, those three annoyances pretty much sum up the Lua experience on the script side. Lua is everything you want in a scripting language: fast, simple, flexible and powerful. But then you find a bunch of code written in a totally unreadable style and you get pissed, or you find yourself mucking about with Lua’s internals because there’s no syntactic sugar for this very simple thing you want to do, or your code doesn’t work because Lua does something different for absolutely no reason.
All that said, if you can live with 1-based indexing, Lua is an excellent language. None of the other annoyances are problematic enough to justify not choosing it.