1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-06-30 03:37:45 +00:00

Port GM commands to Lua

This removes a ton of implementation overlap between the two
command systems. For example, we had two different implementations
of unlocking aetherytes which is just unnecessary.

On the flipside, this makes implementing new GM commands just as
easy as writing debug ones. I moved the existing debug Lua
implementations into their GM counterparts and updated the USAGE
accordingly.
This commit is contained in:
Joshua Goins 2025-06-28 09:56:04 -04:00
parent 2cbf70fbe5
commit 6951f9448d
21 changed files with 307 additions and 247 deletions

View file

@ -113,10 +113,6 @@ These special debug commands start with `!` and are custom to Kawari.
* `!nudge <distance> <up/down (optional)>`: Teleport forward, back, up or down `distance` yalms. Specifying up or down will move the player up or down instead of forward or back. Examples: `!nudge 5 up` to move up 5 yalms, `!nudge 5` to move forward 5 yalms, `!nudge -5` to move backward 5 yalms.
* `!festival <id1> <id2> <id3> <id4>`: Sets the festival in the current zone. Multiple festivals can be set together to create interesting effects.
* `!reload`: Reloads `Global.lua` that is normally only loaded once at start-up.
* `!wireframe: Toggles the hidden GM wireframe rendering mode on or off.`
* `!invis: Toggles the hidden GM invisibility mode on or off for your character.`
* `!unlockaetheryte <on/off> <id>`: Unlock an aetheryte. For the first parameter, the literal words are required.
* `!teri <id>`: Changes to the specified territory.
* `!finishevent`: Forcefully finishes the current event, useful if the script has an error and you're stuck talking to something.
### GM commands

View file

@ -1,16 +1,41 @@
DBG_DIR = "commands/debug/"
GM_DIR = "commands/gm/"
-- GM commands
GM_SET_LEVEL = 1
GM_CHANGE_WEATHER = 6
GM_SPEED = 9
GM_INVISIBILITY = 13
GM_AETHERYTE = 94
GM_EXP = 104
GM_ORCHESTRION = 116
GM_GIVE_ITEM = 200
GM_GIL = 201
GM_WIREFRAME = 550
GM_TERRITORY = 600
GM_TERRITORY_INFO = 605
registerGMCommand(GM_SET_LEVEL, GM_DIR.."SetLevel.lua")
registerGMCommand(GM_CHANGE_WEATHER, GM_DIR.."ChangeWeather.lua")
registerGMCommand(GM_SPEED, GM_DIR.."SetSpeed.lua")
registerGMCommand(GM_INVISIBILITY, GM_DIR.."ToggleInvisibility.lua")
registerGMCommand(GM_AETHERYTE, GM_DIR.."UnlockAetheryte.lua")
registerGMCommand(GM_EXP, GM_DIR.."Exp.lua")
registerGMCommand(GM_ORCHESTRION, GM_DIR.."Orchestrion.lua")
registerGMCommand(GM_GIVE_ITEM, GM_DIR.."GiveItem.lua")
registerGMCommand(GM_GIL, GM_DIR.."Gil.lua")
registerGMCommand(GM_WIREFRAME, GM_DIR.."ToggleWireframe.lua")
registerGMCommand(GM_TERRITORY, GM_DIR.."ChangeTerritory.lua")
registerGMCommand(GM_TERRITORY_INFO, GM_DIR.."TerritoryInfo.lua")
-- Debug commands
-- Please keep these in alphabetical order!
registerCommand("classjob", DBG_DIR.."ClassJob.lua")
registerCommand("festival", DBG_DIR.."Festival.lua")
registerCommand("invis", DBG_DIR.."ToggleInvisibility.lua")
registerCommand("nudge", DBG_DIR.."Nudge.lua")
registerCommand("ost", DBG_DIR.."OnScreenTest.lua")
registerCommand("permtest", DBG_DIR.."PermissionTest.lua")
registerCommand("setpos", DBG_DIR.."SetPos.lua")
registerCommand("setspeed", DBG_DIR.."SetSpeed.lua")
registerCommand("teri", DBG_DIR.."ChangeTerritory.lua")
registerCommand("unlock", DBG_DIR.."Unlock.lua")
registerCommand("unlockaetheryte", DBG_DIR.."UnlockAetheryte.lua")
registerCommand("wireframe", DBG_DIR.."ToggleWireframe.lua")

