PDA

View Full Version : Controllable boats


realityincarnate
04-18-2009, 12:34 AM
It took a bit of messing with, but I finally got the player controllable boats to work (more or less) properly. The biggest issue is that the boats seem to be really picky about their starting coordinates before they let themselves be moved. All the boats in Lake Rathetear work properly, but the one in Tox forest only lets you turn it and the one in Qeynos Hills doesn't let you move it at all. I was able to get both these boats working by tinkering with their spawn coordinates, but then I updated my database and lost the working coords. The code also works with SOF, except that the boats spawn in slightly different positions, so even the Lake Rathe ones need to be moved a bit. Anyway, here's the server code:

Index: EQEmuServer/common/eq_packet_structs.h
================================================== =================
--- EQEmuServer/common/eq_packet_structs.h (revision 433)
+++ EQEmuServer/common/eq_packet_structs.h (working copy)
@@ -3727,6 +3727,13 @@
uint16 GuildID;
} GroupLFPMemberEntry;

+struct ControlBoat_Struct {
+/*000*/ uint32 boatId; // entitylist id of the boat
+/*004*/ bool TakeControl; // 01 if taking control, 00 if releasing it
+/*007*/ // no idea what these last three bytes represent
+};
+
+
//old structures live here:
#include "eq_old_structs.h"

Index: EQEmuServer/utils/patch_Titanium.conf
================================================== =================
--- EQEmuServer/utils/patch_Titanium.conf (revision 433)
+++ EQEmuServer/utils/patch_Titanium.conf (working copy)
@@ -258,7 +258,7 @@
OP_ClientError=0x0000
OP_DeleteItem=0x4d81
OP_DeleteCharge=0x1c4a
-OP_ControlBoat=0x0000
+OP_ControlBoat=0x2c81
OP_DumpName=0x0000
OP_FeignDeath=0x7489
OP_Heartbeat=0x0000
Index: EQEmuServer/zone/client.h
================================================== =================
--- EQEmuServer/zone/client.h (revision 433)
+++ EQEmuServer/zone/client.h (working copy)
@@ -971,7 +971,7 @@
int16 weight;
bool berserk;
bool dead;
- bool IsOnBoat;
+ int16 BoatID;
bool IsTracking;
int16 CustomerID;
bool Trader;
Index: EQEmuServer/zone/client_packet.cpp
================================================== =================
--- EQEmuServer/zone/client_packet.cpp (revision 433)
+++ EQEmuServer/zone/client_packet.cpp (working copy)
@@ -947,9 +947,29 @@
}
PlayerPositionUpdateClient_Struct* ppu = (PlayerPositionUpdateClient_Struct*)app->pBuffer;

- if(ppu->spawn_id != GetID())
- return;
+ if(ppu->spawn_id != GetID()) {
+ // check if the id is for a boat the player is controlling
+ if (ppu->spawn_id == BoatID) {
+ Mob* boat = entity_list.GetMob(BoatID);
+ if (boat == 0) { // if the boat ID is invalid, reset the id and abort
+ BoatID = 0;
+ return;
+ }

+ // set the boat's position deltas
+ boat->SetDeltas(ppu->delta_x, ppu->delta_y, ppu->delta_z, ppu->delta_heading);
+ // send an update to everyone nearby except the client controlling the boat
+ EQApplicationPacket* outapp = new EQApplicationPacket(OP_ClientUpdate, sizeof(PlayerPositionUpdateServer_Struct));
+ PlayerPositionUpdateServer_Struct* ppus = (PlayerPositionUpdateServer_Struct*)outapp->pBuffer;
+ boat->MakeSpawnUpdate(ppus);
+ entity_list.QueueCloseClients(boat,outapp,true,300 ,this,false);
+ safe_delete(outapp);
+ // update the boat's position on the server, without sending an update
+ boat->GMMove(ppu->x_pos, ppu->y_pos, ppu->z_pos, EQ19toFloat(ppu->heading), false);
+ return;
+ }
+ else return; // if not a boat, do nothing
+ }

