ArthurCose / Scriptable-OpenNetBattle-Server

An OpenNetBattle Server with Lua scripting support.
GNU General Public License v3.0
9 stars 7 forks source link

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.

Assets

Types of assets:

Paths

Areas

Maps for areas are stored in ./areas. The first area players will see is default.tmx (required).

Suggested Settings

Editor:

Map:

Tilesets:

Layers:

Custom properties

Map:

Tiles:

Object and Tile Classes

Classes are used to denote special tiles or objects understood by the client.

Home Warp

Position Warp:

Server Warp:

Custom Server Warp:

Custom Warp:

Board:

Shop:

Stairs:

Conveyor:

Ice:

Treadmill:

Arrow:

Invisible

Lua API

Commented functions are in development and require changes to the client (specified below).

Net Events

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)

Net API

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 API

-- 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)

Object API

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
}

Bot API

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
}[]

Player API

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?)

Widget API

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?)

Player Data API

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)

Asset API

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)

Async API

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

Asyncified Net API

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 }[] } }

Event Emitters

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

Lua STD Changes

print and tostring will display tables.

printerr will output red text to stdout.

Building the Project

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.

Distributing

Install cargo-about: cargo install cargo-about

Run cargo run --bin create_distributable, a folder named dist will be created.