View file

@ -1,23 +0,0 @@
required_rank = GM_RANK_DEBUG
command_sender = "[teri] "
function onCommand(args, player)
local parts = split(args)
local argc = #parts
local usage = "\nThis command moves the user to a new zone/territory.\nUsage: !teri <id>"
if argc == 0 then
printf(player, "A territory id is required to use this command."..usage)
return
end
local id = tonumber(parts[1])
if not id or id < 0 or id > 65535 then -- Must be in range of unsigned 16-bit value
printf(player, "Error parsing territory id! Make sure your input is an integer between 0 and 65535."..usage)
return
end
player:change_territory(id)
printf(player, "Changing territory to %s.", id)
end

View file

@ -1,39 +0,0 @@
required_rank = GM_RANK_DEBUG
command_sender = "[unlockaetheryte] "
function onCommand(args, player)
local parts = split(args)
local argc = #parts
local usage = "\nThis command unlocks an aetheryte for the user.\nUsage: !unlockaetheryte <on/off> <id>"
if argc < 2 then
printf(player, "This command requires two parameters."..usage)
return
end
local on = parts[1]
if on == "on" then
on = 1
elseif on == "off" then
on = 0
else
printf(player, "Error parsing first parameter. Must be either of the words: 'on' or 'off'."..usage)
return
end
local id = tonumber(parts[2])
if not id then
id = parts[2]
if id == "all" then
id = 0
else
printf(player, "Error parsing id parameter. Must be an aetheryte id or the word 'all'."..usage)
return
end
end
player:unlock_aetheryte(on, id)
printf(player, "Aetheryte(s) %s had their unlocked status changed!", id)
end

View file

@ -0,0 +1,9 @@
required_rank = GM_RANK_DEBUG
command_sender = "[teri] "
function onCommand(args, player)
local id = args[1]
player:change_territory(id)
printf(player, "Changing territory to %s.", id)
end

View file

@ -0,0 +1,9 @@
required_rank = GM_RANK_DEBUG
command_sender = "[weather] "
function onCommand(args, player)
local id = args[1]
player:change_weather(id)
printf(player, "Changing weather to %s.", id)
end

View file

View file

@ -0,0 +1,9 @@
required_rank = GM_RANK_DEBUG
command_sender = "[gil] "
function onCommand(args, player)
local amount = args[1]
player:add_gil(amount)
printf(player, "Added %s gil.", amount)
end

View file

@ -0,0 +1,10 @@
required_rank = GM_RANK_DEBUG
command_sender = "[orchestrion] "
function onCommand(args, player)
local on = args[1] -- TODO: reverse
local id = args[2]
player:unlock_aetheryte(on, id)
printf(player, "Orchestrion(s) %s had their unlocked status changed!", id)
end

View file

@ -0,0 +1,5 @@
required_rank = GM_RANK_DEBUG
function onCommand(args, player)
player:set_level(args[1])
end

View file

@ -2,18 +2,8 @@ required_rank = GM_RANK_DEBUG
command_sender = "[setspeed] "
function onCommand(args, player)
local parts = split(args)
local argc = #parts
local usage = "\nThis command sets the user's speed to a desired multiplier.\nUsage: !setspeed <multiplier>"
local SPEED_MAX = 10 -- Arbitrary, but it's more or less unplayable even at this amount
local speed_multiplier = tonumber(parts[1])
if argc == 1 and not speed_multiplier then
printf(player, "Error parsing speed multiplier! Make sure the multiplier is an integer."..usage)
return
elseif argc == 0 then
speed_multiplier = 1
end
local speed_multiplier = args[1]
if speed_multiplier <= 0 then
speed_multiplier = 1