float dist = 0;
float tmp;
@@ -3595,19 +3615,25 @@
void Client::Handle_OP_BoardBoat(const EQApplicationPacket *app)
{
char *boatname;
- this->IsOnBoat=true;
- boatname = new char[app->size-4];
- memset(boatname, 0, app->size-4);
+ boatname = new char[app->size-3];
+ memset(boatname, 0, app->size-3);
memcpy(boatname, app->pBuffer, app->size-4);
- printf("%s has gotten on the boat %s\n",GetName(),boatname);
- //Mob* boat = entity_list.GetMob(boatname);
+
+ Mob* boat = entity_list.GetMob(boatname);
+ if (boat)
+ this->BoatID = boat->GetID(); // set the client's BoatID to show that it's on this boat
safe_delete(boatname);
return;
}

void Client::Handle_OP_LeaveBoat(const EQApplicationPacket *app)
{
- this->IsOnBoat=false;
+ Mob* boat = entity_list.GetMob(this->BoatID); // find the mob corresponding to the boat id
+ if (boat) {
+ if ((boat->GetTarget() == this) && boat->GetHateAmount(this) == 0) // if the client somehow left while still controlling the boat (and the boat isn't attacking them)
+ boat->SetTarget(0); // fix it to stop later problems
+ }
+ this->BoatID = 0;
return;
}

@@ -6526,6 +6552,29 @@

void Client::Handle_OP_ControlBoat(const EQApplicationPacket *app)
{
+ ControlBoat_Struct* cbs = (ControlBoat_Struct*)app->pBuffer;
+ Mob* boat = entity_list.GetMob(cbs->boatId);
+ if (boat == 0)
+ return; // do nothing if the boat isn't valid
+
+ if (cbs->TakeControl) {
+ // this uses the boat's target to indicate who has control of it. It has to check hate to make sure the boat isn't actually attacking anyone.
+ if ((boat->GetTarget() == 0) || (boat->GetTarget() == this && boat->GetHateAmount(this) == 0)) {
+ boat->SetTarget(this);
+ }
+ else {
+ this->Message_StringID(13,IN_USE);
+ return;
+ }
+ }
+ else
+ boat->SetTarget(0);
+
+ EQApplicationPacket* outapp=new EQApplicationPacket(OP_ControlBoat,0);
+ FastQueuePacket(&outapp);
+ safe_delete(outapp);
+ // have the boat signal itself, so quests can be triggered by boat use
+ boat->CastToNPC()->SignalNPC(0);
}

void Client::Handle_OP_DumpName(const EQApplicationPacket *app)
Index: EQEmuServer/zone/mob.cpp
================================================== =================
--- EQEmuServer/zone/mob.cpp (revision 433)
+++ EQEmuServer/zone/mob.cpp (working copy)
@@ -1088,7 +1088,7 @@
}
}

