666 lines
20 KiB
Plaintext
666 lines
20 KiB
Plaintext
work in progress
|
|
|
|
libraries are "client" and "server"
|
|
stuff in both is also put in "common"
|
|
note, behaviour might be subtly different
|
|
|
|
all files must use "/" as a path separator (Unix Master Race)
|
|
valid characters are listed here (ignoring the path separator):
|
|
-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz
|
|
anything else is invalid and will just be outright rejected
|
|
|
|
furthermore, no paths will be accepted where a . follows a /
|
|
this means that hidden files on unixlikes will not be accepted!
|
|
|
|
files that are meant to be intercepted should be prefixed with a *
|
|
e.g. "*thismap"
|
|
|
|
maximum path length is 128
|
|
|
|
stuff that's actually implemented will be marked with an @
|
|
|
|
lua base library stuff:
|
|
math.* @
|
|
string.* @
|
|
pcall @
|
|
error @
|
|
print @
|
|
loadstring @
|
|
loadfile @
|
|
dofile @
|
|
_G @
|
|
|
|
nothing else yet
|
|
(TODO: work out what we want to keep,
|
|
as there's stuff being used that isn't listed here!)
|
|
|
|
check http://www.lua.org/manual/5.1/ for more info on these
|
|
|
|
some notes:
|
|
- dofile(x) = loadfile(x)()
|
|
- loadfile(x) = common.fetch_block("lua", x)
|
|
- common.fetch_block(ftype, x) =
|
|
local obj = common.fetch_start(ftype, x)
|
|
if obj ~= true then return obj end
|
|
while true do
|
|
local obj = common.fetch_poll()
|
|
if obj ~= false then return obj end
|
|
yield()
|
|
end
|
|
- just about every "load" function is based on common.fetch_block
|
|
|
|
common.base_dir @
|
|
current base directory (minus the "pkg/" bit)
|
|
|
|
writing to this string will *not* change the base dir,
|
|
so *don't write to it* or else you'll just screw up your own code!
|
|
|
|
common.version = {cmp={w,x,y,a,z},num,str="w.x.ya-z"} @
|
|
current engine version
|
|
|
|
cmp is for the individual version components
|
|
num is a comparable number (bit count for each: 5,5,7,5,10)
|
|
str is the version as a string
|
|
|
|
client/server.hook_tick = fn(sec_curtime, sec_delta)->sec_wait @
|
|
sets a hook called every "tick"
|
|
returns a number telling it how long it wants to wait for
|
|
|
|
"sec_curtime" is the current time elapsed from the start
|
|
"sec_delta" is the time elapsed from the start of the last
|
|
call to the hooked function
|
|
|
|
setting this to nil will quit your game
|
|
|
|
NOTE: DO NOT USE COMMON FOR THIS HOOK!
|
|
(only client/server is accepted)
|
|
|
|
server.hook_file = fn(sockfd, ftype, fname)->object
|
|
sets a hook to intercept networked file requests
|
|
|
|
"sockfd" is the socket pertaining to the file request
|
|
|
|
can return nil to cancel the request
|
|
can return true to perform the default request
|
|
otherwise, return an object to be serialised
|
|
|
|
currently supported values for ftype:
|
|
- "icemap"
|
|
|
|
the rest must be either nil or true.
|
|
|
|
client.hook_render = fn() @
|
|
sets a hook called every frame
|
|
|
|
this is the only place where it is safe to render stuff
|
|
such as models and HUDs and whatnot
|
|
|
|
client.hook_key = fn(key, state, modif, uni) @
|
|
sets a hook called every time a key changes
|
|
|
|
state is either true or false
|
|
uni is an index of a unicode character, which is 0 if there is no unicode decoding
|
|
|
|
client.hook_mouse_button = fn(button, state) @
|
|
sets a hook called every time a mouse button changes
|
|
|
|
state is either true or false
|
|
|
|
client.hook_mouse_motion = fn(x, y, dx, dy) @
|
|
sets a hook called every time the mouse is moved
|
|
|
|
x, y are the current position
|
|
dx, dy are the delta positions
|
|
|
|
you will probably use the latter pair if the mouse is "locked"
|
|
as x, y are not defined the same on all platforms!
|
|
|
|
server.hook_connect = fn(sockfd, addrinfo) @
|
|
sets a hook called every time a client connects
|
|
|
|
note, sockfd == true for "local multiplayer" mode
|
|
|
|
"addrinfo" is a table which may or may not contain:
|
|
- "proto" / "addr": protocol / address used, one of the following:
|
|
- "local" / nil
|
|
- "tcp/ip" or "tcp/ip6" / {
|
|
["ip"] = address,
|
|
["host"] = hostname or nil,
|
|
["cport"] = client port,
|
|
["sport"] = server port,
|
|
}
|
|
|
|
note, not all of the features are supported.
|
|
|
|
server.hook_disconnect = fn(sockfd, server_force, reason) @
|
|
sets a hook called every time a client disconnects
|
|
|
|
"server_force" is true where the server forces the disconnect
|
|
|
|
client.mouse_lock_set(state) @
|
|
locks / unlocks the mouse depending on "state"
|
|
|
|
client.mouse_visible_set(state) @
|
|
shows / hides the mouse depending on "state"
|
|
|
|
client.mouse_warp(x, y) @
|
|
moves the mouse to x,y
|
|
|
|
obj = common.fetch_start(ftype, fname)
|
|
initiates a file fetch
|
|
|
|
"ftype" is one of the following:
|
|
- "lua": lua script
|
|
- "map": map (autodetect)
|
|
- "icemap": map (icemap) - in-memory maps are serialised as THIS.
|
|
- "vxl": map (vxl) - CANNOT SAVE IN THIS FORMAT.
|
|
- "pmf": pmf model
|
|
- "tga": tga image
|
|
- "json": json data
|
|
- "log": log data (TODO)
|
|
- "wav": wav sound (TODO)
|
|
|
|
for the server, this just loads the file from the disk.
|
|
|
|
for the client, all clsave/* stuff is taken from the disk,
|
|
but all other files are downloaded from the server.
|
|
|
|
returns true if the fetch has started,
|
|
nil if there is an error,
|
|
or the object if this was an immediate load.
|
|
|
|
if there is already a file in the queue,
|
|
this will return "nil".
|
|
|
|
obj, csize, usize, amount = common.fetch_poll()
|
|
polls the
|
|
"obj" is one of the following:
|
|
- "nil" if transfer aborted or nothing is being fetched
|
|
- in this case, all other fields will be nil
|
|
- "false" if still downloading
|
|
- the object you requested
|
|
- in this case, another poll will just return nils
|
|
|
|
"amount" is in the range 0 <= "amount" <= 1,
|
|
and indicates how much is downloaded
|
|
"csize" is the compressed size of the file
|
|
"usize" is the uncompressed size
|
|
|
|
the two sizes will be nil while unknown.
|
|
|
|
note, all vxl maps will be converted to icemap before sending.
|
|
|
|
obj = common.fetch_block(ftype, fname) @
|
|
fetches a file using common.fetch_*
|
|
|
|
simply returns "nil" on error
|
|
|
|
if there is already something being fetched,
|
|
it will return "nil", too
|
|
|
|
map = common.map_load(fname, fmt = "auto") @
|
|
loads a map, either "vxl" or "icemap"
|
|
use "auto" to autodetect
|
|
|
|
this will return nil if it fails.
|
|
|
|
map = common.map_new(lx, ly, lz) @
|
|
creates a new map
|
|
|
|
note, ly is the *height* of the new map
|
|
|
|
also all dimensions must be powers of two,
|
|
otherwise this will fail horribly!
|
|
|
|
this will throw a lua error if it fails.
|
|
|
|
common.map_free(map) @
|
|
free the given map
|
|
if you don't do this then it's memoryleaktopia
|
|
(plus i'm allowed to kill you)
|
|
|
|
ALSO DON'T DO THIS TO THE CURRENT "SET" MAP
|
|
|
|
TODO: clean up all models on game kill
|
|
|
|
common.map_set(map) @
|
|
sets the "current" map to "map"
|
|
note, "map" can be nil to disable rendering anything
|
|
|
|
map = common.map_get() @
|
|
sets the "current" map
|
|
may return "nil"
|
|
|
|
common.map_save(map, fname, fmt = "icemap") @
|
|
saves a map to a file, either "vxl" or "icemap"
|
|
|
|
this will throw a lua error if it fails.
|
|
|
|
r, g, b, dist = client.map_fog_get() @
|
|
gets the current fog colour / distance
|
|
|
|
client.map_fog_set(r, g, b, dist) @
|
|
sets the current fog colour / distance
|
|
|
|
lx, ly, lz = common.map_get_dims() @
|
|
gets the map's dimensions
|
|
|
|
these will be nil if there is no map loaded
|
|
|
|
table = common.map_pillar_get(px, pz) @
|
|
returns a full pillar of data, skipping the total size header
|
|
|
|
this will be nil if there is no map loaded
|
|
|
|
note, the data wraps around here (wrt px,pz)!
|
|
|
|
common.map_pillar_set(px, pz, table) @
|
|
sets a full pillar of data
|
|
|
|
this will be nil if there is no map loaded
|
|
|
|
the data is checked before setting,
|
|
and will throw a lua error if it fails.
|
|
|
|
note, the data wraps around here (wrt px,pz)!
|
|
|
|
WARNING: You MUST update the side pillars in your code!
|
|
You MUST ALSO fill in the newly-exposed blocks,
|
|
as well as remove the newly-unexposed blocks!
|
|
Otherwise, there WILL be gaps!
|
|
|
|
WE'RE NOT DOING IT FOR YOU!
|
|
|
|
client.camera_point(dx, dy, dz, zoom = 1.0, roll = 0.0) @
|
|
points the camera in a direction with zoom factor "zoom"
|
|
and roll "roll" (in radians, sorry)
|
|
|
|
client.camera_point_sky(dx, dy, dz, zoom = 1.0, sx = 0.0, sy = -1.0, sz = 0.0) @
|
|
points the camera in a direction with zoom factor "zoom"
|
|
and sky arrow sx,sy,sz
|
|
|
|
client.camera_move_local(dx, dy, dz) @
|
|
moves the camera in the camera-local direction (dx,dy,dz)
|
|
|
|
client.camera_move_global(dx, dy, dz) @
|
|
moves the camera in the world direction (dx,dy,dz)
|
|
|
|
client.camera_move_to(px, py, pz) @
|
|
moves the camera to the world position (px,py,pz)
|
|
|
|
px, py, pz = client.camera_get_pos() @
|
|
gets the camera's position
|
|
|
|
dx, dy, dz = client.camera_get_forward() @
|
|
gets the camera's forward vector
|
|
|
|
client.camera_shade_set(xn, yn, zn, xp, yp, zp)
|
|
sets the face shading levels per face (0 <= v <= 1)
|
|
sides relative to map are: east, bottom, south, west, top, north
|
|
|
|
pmf = common.model_new(bonemax = 5) @
|
|
creates a new model
|
|
remember to free it when you're done
|
|
as this is only a light userdata
|
|
|
|
"bonemax" is the initial maximum number of "bones"
|
|
this model can have initially
|
|
useful if you know exactly how many you'll need
|
|
|
|
currently the number of bones is limited to 256
|
|
|
|
pmf = common.model_load_pmf(fname) @
|
|
loads a pmf from a file
|
|
remember to free it when you're done
|
|
as this is only a light userdata
|
|
|
|
limits of bones / points still applies here;
|
|
files which exceed these WILL be rejected!
|
|
|
|
returns nil on failure.
|
|
|
|
success = common.model_save_pmf(pmf, fname) @
|
|
saves a pmf to a file
|
|
|
|
common.model_free(pmf) @
|
|
free the given model
|
|
if you don't do this then it's memoryleaktopia
|
|
(plus i'm allowed to kill you)
|
|
|
|
TODO: clean up all models on game kill
|
|
|
|
len = common.model_len(pmf) @
|
|
get the number of bones in this model
|
|
|
|
pmf, boneidx = common.model_bone_new(pmf, ptmax = 20) @
|
|
creates a new bone
|
|
|
|
"ptmax" is the initial maximum number of "points"
|
|
this bone can have initially
|
|
useful if you know exactly how many you'll need
|
|
|
|
currently the number of points is limited to 4096
|
|
|
|
WARNING: YOU *MUST* TAKE THE pmf VALUE RETURNED
|
|
AND *NOT* USE THE OLD ONE ANY LONGER!
|
|
|
|
this is because realloc() is called on this dynamic list
|
|
and it can seriously crash badly
|
|
|
|
JUST SAYING
|
|
|
|
common.model_bone_free(pmf, boneidx) @
|
|
removes a bone from the model
|
|
|
|
name, table = common.model_bone_get(pmf, boneidx) @
|
|
gets a table with every point in the given bone
|
|
|
|
each entry in the table has the following keys:
|
|
uint16_t radius; // fixed point 8.8
|
|
int16_t x,y,z; // fixed point 8.8
|
|
uint8_t r,g,b;
|
|
|
|
the reserved field of each point is inaccessible from this API
|
|
|
|
common.model_bone_set(pmf, boneidx, name, table) @
|
|
replaces the bone's contents with that in the table
|
|
|
|
note, bones will be rejected if:
|
|
- name is > 15 chars long
|
|
- radius,x,y,z,r,g,b are missing
|
|
- 0 <= radius < 65536 fails
|
|
- -32768 <= x,y,z < 32768 fails
|
|
- 0 <= r,g,b < 256 fails
|
|
|
|
these exceptions will throw a lua error
|
|
|
|
boneidx = common.model_bone_find(pmf, name) @
|
|
finds the first bone with the given name
|
|
|
|
note, this is case sensitive
|
|
|
|
if it cannot be found, this returns nil
|
|
|
|
client.model_render_bone_global(pmf, boneidx, px, py, pz, ry, rx, ry2, scale) @
|
|
renders a bone at world position (px,py,pz),
|
|
|
|
rotated around Y by "ry" radians,
|
|
then around X by "rx" radians,
|
|
then around Y by "ry2" radians,
|
|
and scaled "scale" times
|
|
|
|
client.model_render_bone_local(pmf, boneidx, px, py, pz, ry, rx, ry2, scale) @
|
|
renders a bone at camera-local position (px,py,pz),
|
|
|
|
rotated around Y by "ry" radians,
|
|
then around X by "rx" radians,
|
|
then around Y by "ry2" radians,
|
|
and scaled "scale" times
|
|
|
|
width, height = client.screen_get_dims() @
|
|
gets the dimensions of the screen
|
|
|
|
img, width, height = common.img_load(fname) @
|
|
loads an image with filename "fname"
|
|
remember to free it when you're done
|
|
as this is only a light userdata
|
|
|
|
if this fails, img, width, height will all be nil
|
|
|
|
img = common.img_new(width, height) @
|
|
creates a new 32bpp RGBA image
|
|
remember to free it when you're done
|
|
as this is only a light userdata
|
|
|
|
if this fails, img will be nil
|
|
|
|
common.img_pixel_set(img, x, y, color) @
|
|
sets the pixel x,y on image "img" to "color"
|
|
|
|
drawing is clipped
|
|
|
|
common.img_free(img) @
|
|
free the given image
|
|
if you don't do this then it's memoryleaktopia
|
|
(plus i'm allowed to kill you)
|
|
|
|
width, height = common.img_get_dims(img) @
|
|
gets the image's dimensions
|
|
|
|
client.img_blit(img, dx, dy, width = iwidth, height = iheight, sx = 0, sy = 0, color = 0xFFFFFFFF) @
|
|
blits an image onto screen position dx, dy
|
|
|
|
"color" indicates a base 0xAARRGGBB colour to use
|
|
|
|
this is clipped to fit!
|
|
|
|
client.img_blit_to(target, img, dx, dy, width = iwidth, height = iheight, sx = 0, sy = 0, color = 0xFFFFFFFF) @
|
|
blits an image onto another image at position dx, dy
|
|
|
|
"color" indicates a base 0xAARRGGBB colour to use
|
|
|
|
this is clipped to fit!
|
|
|
|
client.img_fill(img, color) @
|
|
fills an image with a solid colour
|
|
|
|
tab = common.json_parse(str) @
|
|
parses the JSON string "str"
|
|
|
|
yes, this parser is anal-retentive,
|
|
and only allows one specific case where it breaks the rules!
|
|
it also errors if it encounters a char 0 in the middle of a string.
|
|
|
|
returns nil on error
|
|
|
|
tab = common.json_load(fname) @
|
|
loads a JSON file and parses it
|
|
|
|
returns nil on error
|
|
|
|
str = common.json_pack(tab)
|
|
takes a table and spits out valid JSON
|
|
(aside from encoding details)
|
|
|
|
the following types are supported:
|
|
- numbers
|
|
- strings
|
|
- true, false, nil
|
|
- tables with only string keys
|
|
- tables with only numeric keys from 1 through #
|
|
|
|
and therefore the following types are NOT supported:
|
|
- functions/closures of ANY sort (C or Lua)
|
|
- tables with mixed hash/numeric keys
|
|
- userdata (light or otherwise)
|
|
|
|
and i think that's about it for types really
|
|
|
|
returns nil on error
|
|
|
|
success = common.json_save(fname, tab)
|
|
takes a table and saves it to a file as JSON
|
|
|
|
returns true on success, false on error
|
|
|
|
str = common.net_pack(fmt, ...) @
|
|
packs data into a string
|
|
|
|
format is as such:
|
|
b/B = signed/unsigned 8-bit [def 0]
|
|
h/H = signed/unsigned 16-bit [def 0]
|
|
i/I = signed/unsigned 32-bit [def 0]
|
|
f = single-precision 32-bit float [def 0.0]
|
|
d = double-precision 64-bit float [def 0.0]
|
|
z = zero-terminated string [def ""]
|
|
#s = fixed-length string (replace # with a decimal number) [def ""]
|
|
|
|
throws a lua error if the fmt syntax is invalid.
|
|
|
|
spews defaults if not enough arguments are provided.
|
|
|
|
..., remain = common.net_unpack(fmt, str) @
|
|
unpacks data from a string
|
|
|
|
will attempt to decode from start to end
|
|
|
|
"remain" is the remainder of the string which was not decoded
|
|
|
|
returns nil for fields that could not be decoded
|
|
|
|
success = common.net_send(sockfd, str) @
|
|
sends a packet
|
|
|
|
"sockfd" is ignored C->S and should be nil
|
|
S->C local multiplayer should set "sockfd" to "true"
|
|
|
|
str, sockfd = common.net_recv() @
|
|
receives a packet
|
|
|
|
for C->S "sockfd" is nil
|
|
S->C local multiplayer will result in "sockfd" being "true"
|
|
|
|
returns nil if nothing is there
|
|
|
|
server.net_kick(sockfd, reason)
|
|
kicks a client from the server
|
|
|
|
fails silently if sockfd is invalid,
|
|
or sockfd is closed / errors out
|
|
|
|
throws a lua error for other weird errors.
|
|
|
|
wav = common.wav_load(fname)
|
|
loads a sound with filename "fname"
|
|
remember to free it when you're done
|
|
as this is only a light userdata
|
|
|
|
common.wav_free(wav)
|
|
free the given sound
|
|
if you don't do this then it's memoryleaktopia
|
|
(plus i'm allowed to kill you)
|
|
|
|
client.wav_cube_size(size)
|
|
sets the size of a block in metres for sound calculations
|
|
|
|
chn = client.wav_play_global(wav, x, y, z, vol = 1.0, freq_mod = 1.0, vol_spread = ?)
|
|
play the given sound at the given world position
|
|
|
|
TODO: define vol_spread properly
|
|
|
|
returns an index of a channel
|
|
returns nil on error
|
|
|
|
chn = client.wav_play_local(wav, x = 0.0, y = 0.0, z = 0.0, vol = 1.0, freq_mod = 1.0, vol_spread = ?)
|
|
play the given sound at the given camera-local position
|
|
|
|
returns an index of a channel
|
|
returns nil on error
|
|
|
|
exists = client.wav_chn_exists(chn)
|
|
checks if an allocated channel still exists
|
|
|
|
if a channel stops, it is garbage collected
|
|
|
|
success = client.wav_chn_update(chn, x = nil, y = nil, z = nil, vol = nil, freq_mod = nil, vol_spread = nil)
|
|
updates information pertaining to a channel
|
|
|
|
any field which is nil is not affected
|
|
|
|
returns false if the channel no longer exists
|
|
|
|
client.wav_kill(chn)
|
|
stops and removes a channel
|
|
|
|
if chn == "true", kills all channels
|
|
|
|
font_mini
|
|
Small ASCII font, for most body text.
|
|
|
|
font_large
|
|
Big ASCII font, for headers and stuff.
|
|
|
|
font_digits
|
|
Big numeric font, for HUD counters.
|
|
|
|
gui_index_mini(i)
|
|
Character lookup helper for the mini font
|
|
|
|
gui_index_digit(i)
|
|
Character lookup helper for the digit font
|
|
|
|
gui_create_fixwidth_font(image, char_width, char_height, indexing_fn, shadow)
|
|
Creates a new fixed width bitmap font.
|
|
|
|
compute_unwrapped(x, y, color, string)
|
|
Computes specific glyphs and positions needed from a string, color, and x/y coordinates.
|
|
|
|
compute_ctab(ctab, x, y)
|
|
Computes specific glyphs and positions from a color table,
|
|
where each line of the table contains a string "msg" and a color "color",
|
|
and will be positioned underneath the previous line.
|
|
|
|
compute_wordwrap(width, x, y, color, string)
|
|
Computes specific glyphs and positions from a word-wrapped string, within the width argument.
|
|
|
|
dimensions(data)
|
|
Get the AABB dimensions of text(l,r,t,b, width, height) given precomputed text data
|
|
|
|
calc_shadow(color)
|
|
Calculate the shadow strength from the percieved luminance of the font; brighter color = darker shadow
|
|
|
|
print(x, y, color, string, buffer=nil)
|
|
Print text with topleft at x, y, color c, string str.
|
|
buffer defines where the text is drawn - to the screen or to an offscreen buffer.
|
|
|
|
print_precomputed(data, offx, offy, buffer=nil)
|
|
Print precomputed text to the given buffer.
|
|
|
|
print_wrap(wp, x, y, c, str, buffer)
|
|
Print text with minimum-space wordwrapping, pixelwidth wp, topleft at x, y, color c, string str
|
|
|
|
alarm{time=1, progress=0, active=true, loop=false, preserve_accumulator=true, on_frame=nil, on_trigger=nil}
|
|
Create an alarm with an abstract time value.
|
|
|
|
time: The target amount of time before the alarm triggers.
|
|
progress: How much time has elapsed(counting from 0 to the target time).
|
|
active: Whether the alarm is presently ticking.
|
|
loop: Whether the alarm loops.
|
|
preserve_accumulator: If the alarm loops, allow the leftover time to carry into the next loop.
|
|
on_frame: A callback run every frame. Recieves the time delta.
|
|
on_trigger: A callback run each time the alarm reaches its target time. Recieves the time delta.
|
|
|
|
gui_create_scene(width, height, shared_rate=1/60)
|
|
Creates a new GUI scene of designated width and height, and a shared update rate in seconds equal to shared_rate.
|
|
|
|
The GUI system is modelled similarly to the Flash AS3 APIs.
|
|
|
|
A GUI scene can contain any number of display objects, ordered in a tree structure.
|
|
Each time the GUI is ticked, it processes an event pipeline. An event is a message containing a type and some attached data.
|
|
When an event reaches a display object with a listener of the same type, the listener's callback is called with the event's data.
|
|
|
|
These events are available:
|
|
GE_DELTA_TIME
|
|
The delta time passed in when listeners are pumped. You can use this to drive tweening over time, for example.
|
|
Callback passes in the dT value.
|
|
GE_SHARED_ALARM
|
|
A tick running at a fixed rate. This is an alternative to delta time for situations where you specifically need a fixed rate,
|
|
for example, an animation that relies on multiplying previous values every step.
|
|
Callback passes in the dT value of the shared alarm timer.
|
|
GE_KEY
|
|
A keypress(up or down) event.
|
|
Callback passes in {key(int), state(bool), modif(int bitmask)}
|
|
GE_BUTTON
|
|
A mapped button press(up or down) event. Use this when you expect the button to be reassignable.
|
|
Callback passes in {key(int), button{name(string), desc(string)}, state(bool), modif(int bitmask)}
|
|
GE_MOUSE
|
|
A mouse movement event.
|
|
Callback passes in {x(number), y(number), dx(number), dy(number)}
|
|
GE_MOUSE_BUTTON
|
|
A mouse button press(up or down).
|
|
Callback passes in {button(int), down(bool)}
|
|
|
|
|
|
|
|
#
|