Scriptable server for OpenNetBattle.
Scripts can be created through Lua. Entry scripts are read from scripts/*/main.lua
for script projects, and scripts/*.lua
for single file scripts.
Support for more sources such as WASM/WASI (C++, Kotlin, etc) or JavaScript can be added by creating a Plugin Interface. For an example of how implement one, take a look at the existing LuaPluginInterface.
The Plugin Interface could also be used to build a Rust based script compiled directly into the server.
Types of assets:
- Texture (.png|.bmp)
- Audio (.ogg)
- Text
Paths
/server
- Pseudo folder that represents files in memory
/assets
- Generated at start from files in
./assets
. ./assets/prog.png
can be referenced with/server/assets/prog.png
- Generated at start from files in
/players
- Stores avatar files sent from players (5 MiB limit)
- Textures are stored as
[id].texture
, and animations are stored as[id].animation
/maps
- Generated from areas and updated every tick.
- Stored as
[area id].txt
- Paths not starting with
/server
are treated as paths to client files. Files of interest are available inresources/
- PATHS ARE CASE SENSITIVE OUT OF WINDOWS, also avoid
\
as that's Windows specific
Maps for areas are stored in ./areas
. The first area players will see is default.tmx
(required).
Editor:
- Fine grid divisions: 2 (Edit -> Preferences -> Interface)
- Snap To Fine Grid (View -> Snapping)
- When working with Object Layer
- Snap To Pixels (View -> Snapping)
- When working with Collision shapes
Map:
- Tile Width: 64
- Tile Height: 32
- Tile Layer Format: CSV (required)
- Create map in assets
- Copy resources/ow/tiles from the client to ./tiles (relative to server folder)
- Server will not send assets from this folder, but will translate the path relative to resources/ow/maps to make use of resources on the client
Tilesets:
- Type: Based on Tileset Image (other types are not currently supported)
- Object Alignment:
- Top - For tile objects stuck to the floor such as warps
- Set drawing offset to 0,0
- Bottom - For tile objects that act as a wall
- Top - For tile objects stuck to the floor such as warps
- Place in a Tile Layer to tune drawing offset
Layers:
- Horizontal Offset: 0
- Vertical Offset: (number of layers below this one) * -16
Map:
- Name: string
- Area name to display in the PersonalMenu
- Song: string
- Path to ogg file
- Background Texture: string
- Path to background image file
- Background Animation: string
- Path to background .animation file
- One animation state "BG"
- First frame of this animation determines background repetition
- Excluding this will use texture size for background repetition
- Background Vel X: float
- Background Vel Y: float
- Background Parallax: float
- Foreground Texture: string
- Path to foreground image file
- Foreground Animation: string
- Path to foreground .animation file
- One animation state "BG"
- First frame of this animation determines foreground repetition
- Excluding this will use texture size for foreground repetition
- Foreground Vel X: float
- Foreground Vel Y: float
- Foreground Parallax: float
Tiles:
- Shadow: string
- "Always"
- "Never"
- Unset - Automatic
Classes are used to denote special tiles or objects understood by the client.
- Warps
- Movement
- Plain Markers
- Tile Objects only
- Visible in minimap
- Players will be warped home if colliding with the warp
- Custom properties:
- Direction: string
- Left
- Right
- Up
- Down
- Up Left
- Up Right
- Down Left
- Down Right
- Direction: string
Position Warp:
- Tile Objects only
- Visible in minimap
- Players will be warped to the set position if colliding with the warp
- Custom properties:
- X: float
- Y: float
- Z: float
- Direction: string
- Left
- Right
- Up
- Down
- Up Left
- Up Right
- Down Left
- Down Right
Server Warp:
- Tile Objects only
- Visible in minimap
- Players will be transferred to a different server if colliding with the warp
- Custom properties:
- Address: string
- Port: number
- Data: string
- Custom data to pass to the other server
- Can be read through handle_player_request on the other server
- Try to keep it short! Long data strings may get ignored
- Direction: string
- Left
- Right
- Up
- Down
- Up Left
- Up Right
- Down Left
- Down Right
Custom Server Warp:
- Tile Objects only
- Visible in minimap
- Players will be warped out if colliding with the warp, the result of the warp can be resolved in handle_custom_warp
- Custom Properties:
- Direction: string
- Left
- Right
- Up
- Down
- Up Left
- Up Right
- Down Left
- Down Right
- Direction: string
Custom Warp:
- Tile Objects only
- Visible in minimap
- Players will be warped out if colliding with the warp, the result of the warp can be resolved in handle_custom_warp
- Direction: string
- Left
- Right
- Up
- Down
- Up Left
- Up Right
- Down Left
- Down Right
- Direction: string
Board:
- Tile Objects only
- Visible in minimap
Shop:
- Tile Objects only
- Visible in minimap
Stairs:
-
Tiles only
-
Visible in minimap
-
Allows players to walk up or down a layer
-
Makes tile directly above become treated as a hole
-
Custom properties:
- Direction: string
- Marks the direction the player will travel up
- Up Left
- Up Right
- Down Left
- Down Right
- Direction: string
Conveyor:
-
Tiles only
-
Visible in minimap
-
Custom properties:
- Direction: string
- Marks the direction the player will travel
- Up Left
- Up Right
- Down Left
- Down Right
- Speed: number? (Tiles per second, default: 6)
- Sound Effect: string
- Direction: string
Ice:
-
Tiles only
-
Custom properties:
- Speed: number? (Tiles per second, default: 6)
- Sound Effect: string
Treadmill:
-
Tiles only
-
Custom properties:
- Direction: string
- Marks the direction the player will travel
- Up Left
- Up Right
- Down Left
- Down Right
- Speed: number? (Tiles per second, default: 1.875)
- Direction: string
Arrow:
-
Tiles only
-
Visible in minimap
-
Custom properties:
- Direction: string
- Up Left
- Up Right
- Down Left
- Down Right
- Direction: string
- Tiles only
- Hides the tile from players, great for invisible pathways
Commented functions are in development and require changes to the client (specified below).
Net:on("tick", function(event)
-- { delta_time: number (seconds) }
print(event.delta_time)
end)
Net:on("authorization", function(event)
-- a player on another server needs to be authenticated with this server
-- the host and port for the other server is provided with the event for custom response / implementation
-- do NOT share identity with other servers, use data for a temporary link between identities without sharing the identity
-- { identity: string, host: string, port: number, data: string }
print(event.identity, event.host, event.port, event.data)
end)
Net:on("player_request", function(event)
-- player requests connection to server (only transfers and kicks should occur here)
-- { player_id: string, data: string }
print(event.player_id, event.data)
end)
Net:on("player_connect", function(event)
-- player connects to the server (good place to setup while the player is loading)
-- { player_id: string }
print(event.player_id)
end)
Net:on("player_join", function(event)
-- player enters their first area after connecting
-- { player_id: string }
print(event.player_id)
end)
Net:on("player_area_transfer", function(event)
-- player changes area
-- { player_id: string }
print(event.player_id)
end)
Net:on("player_disconnect", function(event)
-- the player is invalid after this function excecutes
-- { player_id: string }
print(event.player_id)
end)
Net:on("player_move", function(event)
-- Net.get_player_position(event.player_id) will report the old position
-- { player_id: string, x: number, y: number, z: number }
print(event.player_id, event.x, event.y, event.z)
end)
Net:on("player_avatar_change", function(event)
-- may change in a future update from avatar swapping removal in v2.5
-- health, max_health, and element will be updated on the player before this function executes
-- { player_id: string, texture_path: string, animation_path: string, name: string, element: string, max_health: number, prevent_default: Function }
print(event.player_id, event)
end)
Net:on("player_emote", function(event)
-- { player_id: string, emote: number, prevent_default: Function }
print(event.player_id, event.emote)
end)
Net:on("custom_warp", function(event)
-- player warped out by a "Custom Warp" or "Custom Server Warp"
-- { player_id: string, object_id: number }
print(event.player_id, event.object_id)
end)
Net:on("object_interaction", function(event)
-- { player_id: string, object_id: number, button: number }
print(event.player_id, event.object_id, event.button)
end)
Net:on("actor_interaction", function(event)
-- { player_id: string, actor_id: string, button: number }
-- actor_id is a player or bot id
print(event.player_id, event.actor_id, event.button)
end)
Net:on("tile_interaction", function(event)
-- { player_id: string, x: number, y: number, z: number, button: number }
print(event.player_id, event.x, event.y, event.z, event.button)
end)
Net:on("textbox_response", function(event)
-- { player_id: string, response: number | string }
print(event.player_id, event.response)
end)
Net:on("board_open", function(event)
-- deprecated
print(event.player_id)
end)
Net:on("board_close", function(event)
-- { player_id: string }
print(event.player_id)
end)
Net:on("post_request", function(event)
-- board post request for infinite scroll (UI has exhausted posts)
-- { player_id: string }
print(event.player_id)
end)
Net:on("post_selection", function(event)
-- { player_id: string, post_id: string }
print(event.player_id, event.post_id)
end)
Net:on("shop_close", function(event)
-- { player_id: string }
print(event.player_id)
end)
Net:on("shop_purchase", function(event)
-- { player_id: string, item_name: string }
print(event.player_id, event.item_name)
end)
Net:on("battle_results", function(event)
-- { player_id: string, health: number, score: number, time: number, ran: bool, emotion: number, turns: number, enemies: { id: String, health: number }[] } }
print(event.player_id, event.health, event.time, event.ran, event.emotion, event.turns, event.enemies)
end)
Net:on("server_message", function(event)
-- { host: string, port: number, data: string }
print(event.host, event.port, event.data)
end)
Interactions with the cyberworld are performed through functions attached to a global table called Net
. The APIs defined below are those functions categorized by what they affect.
-- area_id is the filename without extension
-- ./assets/my_area.tmx would be my_area
Net.list_areas() -- area_id[]
-- Net.create_area(new_area_id)
Net.update_area(area_id, map_string)
Net.clone_area(area_id, new_area_id)
Net.remove_area(area_id)
Net.map_to_string(area_id)
Net.get_width(area_id)
Net.get_height(area_id)
Net.get_layer_count(area_id)
Net.get_tile_width(area_id)
Net.get_tile_height(area_id)
Net.get_area_custom_properties(area_id)
Net.get_area_custom_property(area_id, name)
Net.set_area_custom_property(area_id, name, value)
Net.get_area_name(area_id)
Net.set_area_name(area_id)
Net.get_song(area_id) -- song_path
Net.set_song(area_id, song_path)
Net.get_background(area_id) -- { texture_path, animation_path }
Net.get_background_velocity(area_id) -- { x, y }
Net.get_background_parallax(area_id) -- number
Net.set_background(area_id, texture_path, animation_path?, vel_x?, vel_y?, parallax?)
Net.get_foreground(area_id) -- { texture_path, animation_path }
Net.get_foreground_velocity(area_id) -- { x, y }
Net.get_foreground_parallax(area_id) -- number
Net.set_foreground(area_id, texture_path, animation_path?, vel_x?, vel_y?, parallax?)
Net.get_spawn_position(area_id) -- { x, y, z }
Net.set_spawn_position(area_id, x, y, z)
Net.get_spawn_direction(area_id)
Net.set_spawn_direction(area_id, direction)
Net.list_tilesets(area_id) -- tileset_path[]
Net.get_tileset(area_id, tileset_path) -- { path, first_gid }?
Net.get_tileset_for_tile(area_id, tile_gid) -- { path, first_gid }?
Net.get_tile(area_id, x, y, z) -- { gid, flipped_horizontally, flipped_vertically, rotated }
Net.set_tile(area_id, x, y, z, tile_gid, flip_h?, flip_v?, rotate?)
Net.provide_asset(area_id, path)
Net.play_sound(area_id, path)
Net.list_objects(area_id) -- object_id[]
Net.get_object_by_id(area_id, object_id) -- { id, name, class, type, visible, x, y, z, width, height, rotation, data, custom_properties }?
Net.get_object_by_name(area_id, name) -- { id, name, class, visible, x, y, z, width, height, rotation, data, custom_properties }?
Net.create_object(area_id, { name?, type?, visible?, x?, y?, z?, width?, height?, rotation?, data, custom_properties? }) -- object_id
Net.remove_object(area_id, object_id)
Net.set_object_name(area_id, object_id, name)
Net.set_object_class(area_id, object_id, class)
Net.set_object_type(area_id, object_id, type) -- deprecated
Net.set_object_custom_property(area_id, object_id, name, value)
Net.resize_object(area_id, object_id, width, height)
Net.set_object_rotation(area_id, object_id, rotation)
Net.set_object_visibility(area_id, object_id, visibility)
Net.move_object(area_id, object_id, x, y, layer)
Net.set_object_data(area_id, object_id, data)
-- possible values for data:
{
type = "point" | "rect" | "ellipse"
}
{
type = "polygon" | "polyline"
points = { x, y }[],
}
{
type = "tile",
gid, -- int
flipped_horizontally?, -- bool
flipped_vertically?, -- bool
rotated?, -- always false
}
Net.list_bots(area_id) -- bot_id[]
Net.create_bot(bot_id, { name?, area_id?, warp_in?, texture_path?, animation_path?, animation?, x?, y?, z?, direction?, solid? })
Net.create_bot({ name?, area_id?, warp_in?, texture_path?, animation_path?, animation?, x?, y?, z?, direction?, solid? }) -- bot_id
Net.is_bot(bot_id)
Net.remove_bot(bot_id, warp_out?)
Net.get_bot_area(bot_id) -- area_id
Net.get_bot_name(bot_id) -- name
Net.set_bot_name(bot_id, name)
Net.get_bot_direction(bot_id)
Net.set_bot_direction(bot_id, direction)
Net.animate_bot_properties(bot_id, keyframes) -- unstable
Net.get_bot_position(bot_id) -- { x, y, z }
Net.move_bot(bot_id, x, y, z)
-- Net.set_bot_solid(bot_id, solid)
Net.set_bot_avatar(bot_id, texture_path, animation_path)
Net.set_bot_emote(bot_id, emote_id, use_custom_emotes?)
Net.set_bot_minimap_color(bot_id, color) -- color = { r: 0-255, g: 0-255, b: 0-255, a?: 0-255 }
Net.animate_bot(bot_id, state_name, loop?)
Net.transfer_bot(bot_id, area_id, warp_in?, x?, y?, z?)
-- keyframes:
{
properties: {
property: "Animation" | "Animation Speed" | "X" | "Y" | "Z" | "ScaleX" | "ScaleY" | "Rotation" | "Direction" | "Sound Effect" | "Sound Effect Loop",
ease?: "Linear" | "In" | "Out" | "InOut" | "Floor",
value: number | string
}[],
duration: number
}[]
Net.list_players(area_id) -- player_id[]
Net.is_player(player_id)
Net.get_player_area(player_id) -- area_id
Net.get_player_ip(player_id) -- address
Net.get_player_name(player_id) -- name
Net.set_player_name(player_id, name)
Net.get_player_direction(player_id)
Net.get_player_position(player_id) -- { x, y, z }
Net.get_player_mugshot(player_id) -- { texture_path, animation_path }
Net.get_player_avatar(player_id) -- { texture_path, animation_path }
Net.set_player_avatar(player_id, texture_path, animation_path)
Net.set_player_emote(player_id, emote_id, use_custom_emotes?)
Net.exclusive_player_emote(player_id, emoter_id, emote_id, use_custom_emotes?)
Net.set_player_minimap_color(player_id, color) -- color = { r: 0-255, g: 0-255, b: 0-255, a?: 0-255 }
Net.animate_player(player_id, state_name, loop?)
Net.animate_player_properties(player_id, keyframes) -- unstable
Net.is_player_battling(player_id)
Net.is_player_busy(player_id)
Net.provide_asset_for_player(player_id, path)
Net.play_sound_for_player(player_id, path)
Net.exclude_object_for_player(player_id, object_id)
Net.include_object_for_player(player_id, object_id)
Net.exclude_actor_for_player(player_id, actor_id)
Net.include_actor_for_player(player_id, actor_id)
Net.move_player_camera(player_id, x, y, z, holdTimeInSeconds?)
Net.fade_player_camera(player_id, color, durationInSeconds) -- color = { r: 0-255, g: 0-255, b: 0-255, a?: 0-255 }
Net.slide_player_camera(player_id, x, y, z, durationInSeconds)
Net.shake_player_camera(player_id, strength, durationInSeconds)
Net.track_with_player_camera(player_id, actor_id?)
Net.is_player_input_locked(player_id)
Net.unlock_player_camera(player_id)
Net.lock_player_input(player_id)
Net.unlock_player_input(player_id)
Net.teleport_player(player_id, warp, x, y, z, direction?)
Net.offer_package(player_id, package_path)
Net.set_mod_whitelist_for_player(player_id, whitelist_path) -- whitelist has this format: `[md5] [package_id]\n`
Net.initiate_encounter(player_id, package_path, data?) -- data is a table, read as second param in package_build for the encounter package
Net.initiate_pvp(player_1_id, player_2_id, field_script_path?)
Net.transfer_player(player_id, area_id, warp_in?, x?, y?, z?, direction?)
Net.transfer_server(player_id, address, port, warp_out?, data?) -- data = string
Net.request_authorization(player_id, address, port, data?)
Net.kick_player(player_id, reason, warp_out?)
Net.is_player_in_widget(player_id)
Net.is_player_shopping(player_id)
Net.message_player(player_id, message, mug_texture_path?, mug_animation_path?)
Net.question_player(player_id, question, mug_texture_path?, mug_animation_path?)
Net.quiz_player(player_id, option_a?, option_b?, option_c?, mug_texture_path?, mug_animation_path?)
Net.prompt_player(player_id, character_limit?, default_text?)
-- color = { r: 0-255, g: 0-255, b: 0-255 }, posts = { id: string, read: bool?, title: string?, author: string? }[]
-- returns EventEmitter, re-emits post_selection, post_request, board_close
Net.open_board(player_id, board_name, color, posts)
Net.prepend_posts(player_id, posts, post_id?) -- unstable, issues arise when multiple scripts create boards at the same time
Net.append_posts(player_id, posts, post_id?) -- unstable, issues arise when multiple scripts create boards at the same time
Net.remove_post(player_id, post_id) -- unstable, issues arise when multiple scripts create boards at the same time
Net.close_bbs(player_id)
-- items = { name: string, description: string, price: number }[]
-- returns EventEmitter, re-emits shop_purchase, shop_close
Net.open_shop(player_id, items, mug_texture_path?, mug_animation_path?)
Net.get_player_secret(player_id) -- the secret identifier for this player. similar to a password, do not share
Net.get_player_element(player_id) -- string
Net.get_player_health(player_id)
Net.set_player_health(player_id, health)
Net.get_player_max_health(player_id)
Net.set_player_max_health(player_id, health)
Net.get_player_emotion(player_id)
Net.set_player_emotion(player_id, emotion)
Net.get_player_money(player_id)
Net.set_player_money(player_id, money)
Net.get_player_items(player_id) -- string[]
Net.give_player_item(player_id, item_id)
Net.remove_player_item(player_id, item_id)
Net.player_has_item(player_id, item_id)
Net.create_item(item_id, { name, description })
Net.get_item_name(item_id)
Net.get_item_description(item_id)
Net.update_asset(server_path, content)
Net.remove_asset(server_path)
Net.has_asset(server_path)
Net.get_asset_type(server_path)
Net.get_asset_size(server_path)
If you want to use IO while players are connected, you'll want to use the Async API to prevent server hiccups. Note: paths in this section use system paths and not asset paths.
-- promise objects returned by most async functions
promise.and_then(function(value))
Async.await(async_iterator) -- iterator
Async.await(promise) -- value -- for coroutines
Async.await_all(promises) -- values[] -- for coroutines
Async.promisify(coroutine) -- promise
Async.create_promise(function(resolve)) -- promise -- resolve = function(value)
Async.request(url, { method?, headers?, body? }?) -- promise, value = { status, headers, body }?
Async.download(path, url, { method?, headers?, body? }?) -- promise, value = bool
Async.read_file(path) -- promise, value = string
Async.write_file(path, content) -- promise, value = bool
Async.poll_server(address, port) -- promise, value = { max_message_size }?
Async.message_server(address, port, data) -- you will not know if this succeeds, the other server will need to reply
Async.sleep(duration) -- promise, value = nil
Async alternatives to some Net API functions. Promises return nil if the user disconnects.
Async.message_player(player_id, message, mug_texture_path?, mug_animation_path?) -- promise, value = number?
Async.question_player(player_id, question, mug_texture_path?, mug_animation_path?) -- promise, value = number?
Async.quiz_player(player_id, option_a?, option_b?, option_c?, mug_texture_path?, mug_animation_path?) -- promise, value = number?
Async.prompt_player(player_id, character_limit?, default_text?) -- promise, value = string?
Async.initiate_encounter(player_id, package_path, data?) -- promise, value = { player_id: string, health: number, score: number, time: number, ran: bool, emotion: number, turns: number, enemies: { id: String, health: number }[] } }
Async.initiate_pvp(player_1_id, player_2_id, field_script_path?) -- promise, value = { player_id: string, health: number, score: number, time: number, ran: bool, emotion: number, turns: number, enemies: { id: String, health: number }[] } }
local emitter = Net.EventEmitter.new()
emitter:emit(event_name, ...)
emitter:on(event_name, function(...))
emitter:once(event_name, function(...))
emitter:on_any(function(event_name, ...))
emitter:on_any_once(function(event_name, ...))
emitter:remove_listener(event_name, callback)
emitter:remove_on_any_listener(callback)
emitter:async_iter(event_name) -- iterator that returns promises, value = ...
emitter:async_iter_all(event_name) -- iterator that returns promises, value = event_name, ...
emitter:destroy() -- allows async iterators to complete
print
and tostring
will display tables.
printerr
will output red text to stdout.
Windows requires for building lua MSVC++
This project is built with Rust, so after installing Cargo, you can compile and run the project with cargo run
.
If you are interested in understanding the source before making changes, check out the achitecture document.
Install cargo-about: cargo install cargo-about
Run cargo run --bin create_distributable
, a folder named dist
will be created.