-void Mob::GMMove(float x, float y, float z, float heading) {
+void Mob::GMMove(float x, float y, float z, float heading, bool SendUpdate) {
x_pos = x;
y_pos = y;
z_pos = z;
@@ -1096,7 +1096,8 @@
this->heading = heading;
if(IsNPC())
CastToNPC()->SaveGuardSpot(true);
- SendAllPosition();
+ if(SendUpdate)
+ SendAllPosition();
//SendPosUpdate(1);
#ifdef PACKET_UPDATE_MANAGER
if(IsClient()) {
@@ -4348,3 +4349,10 @@

return;
}
+
+void Mob::SetDeltas(float dx, float dy, float dz, float dh) {
+ delta_x = dx;
+ delta_y = dy;
+ delta_z = dz;
+ delta_heading = dh;
+}
Index: EQEmuServer/zone/mob.h
================================================== =================
--- EQEmuServer/zone/mob.h (revision 433)
+++ EQEmuServer/zone/mob.h (working copy)
@@ -428,7 +428,8 @@
void DoAnim(const int animnum, int type=0, bool ackreq = true, eqFilterType filter = FilterNone);

void ChangeSize(float in_size, bool bNoRestriction = false);
- virtual void GMMove(float x, float y, float z, float heading = 0.01);
+ virtual void GMMove(float x, float y, float z, float heading = 0.01, bool SendUpdate = true);
+ void SetDeltas(float delta_x, float delta_y, float delta_z, float delta_h);
void SendPosUpdate(int8 iSendToSelf = 0);
void MakeSpawnUpdateNoDelta(PlayerPositionUpdateServer_ Struct* spu);
void MakeSpawnUpdate(PlayerPositionUpdateServer_Struct* spu);
Index: EQEmuServer/zone/StringIDs.h
================================================== =================
--- EQEmuServer/zone/StringIDs.h (revision 433)
+++ EQEmuServer/zone/StringIDs.h (working copy)
@@ -192,4 +192,5 @@
#define SONG_NEEDS_STRINGS 407 // You need to play a stringed instrument for this song
#define SONG_NEEDS_BRASS 408 // You need to play a brass instrument for this song
#define NO_INSTRUMENT_SKILL 269 // "Stick to singing until you learn to play this instrument."
+#define IN_USE 1406 // "Someone else is using that. Try again later."
#endif


The other part of using the boats is making them return to their original positions after they haven't been used for a while. To make this work fairly simply (especially in lake rathe, where 7 boats share a quest file and an npctypeid), I made the server send a signal ($signalid = 0) to the boat when it gets a control request. When in use, the boat targets the player controlling it, otherwise it has no target. Here's the perl script I've been using to control boat respawns.

# Quest to return boats to their spawn points after an hour without use
# by RealityIncarnate

# The boat receives a signal (id 0) whenever the server gets a ControlBoat opcode
sub EVENT_SIGNAL {
quest::stoptimer("respawn"); # stop any respawn countdown in progress
quest::settimer("checkuse", 60); # begin checking for use every minute
}

sub EVENT_TIMER {
if($timer eq "checkuse") {
if(!$npc->GetTarget()) { # if the boat has a target, it's in use. If not, begin the respawn timer
quest::stoptimer("checkuse"); # stop checking for use
quest::settimer("respawn",3600); # start the respawn timer at 1 hour
}
}
if($timer eq "respawn") {
quest::respawn($mobid,0); # it hasn't been used for an hour, so respawn the boat
}
}

janusd
04-19-2009, 11:03 AM
Wait... you got the boat to target the player without the player having to initiate contact with an entry or some other such? Pretty sexy! I'd forgotten all about those boats, honestly.

You think it's possible to work up a PERL command to make NPCs respond to text without being targeted? Just when something is said within a certain radius of 'em they respond if they're keyed to respond to it?

cavedude
04-19-2009, 01:07 PM
This is in SVN, and I added a_boat.pl to the templates folder on the PEQ SVN. I'll work on the spawnpoints for the broken boats. Thanks!

realityincarnate
04-19-2009, 11:34 PM
Wait... you got the boat to target the player without the player having to initiate contact with an entry or some other such? Pretty sexy! I'd forgotten all about those boats, honestly.

You think it's possible to work up a PERL command to make NPCs respond to text without being targeted? Just when something is said within a certain radius of 'em they respond if they're keyed to respond to it?

The boat targeting was fairly simple because the client sends a couple of different op codes when it enters or leaves boats, so I used those to trigger the changes.

I can think of a couple of ways that NPCs could respond to things said within a certain radius, but they all involve adding a lot of extra work for the server (as every npc sifts through everything that happens near them, looking for those few relevant items). At the very least, it would probably need a database flag, similar to the quest globals flag, to designate the few NPCs that need to do that.

janusd
04-20-2009, 07:37 AM
If it were coded to switch on an off as PCs enter or leave a certain radius of the npc that might make the server load a lot lighter.

The main reason I ask is that there are some Live quests that require proximity speech without a target. I'm also fairly certain that the custom admins could use a similar function to their own devious ends.