View file

@ -0,0 +1,11 @@
required_rank = GM_RANK_DEBUG
command_sender = "[teri_info] "
function onCommand(args, player)
local teri_info = "Territory Info for zone "..player.zone.id..":"
local current_weather = "Current weather: "..player.zone.weather_id
local internal_name = "Internal name: "..player.zone.internal_name
local region_name = "Region name: "..player.zone.region_name
local place_name = "Place name: "..player.zone.place_name
printf(player, teri_info.."\n"..current_weather.."\n"..internal_name.."\n"..region_name.."\n"..place_name)
end

View file

@ -2,8 +2,6 @@ required_rank = GM_RANK_DEBUG
command_sender = "[invis] "
function onCommand(args, player)
local usage = "\nThis command makes the user invisible to all other actors."
player:toggle_invisibility()
printf(player, "Invisibility toggled.")
end

View file

@ -2,8 +2,6 @@ required_rank = GM_RANK_DEBUG
command_sender = "[wireframe] "
function onCommand(args, player)
local usage = "\nThis command allows the user to view the world in wireframe mode."
player:toggle_wireframe()
printf(player, "Wireframe mode toggled.")
end

View file

@ -0,0 +1,10 @@
required_rank = GM_RANK_DEBUG
command_sender = "[unlockaetheryte] "
function onCommand(args, player)
local on = args[1] -- TODO: reverse
local id = args[2]
player:unlock_aetheryte(on, id)
printf(player, "Aetheryte(s) %s had their unlocked status changed!", id)
end

View file

