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

Reimplement several commands in Lua (#37)

* Reimplement !unlockaction in Lua
Rebased on upstream master

* Unlockaction: display the action id that was unlocked

* Reimplement GM speed in Lua

* Fix warnings and errors

* Run cargo fmt

* Reimplement GM wireframe in Lua

* Reimplement GM invis command, with a caveat
It can't toggle the invis state yet, and I'm not sure where to update it.

* Lua invis: add the gm_invisible toggle, but it still doesn't revert back to false...

* Reimplement GM aetheryte in Lua, with a caveat
It's seemingly not working right now though: it doesn't add any new aetherytes to the teleport menu.
But I can't get the command `//gm aetheryte on X` to do it either, so it's possible Kawari isn't responding correctly?
Either way this needs further testing.

* Lua invis: add the forgotten Lua file

* Reimplement GM teri in Lua
Also add a TODO for UnlockAetheryte

* Make comment in lua.rs more useful

* Run cargo fmt again

* Teri: range check the territory ID

* Update USAGE.md to reflect the new commands
Rebased on upstream master

* Clarify unlockaetheryte USAGE and in-script usage

* Refactor UnlockAetheryte.lua, and make ToggleInvisibility actually work properly.
I opted to create a Task for this, because sticking it in kawari-world.rs felt like a hack to me.

* Run cargo fmt for hopefully the last time today

* Move lua.ra:toggle_invisibility down with the other queued tasks

* Fix spaces in USAGE.md, remove stray rebase message
This commit is contained in:
thedax 2025-06-21 13:30:52 -04:00 committed by GitHub
parent 6561e63fd4
commit 61616df842
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 243 additions and 11 deletions

View file

@ -113,6 +113,10 @@ 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.
### GM commands

View file

@ -86,3 +86,8 @@ registerCommand("nudge", "commands/debug/Nudge.lua")
registerCommand("festival", "commands/debug/Festival.lua")
registerCommand("permtest", "commands/debug/PermissionTest.lua")
registerCommand("reload", "commands/debug/Reload.lua")
registerCommand("unlockaction", "commands/debug/UnlockAction.lua")
registerCommand("wireframe", "commands/debug/ToggleWireframe.lua")
registerCommand("invis", "commands/debug/ToggleInvisibility.lua")
registerCommand("unlockaetheryte", "commands/debug/UnlockAetheryte.lua")
registerCommand("teri", "commands/debug/ChangeTerritory.lua")

View file

@ -0,0 +1,22 @@
required_rank = GM_RANK_DEBUG
function onCommand(args, player)
local parts = split(args)
local argc = table.getn(parts)
local sender = "[teri] "
local usage = "\nThis command moves the user to a new zone/territory.\nUsage: !teri <id>"
if argc == 0 then
player:send_message(sender.."A territory id is required to use this command."..usage)
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
player:send_message(sender.."Error parsing territory id! Make sure your input is an integer between 0 and 65535."..usage)
return
end
player:change_territory(id)
player:send_message(string.format("%s Changing territory to %s.", sender, id))
end

View file

@ -0,0 +1,26 @@
required_rank = GM_RANK_DEBUG
function onCommand(args, player)
local parts = split(args)
local argc = table.getn(parts)
local sender = "[setspeed] "
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
player:send_message(sender.."Error parsing speed multiplier! Make sure the multiplier is an integer."..usage)
return
elseif argc == 0 then
speed_multiplier = 1
end
if speed_multiplier <= 0 then
speed_multiplier = 1
elseif speed_multiplier > SPEED_MAX then
speed_multiplier = SPEED_MAX
end
player:set_speed(speed_multiplier)
player:send_message(string.format("%sSpeed multiplier set to %s.", sender, speed_multiplier))
end

View file

@ -0,0 +1,11 @@
required_rank = GM_RANK_DEBUG
function onCommand(args, player)
local parts = split(args)
local argc = table.getn(parts)
local sender = "[invis] "
local usage = "\nThis command makes the user invisible to all other actors."
player:toggle_invisibility()
player:send_message(sender.."Invisibility toggled.")
end

View file

@ -0,0 +1,11 @@
required_rank = GM_RANK_DEBUG
function onCommand(args, player)
local parts = split(args)
local argc = table.getn(parts)
local sender = "[wireframe] "
local usage = "\nThis command allows the user to view the world in wireframe mode."
player:toggle_wireframe()
player:send_message(sender.."Wireframe mode toggled.")
end

View file

@ -0,0 +1,23 @@
required_rank = GM_RANK_DEBUG
function onCommand(args, player)
local parts = split(args)
local argc = table.getn(parts)
local sender = "[unlockaction] "
local usage = "\nThis command teaches the user an action.\nUsage: !useaction <id>"
if argc < 1 then
player:send_message(sender.."This command requires 1 parameter."..usage)
return
end
local id = tonumber(parts[1])
if not id then
player:send_message(sender.."Error parsing action id! Make sure the id is an integer."..usage)
return
end
player:unlock_action(id)
player:send_message(string.format("%s Action %s unlocked!", sender, id))
end

View file

@ -0,0 +1,39 @@
required_rank = GM_RANK_DEBUG
function onCommand(args, player)
local parts = split(args)
local argc = table.getn(parts)
local sender = "[unlockaetheryte] "
local usage = "\nThis command unlocks an aetheryte for the user.\nUsage: !unlockaetheryte <on/off> <id>"
if argc < 2 then
player:send_message(sender.."This command requires two parameters."..usage)
return
end
local on = parts[1]
if on == "on" then
on = 0
elseif on == "off" then
on = 1
else
player:send_message(sender.."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
player:send_message(sender.."Error parsing id parameter. Must be a territory id or the word 'all'."..usage)
return
end
end
player:unlock_aetheryte(on, id)
player:send_message(string.format("%s Aetheryte(s) %s had their unlocked status changed!", sender, id))
end

View file

@ -1,6 +1,6 @@
use crate::{
inventory::Storage,
ipc::zone::{ActorControlCategory, ActorControlSelf, ChatMessage, GameMasterRank},
ipc::zone::{ChatMessage, GameMasterRank},
world::ToServer,
};
@ -44,16 +44,6 @@ impl ChatHandler {
))
.await;
}
"!unlockaction" => {
let parts: Vec<&str> = chat_message.message.split(' ').collect();
let id = parts[1].parse::<u32>().unwrap();
connection
.actor_control_self(ActorControlSelf {
category: ActorControlCategory::ToggleActionUnlock { id, unlocked: true },
})
.await;
}
"!equip" => {
let (_, name) = chat_message.message.split_once(' ').unwrap();

View file

@ -568,6 +568,26 @@ impl ZoneConnection {
.await;
}
pub async fn toggle_invisibility(&mut self, invisible: bool) {
self.player_data.gm_invisible = invisible;
let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::ActorControlSelf,
timestamp: timestamp_secs(),
data: ServerZoneIpcData::ActorControlSelf(ActorControlSelf {
category: ActorControlCategory::ToggleInvisibility { invisible },
}),
..Default::default()
};
self.send_segment(PacketSegment {
source_actor: self.player_data.actor_id,
target_actor: self.player_data.actor_id,
segment_type: SegmentType::Ipc,
data: SegmentData::Ipc { data: ipc },
})
.await;
}
pub async fn process_lua_player(&mut self, player: &mut LuaPlayer) {
for segment in &player.queued_segments {
self.send_segment(segment.clone()).await;
@ -596,6 +616,9 @@ impl ZoneConnection {
let mut lua = self.lua.lock().unwrap();
load_global_script(&mut lua);
}
Task::ToggleInvisibility { invisible } => {
self.toggle_invisibility(*invisible).await;
}
}
}
player.queued_tasks.clear();