@ -3,24 +3,23 @@ use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use kawari::RECEIVE_BUFFER_SIZE;
use kawari::common::{GameData, TerritoryNameKind, timestamp_secs};
use kawari::common::{Position, value_to_flag_byte_index_value};
use kawari::common::Position;
use kawari::common::{GameData, timestamp_secs};
use kawari::config::get_config;
use kawari::inventory::{Item, Storage};
use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment};
use kawari::ipc::zone::{
ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList,
};
use kawari::ipc::zone::{
ClientTriggerCommand, ClientZoneIpcData, CommonSpawn, EventStart, GameMasterCommandType,
GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
ClientTriggerCommand, ClientZoneIpcData, CommonSpawn, EventStart, GameMasterRank, OnlineStatus,
ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
};
use kawari::opcodes::{ServerChatIpcType, ServerZoneIpcType};
use kawari::packet::oodle::OodleNetwork;
use kawari::packet::{
ConnectionType, PacketSegment, PacketState, SegmentData, SegmentType, send_keep_alive,
};
use kawari::world::{ChatHandler, ExtraLuaState, Zone, ZoneConnection, load_init_script};
use kawari::world::{ChatHandler, ExtraLuaState, LuaZone, Zone, ZoneConnection, load_init_script};
use kawari::world::{
ClientHandle, Event, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer,
WorldDatabase, handle_custom_ipc, server_main_loop,
@ -617,136 +616,70 @@ async fn client_loop(
}
}
ClientZoneIpcData::GMCommand { command, arg0, arg1, .. } => {
tracing::info!("Got a game master command!");
let lua = lua.lock().unwrap();
let state = lua.app_data_ref::<ExtraLuaState>().unwrap();
match &command {
GameMasterCommandType::SetLevel => {
connection.player_data.set_current_level(*arg0 as i32);
connection.update_class_info().await;
}
GameMasterCommandType::ChangeWeather => {
connection.change_weather(*arg0 as u16).await
}
GameMasterCommandType::Speed => {
// TODO: Maybe allow setting the speed of a targeted player too?
connection
.actor_control_self(ActorControlSelf {
category:
ActorControlCategory::Flee {
speed: *arg0 as u16,
},
})
.await
}
GameMasterCommandType::ChangeTerritory => {
connection.change_zone(*arg0 as u16).await
}
GameMasterCommandType::ToggleInvisibility => {
connection.player_data.gm_invisible = !connection.player_data.gm_invisible;
connection
.actor_control_self(ActorControlSelf {
category:
ActorControlCategory::ToggleInvisibility {
invisible: connection.player_data.gm_invisible,
},
})
.await
}
GameMasterCommandType::ToggleWireframe => connection
.actor_control_self(ActorControlSelf {
category:
ActorControlCategory::ToggleWireframeRendering(),
})
.await,
GameMasterCommandType::GiveItem => {
connection.player_data.inventory.add_in_next_free_slot(Item { id: *arg0, quantity: 1 });
connection.send_inventory(false).await;
}
GameMasterCommandType::Orchestrion => {
let on = *arg0 == 1; // This command uses 1 for on, 2 for off.
let id = *arg1 as u16;
if let Some(command_script) =
state.gm_command_scripts.get(command)
{
let file_name = format!(
"{}/{}",
&config.world.scripts_location, command_script
);
// id == 0 means "all"
if id == 0 {
/* Currently 792 songs ingame.
* Commented out because this learns literally zero songs
* for some unknown reason. */
/*for i in 1..793 {
let idd = i as u16;
connection.send_message("test!").await;
connection.actor_control_self(ActorControlSelf {
category: ActorControlCategory::ToggleOrchestrionUnlock { song_id: id, unlocked: on } }).await;
}*/
} else {
connection.actor_control_self(ActorControlSelf {
category: ActorControlCategory::ToggleOrchestrionUnlock { song_id: id, unlocked: on }
}).await;
}
}
GameMasterCommandType::Aetheryte => {
let on = *arg0 == 0;
let id = *arg1;
let mut run_script = || -> mlua::Result<()> {
lua.scope(|scope| {
let connection_data = scope
.create_userdata_ref_mut(&mut lua_player)?;
// id == 0 means "all"
if id == 0 {
for i in 1..239 {
let (value, index) = value_to_flag_byte_index_value(i);
if on {
connection.player_data.aetherytes[index as usize] |= value;
} else {
connection.player_data.aetherytes[index as usize] ^= value;
lua.load(
std::fs::read(&file_name)
.expect(format!("Failed to load script file {}!", &file_name).as_str()),
)
.set_name("@".to_string() + &file_name)
.exec()?;
let required_rank = lua.globals().get("required_rank");
if let Err(error) = required_rank {
tracing::info!("Script is missing required_rank! Unable to run command, sending error to user. Additional information: {}", error);
let func: Function =
lua.globals().get("onCommandRequiredRankMissingError")?;
func.call::<()>((error.to_string(), connection_data))?;
return Ok(());
}
/* Reset state for future commands. Without this it'll stay set to the last value
* and allow other commands that omit required_rank to run, which is undesirable. */
lua.globals().set("required_rank", mlua::Value::Nil)?;
if connection.player_data.gm_rank as u8 >= required_rank? {
let func: Function =
lua.globals().get("onCommand")?;
func.call::<()>(([*arg0, *arg1], connection_data))?;
/* `command_sender` is an optional variable scripts can define to identify themselves in print messages.
* It's okay if this global isn't set. We also don't care what its value is, just that it exists.
* This is reset -after- running the command intentionally. Resetting beforehand will never display the command's identifier.
*/
let command_sender: Result<mlua::prelude::LuaValue, mlua::prelude::LuaError> = lua.globals().get("command_sender");
if let Ok(_) = command_sender {
lua.globals().set("command_sender", mlua::Value::Nil)?;
}
connection.actor_control_self(ActorControlSelf {
category: ActorControlCategory::LearnTeleport { id: i, unlocked: on } }).await;
}
} else {
let (value, index) = value_to_flag_byte_index_value(id);
if on {
connection.player_data.aetherytes[index as usize] |= value;
Ok(())
} else {
connection.player_data.aetherytes[index as usize] ^= value;
tracing::info!("User with account_id {} tried to invoke GM command {} with insufficient privileges!",
connection.player_data.account_id, command);
let func: Function =
lua.globals().get("onCommandRequiredRankInsufficientError")?;
func.call::<()>(connection_data)?;
Ok(())
}
})
};
connection.actor_control_self(ActorControlSelf {
category: ActorControlCategory::LearnTeleport { id, unlocked: on } }).await;
}
if let Err(err) = run_script() {
tracing::warn!("Lua error in {file_name}: {:?}", err);
}
GameMasterCommandType::EXP => {
let amount = *arg0;
connection.player_data.set_current_exp(connection.player_data.current_exp() + amount);
connection.update_class_info().await;
}
GameMasterCommandType::TerritoryInfo => {
let id: u32 = connection.zone.as_ref().unwrap().id.into();
let weather_id;
let internal_name;
let place_name;
let region_name;
{
let mut game_data = connection.gamedata.lock().unwrap();
// TODO: Maybe the current weather should be cached somewhere like the zone id?
weather_id = game_data
.get_weather(id)
.unwrap_or(1) as u16;
let fallback = "<Unable to load name!>";
internal_name = game_data.get_territory_name(id, TerritoryNameKind::Internal).unwrap_or(fallback.to_string());
region_name = game_data.get_territory_name(id, TerritoryNameKind::Region).unwrap_or(fallback.to_string());
place_name = game_data.get_territory_name(id, TerritoryNameKind::Place).unwrap_or(fallback.to_string());
}
connection.send_message(format!(concat!("Territory Info for zone {}:\n",
"Current weather: {}\n",
"Internal name: {}\n",
"Region name: {}\n",
"Place name: {}"), id, weather_id, internal_name, region_name, place_name).as_str()).await;
},
GameMasterCommandType::Gil => {
let amount = *arg0;
connection.player_data.inventory.currency.get_slot_mut(0).quantity += amount;
connection.send_inventory(false).await;
},
}
}
ClientZoneIpcData::ZoneJump {
@ -770,7 +703,7 @@ async fn client_loop(
// find the pop range on the other side
let mut game_data = game_data.lock().unwrap();
let new_zone = Zone::load(&mut game_data.game_data, exit_box.territory_type);
let new_zone = Zone::load(&mut game_data, exit_box.territory_type);
let (destination_object, _) = new_zone
.find_pop_range(exit_box.destination_instance_id)
.unwrap();
@ -977,6 +910,16 @@ async fn client_loop(
// update lua player
lua_player.player_data = connection.player_data.clone();
lua_player.status_effects = connection.status_effects.clone();
if let Some(zone) = &connection.zone {
lua_player.zone_data = LuaZone {
zone_id: zone.id,
weather_id: connection.weather_id,
internal_name: zone.internal_name.clone(),
region_name: zone.region_name.clone(),
place_name: zone.place_name.clone(),
};
}
}
},
Err(_) => {
@ -1092,7 +1035,8 @@ async fn main() {
exit_position: None,
exit_rotation: None,
last_keep_alive: Instant::now(),
gracefully_logged_out: false
gracefully_logged_out: false,
weather_id: 0,
});
}
Some((mut socket, _)) = handle_rcon(&rcon_listener) => {

View file

@ -148,24 +148,6 @@ impl Default for ServerZoneIpcSegment {
}
}
#[binrw]
#[brw(repr = u8)]
#[derive(Clone, PartialEq, Debug)]
pub enum GameMasterCommandType {
SetLevel = 0x1,
ChangeWeather = 0x6,
Speed = 0x9,
ToggleInvisibility = 0xD,
ToggleWireframe = 0x26,
ChangeTerritory = 0x58,
EXP = 0x68,
Orchestrion = 0x74,
GiveItem = 0xC8,
Aetheryte = 0x5E,
TerritoryInfo = 0x25D,
Gil = 0xC9,
}
#[binrw]
#[br(import(magic: &ServerZoneIpcType, _size: &u32))]
#[derive(Debug, Clone)]
@ -405,8 +387,7 @@ pub enum ClientZoneIpcData {
/// Sent by the client when they send a GM command. This can only be sent by the client if they are sent a GM rank.
#[br(pre_assert(*magic == ClientZoneIpcType::GMCommand))]
GMCommand {
#[brw(pad_after = 3)] // padding
command: GameMasterCommandType,
command: u32,
arg0: u32,
arg1: u32,
arg2: u32,

View file

@ -14,7 +14,7 @@ use crate::{
GameData, ObjectId, ObjectTypeId, Position, timestamp_secs, value_to_flag_byte_index_value,
},
config::{WorldConfig, get_config},
inventory::{ContainerType, Inventory, Item},
inventory::{ContainerType, Inventory, Item, Storage},
ipc::{
chat::ServerChatIpcSegment,
zone::{
@ -46,6 +46,7 @@ pub struct ExtraLuaState {
pub action_scripts: HashMap<u32, String>,
pub event_scripts: HashMap<u32, String>,
pub command_scripts: HashMap<String, String>,
pub gm_command_scripts: HashMap<u32, String>,
}
#[derive(Debug, Default, Clone)]
@ -132,6 +133,9 @@ pub struct ZoneConnection {
/// Whether the player was gracefully logged out
pub gracefully_logged_out: bool,
// TODO: really needs to be moved somewhere else
pub weather_id: u16,
}
impl ZoneConnection {
@ -350,7 +354,7 @@ impl ZoneConnection {
// load the new zone now
{
let mut game_data = self.gamedata.lock().unwrap();
self.zone = Some(Zone::load(&mut game_data.game_data, new_zone_id));
self.zone = Some(Zone::load(&mut game_data, new_zone_id));
}
self.player_data.zone_id = new_zone_id;
@ -362,10 +366,9 @@ impl ZoneConnection {
{
let config = get_config();
let weather_id;
{
let mut game_data = self.gamedata.lock().unwrap();
weather_id = game_data
self.weather_id = game_data
.get_weather(self.zone.as_ref().unwrap().id.into())
.unwrap_or(1) as u16;
}
@ -375,7 +378,7 @@ impl ZoneConnection {
timestamp: timestamp_secs(),
data: ServerZoneIpcData::InitZone(InitZone {
territory_type: self.zone.as_ref().unwrap().id,
weather_id,
weather_id: self.weather_id,
obsfucation_mode: if config.world.enable_packet_obsfucation {
OBFUSCATION_ENABLED_MODE
} else {
@ -405,7 +408,7 @@ impl ZoneConnection {
.get_warp(warp_id)
.expect("Failed to find the warp!");
let new_zone = Zone::load(&mut game_data.game_data, zone_id);
let new_zone = Zone::load(&mut game_data, zone_id);
// find it on the other side
if let Some((object, _)) = new_zone.find_pop_range(pop_range_id) {
@ -438,7 +441,7 @@ impl ZoneConnection {
.get_aetheryte(aetheryte_id)
.expect("Failed to find the aetheryte!");
let new_zone = Zone::load(&mut game_data.game_data, zone_id);
let new_zone = Zone::load(&mut game_data, zone_id);
// find it on the other side
if let Some((object, _)) = new_zone.find_pop_range(pop_range_id) {
@ -462,6 +465,8 @@ impl ZoneConnection {
}
pub async fn change_weather(&mut self, new_weather_id: u16) {
self.weather_id = new_weather_id;
let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::WeatherId,
timestamp: timestamp_secs(),
@ -729,6 +734,39 @@ impl ZoneConnection {
.await;
}
}
Task::SetLevel { level } => {
self.player_data.set_current_level(*level);
self.update_class_info().await;
}
Task::ChangeWeather { id } => {
self.change_weather(*id).await;
}
Task::AddGil { amount } => {
self.player_data.inventory.currency.get_slot_mut(0).quantity += *amount as u32;
self.send_inventory(false).await;
}
Task::UnlockOrchestrion { id, on } => {
// id == 0 means "all"
if *id == 0 {
/* Currently 792 songs ingame.
* Commented out because this learns literally zero songs
* for some unknown reason. */
/*for i in 1..793 {
let idd = i as u16;
connection.send_message("test!").await;
connection.actor_control_self(ActorControlSelf {
category: ActorControlCategory::ToggleOrchestrionUnlock { song_id: id, unlocked: on } }).await;
}*/
} else {
self.actor_control_self(ActorControlSelf {
category: ActorControlCategory::ToggleOrchestrionUnlock {
song_id: *id,
unlocked: *on,
},
})
.await;
}
}
}
}
player.queued_tasks.clear();

View file

@ -29,6 +29,29 @@ pub enum Task {
ToggleInvisibility { invisible: bool },
Unlock { id: u32 },
UnlockAetheryte { id: u32, on: bool },
SetLevel { level: i32 },
ChangeWeather { id: u16 },
AddGil { amount: u32 },
UnlockOrchestrion { id: u16, on: bool },
}
#[derive(Default, Clone)]
pub struct LuaZone {
pub zone_id: u16,
pub weather_id: u16,
pub internal_name: String,
pub region_name: String,
pub place_name: String,
}
impl UserData for LuaZone {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, this| Ok(this.zone_id));
fields.add_field_method_get("weather_id", |_, this| Ok(this.weather_id));
fields.add_field_method_get("internal_name", |_, this| Ok(this.internal_name.clone()));
fields.add_field_method_get("region_name", |_, this| Ok(this.region_name.clone()));
fields.add_field_method_get("place_name", |_, this| Ok(this.place_name.clone()));
}
}
#[derive(Default)]
@ -37,6 +60,7 @@ pub struct LuaPlayer {
pub status_effects: StatusEffects,
pub queued_segments: Vec<PacketSegment<ServerZoneIpcSegment>>,
pub queued_tasks: Vec<Task>,
pub zone_data: LuaZone,
}
impl LuaPlayer {
@ -197,11 +221,31 @@ impl LuaPlayer {
fn reload_scripts(&mut self) {
self.queued_tasks.push(Task::ReloadScripts);
}
fn toggle_invisiblity(&mut self) {
self.queued_tasks.push(Task::ToggleInvisibility {
invisible: !self.player_data.gm_invisible,
});
}
fn set_level(&mut self, level: i32) {
self.queued_tasks.push(Task::SetLevel { level });
}
fn change_weather(&mut self, id: u16) {
self.queued_tasks.push(Task::ChangeWeather { id });
}
fn add_gil(&mut self, amount: u32) {
self.queued_tasks.push(Task::AddGil { amount });
}
fn unlock_orchestrion(&mut self, unlocked: u32, id: u16) {
self.queued_tasks.push(Task::UnlockOrchestrion {
id,
on: unlocked == 1,
});
}
}
impl UserData for LuaPlayer {
@ -317,6 +361,22 @@ impl UserData for LuaPlayer {
this.reload_scripts();
Ok(())
});
methods.add_method_mut("set_level", |_, this, level: i32| {
this.set_level(level);
Ok(())
});
methods.add_method_mut("change_weather", |_, this, id: u16| {
this.change_weather(id);
Ok(())
});
methods.add_method_mut("add_gil", |_, this, amount: u32| {
this.add_gil(amount);
Ok(())
});
methods.add_method_mut("unlock_orchestrion", |_, this, (unlock, id): (u32, u16)| {
this.unlock_orchestrion(unlock, id);
Ok(())
});
}
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
@ -332,6 +392,7 @@ impl UserData for LuaPlayer {
});
fields.add_field_method_get("rotation", |_, this| Ok(this.player_data.rotation));
fields.add_field_method_get("position", |_, this| Ok(this.player_data.position));
fields.add_field_method_get("zone", |_, this| Ok(this.zone_data.clone()));
}
}
@ -441,11 +502,22 @@ pub fn load_init_script(lua: &mut Lua) -> mlua::Result<()> {
Ok(())
})?;
let register_gm_command_func =
lua.create_function(|lua, (command_type, command_script): (u32, String)| {
let mut state = lua.app_data_mut::<ExtraLuaState>().unwrap();
let _ = state
.gm_command_scripts
.insert(command_type, command_script);
Ok(())
})?;
lua.set_app_data(ExtraLuaState::default());
lua.globals().set("registerAction", register_action_func)?;
lua.globals().set("registerEvent", register_event_func)?;
lua.globals()
.set("registerCommand", register_command_func)?;
lua.globals()
.set("registerGMCommand", register_gm_command_func)?;
let effectsbuilder_constructor = lua.create_function(|_, ()| Ok(EffectsBuilder::default()))?;
lua.globals()

View file

@ -11,7 +11,7 @@ mod database;
pub use database::{CharacterData, WorldDatabase};
mod lua;
pub use lua::{EffectsBuilder, LuaPlayer, load_init_script};
pub use lua::{EffectsBuilder, LuaPlayer, LuaZone, load_init_script};
mod event;
pub use event::Event;

View file

@ -1,16 +1,20 @@
use icarus::TerritoryType::TerritoryTypeSheet;
use physis::{
common::Language,
gamedata::GameData,
layer::{
ExitRangeInstanceObject, InstanceObject, LayerEntryData, LayerGroup, PopRangeInstanceObject,
},
};
use crate::common::{GameData, TerritoryNameKind};
/// Represents a loaded zone
#[derive(Default, Debug)]
pub struct Zone {
pub id: u16,
pub internal_name: String,
pub region_name: String,
pub place_name: String,
planevent: Option<LayerGroup>,
vfx: Option<LayerGroup>,
planmap: Option<LayerGroup>,
@ -27,7 +31,8 @@ impl Zone {
..Default::default()
};
let sheet = TerritoryTypeSheet::read_from(game_data, Language::None).unwrap();
let sheet =
TerritoryTypeSheet::read_from(&mut game_data.game_data, Language::None).unwrap();
let Some(row) = sheet.get_row(id as u32) else {
tracing::warn!("Invalid zone id {id}, allowing anyway...");
return zone;
@ -42,7 +47,7 @@ impl Zone {
let mut load_lgb = |name: &str| -> Option<LayerGroup> {
let path = format!("bg/{}/level/{}.lgb", &bg_path[..level_index], name);
let lgb_file = game_data.extract(&path)?;
let lgb_file = game_data.game_data.extract(&path)?;
tracing::info!("Loading {path}");
let lgb = LayerGroup::from_existing(&lgb_file);
if lgb.is_none() {
@ -61,6 +66,18 @@ impl Zone {
zone.sound = load_lgb("sound");
zone.planlive = load_lgb("planlive");
// load names
let fallback = "<Unable to load name!>";
zone.internal_name = game_data
.get_territory_name(id as u32, TerritoryNameKind::Internal)
.unwrap_or(fallback.to_string());
zone.region_name = game_data
.get_territory_name(id as u32, TerritoryNameKind::Region)
.unwrap_or(fallback.to_string());
zone.place_name = game_data
.get_territory_name(id as u32, TerritoryNameKind::Place)
.unwrap_or(fallback.to_string());
zone
}