View file

@ -26,6 +26,7 @@ pub enum Task {
SetClassJob { classjob_id: u8 },
WarpAetheryte { aetheryte_id: u32 },
ReloadScripts,
ToggleInvisibility { invisible: bool },
}
#[derive(Default)]
@ -132,6 +133,58 @@ impl LuaPlayer {
self.create_segment_self(op_code, data);
}
fn unlock_action(&mut self, id: u32) {
let op_code = ServerZoneIpcType::ActorControlSelf;
let data = ServerZoneIpcData::ActorControlSelf(ActorControlSelf {
category: ActorControlCategory::ToggleActionUnlock { id, unlocked: true },
});
self.create_segment_self(op_code, data);
}
fn set_speed(&mut self, speed: u16) {
let op_code = ServerZoneIpcType::ActorControlSelf;
let data = ServerZoneIpcData::ActorControlSelf(ActorControlSelf {
category: ActorControlCategory::Flee { speed },
});
self.create_segment_self(op_code, data);
}
fn toggle_wireframe(&mut self) {
let op_code = ServerZoneIpcType::ActorControlSelf;
let data = ServerZoneIpcData::ActorControlSelf(ActorControlSelf {
category: ActorControlCategory::ToggleWireframeRendering(),
});
self.create_segment_self(op_code, data);
}
fn unlock_aetheryte(&mut self, unlocked: u32, id: u32) {
let op_code = ServerZoneIpcType::ActorControlSelf;
let on = unlocked == 0;
if id == 0 {
for i in 1..239 {
let data = ServerZoneIpcData::ActorControlSelf(ActorControlSelf {
category: ActorControlCategory::LearnTeleport {
id: i,
unlocked: on,
},
});
/* Unknown if this will make the server panic from a flood of packets.
* Needs testing once toggling aetherytes actually works. */
self.create_segment_self(op_code.clone(), data);
}
} else {
let data = ServerZoneIpcData::ActorControlSelf(ActorControlSelf {
category: ActorControlCategory::LearnTeleport { id, unlocked: on },
});
self.create_segment_self(op_code, data);
}
}
fn change_territory(&mut self, zone_id: u16) {
self.queued_tasks.push(Task::ChangeTerritory { zone_id });
}
@ -163,6 +216,11 @@ 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,
});
}
}
impl UserData for LuaPlayer {
@ -201,6 +259,26 @@ impl UserData for LuaPlayer {
Ok(())
},
);
methods.add_method_mut("unlock_aetheryte", |_, this, (unlock, id): (u32, u32)| {
this.unlock_aetheryte(unlock, id);
Ok(())
});
methods.add_method_mut("unlock_action", |_, this, action_id: u32| {
this.unlock_action(action_id);
Ok(())
});
methods.add_method_mut("set_speed", |_, this, speed: u16| {
this.set_speed(speed);
Ok(())
});
methods.add_method_mut("toggle_wireframe", |_, this, _: Value| {
this.toggle_wireframe();
Ok(())
});
methods.add_method_mut("toggle_invisibility", |_, this, _: Value| {
this.toggle_invisiblity();
Ok(())
});
methods.add_method_mut("change_territory", |_, this, zone_id: u16| {
this.change_territory(zone_id);
Ok(())