game/src/cract.cc
2025-06-12 04:17:13 -03:00

1590 lines
40 KiB
C++

#include "cr.hh"
#include "config.hh"
#include "info.hh"
#include "operate.hh"
#include "stubs.hh"
// TShortway
// =============================================================================
struct TShortwayPoint {
TShortwayPoint(void){
this->x = 0;
this->y = 0;
this->Waypoints = -1;
this->Waylength = -1;
this->Heuristic = -1;
this->Predecessor = NULL;
this->NextToExpand = NULL;
}
int x;
int y;
int Waypoints;
int Waylength;
int Heuristic;
TShortwayPoint *Predecessor;
TShortwayPoint *NextToExpand;
};
struct TShortway{
TShortway(TCreature *Creature, int VisibleX, int VisibleY);
~TShortway(void);
void FillMap(void);
void ClearMap(void);
void Expand(TShortwayPoint *Node);
bool Calculate(int DestX, int DestY, bool MustReach, int MaxSteps);
// DATA
// =================
matrix<TShortwayPoint> *Map;
TShortwayPoint *FirstToExpand;
TCreature *Creature;
int VisibleX;
int VisibleY;
int StartX;
int StartY;
int StartZ;
int MinWaypoints;
};
TShortway::TShortway(TCreature *Creature, int VisibleX, int VisibleY){
if(Creature == NULL){
error("TShortway::TShortway: Übergebene Kreatur ist NULL.\n");
return;
}
if(VisibleX < 1 || VisibleX > 100 || VisibleY < 1 || VisibleY > 100){
error("TShortway::TShortway: Ungültiger Sichtbarkeitsbereich %d*%d.\n", VisibleX, VisibleY);
return;
}
this->Creature = Creature;
this->VisibleX = VisibleX;
this->VisibleY = VisibleY;
this->StartX = Creature->posx;
this->StartY = Creature->posy;
this->StartZ = Creature->posz;
this->Map = new matrix<TShortwayPoint>(
-(VisibleX + 1), +(VisibleX + 1),
-(VisibleY + 1), +(VisibleY + 1));
this->FillMap();
}
TShortway::~TShortway(void){
if(this->Map != NULL){
delete this->Map;
}
}
void TShortway::FillMap(void){
this->MinWaypoints = 1000;
for(int X = -this->VisibleX; X <= this->VisibleX; X += 1)
for(int Y = -this->VisibleY; Y <= this->VisibleY; Y += 1){
int FieldX = this->StartX + X;
int FieldY = this->StartY + Y;
int FieldZ = this->StartZ;
int Waypoints = -1;
Object Obj = GetFirstObject(FieldX, FieldY, FieldZ);
if(Obj.exists()){
ObjectType ObjType = Obj.getObjectType();
if(ObjType.getFlag(BANK) && !ObjType.getFlag(UNPASS)){
Waypoints = (int)ObjType.getAttribute(WAYPOINTS);
if(Waypoints == 0){
error("TShortway::FillMap: Ungültiger Wegpunkte-Wert %d für Bank %d.\n",
Waypoints, ObjType.TypeID);
Waypoints = -1;
}
if(!this->Creature->MovePossible(FieldX, FieldY, FieldZ, false, false)){
Waypoints = -1;
}
if(Waypoints > 0 && Waypoints < this->MinWaypoints){
this->MinWaypoints = Waypoints;
}
}
}
TShortwayPoint *Node = this->Map->at(X, Y);
Node->x = X;
Node->y = Y;
Node->Waypoints = Waypoints;
}
}
void TShortway::ClearMap(void){
for(int X = -this->VisibleX; X <= this->VisibleX; X += 1)
for(int Y = -this->VisibleY; Y <= this->VisibleY; Y += 1){
TShortwayPoint *Node = this->Map->at(X, Y);
Node->Waylength = INT_MAX;
Node->Heuristic = INT_MAX;
Node->Predecessor = NULL;
Node->NextToExpand = NULL;
}
}
void TShortway::Expand(TShortwayPoint *Node){
if(Node == NULL){
error("TShortway::Expand: Übergebener Knoten ist NULL.\n");
return;
}
this->FirstToExpand = Node->NextToExpand;
int MinNeighborWaylength = Node->Waylength + Node->Waypoints;
if(MinNeighborWaylength >= this->Map->at(0, 0)->Waylength){
return;
}
for(int OffsetX = -1; OffsetX <= 1; OffsetX += 1)
for(int OffsetY = -1; OffsetY <= 1; OffsetY += 1){
if(OffsetX == 0 && OffsetY == 0){
continue;
}
TShortwayPoint *Neighbor = this->Map->at(Node->x + OffsetX, Node->y + OffsetY);
// NOTE(fusion): The minimum neighbor waylength already contains the cost
// of a single step. Diagonal steps are three times more expensive so we
// add waypoints two more times.
int NeighborWaylength = MinNeighborWaylength;
if(OffsetX != 0 && OffsetY != 0){
NeighborWaylength += Node->Waylength * 2;
}
if(NeighborWaylength < Neighbor->Waylength){
Neighbor->Waylength = NeighborWaylength;
Neighbor->Predecessor = Node;
if((Neighbor->x != 0 || Neighbor->y != 0) && Neighbor->Waypoints != -1){
// NOTE(fusion): Remove from expand list if it was already expanded upon.
if(Neighbor->Heuristic != INT_MAX){
TShortwayPoint *Prev = NULL;
TShortwayPoint *Cur = this->FirstToExpand;
while(Cur != NULL && Cur != Neighbor){
Prev = Cur;
Cur = Cur->NextToExpand;
}
if(Cur != Neighbor){
error("TShortway::Expand: Knoten steht nicht in der ExpandList.\n");
}else if(Prev == NULL){
this->FirstToExpand = Neighbor->NextToExpand;
}else{
Prev->NextToExpand = Neighbor->NextToExpand;
}
}
// NOTE(fusion): Compute heuristic using the manhattan distance.
int Distance = std::abs(Neighbor->x) + std::abs(Neighbor->y);
Neighbor->Heuristic = Neighbor->Waylength
+ Neighbor->Waypoints * 1
+ this->MinWaypoints * (Distance - 1);
// NOTE(fusion): Insert into expand list.
{
TShortwayPoint *Prev = NULL;
TShortwayPoint *Cur = this->FirstToExpand;
while(Cur != NULL && Cur->Heuristic < Neighbor->Heuristic){
Prev = Cur;
Cur = Cur->NextToExpand;
}
if(Prev == NULL){
this->FirstToExpand = Neighbor;
}else{
Prev->NextToExpand = Neighbor;
}
Neighbor->NextToExpand = Cur;
}
}
}
}
}
bool TShortway::Calculate(int DestX, int DestY, bool MustReach, int MaxSteps){
if(this->Map == NULL){
error("TShortway::Calculate: Karte existiert nicht.\n");
return false;
}
// NOTE(fusion): Transform dest to relative coordinates.
DestX -= this->StartX;
DestY -= this->StartY;
// NOTE(fusion): Check if already at destination.
if(DestX == 0 && DestY == 0){
return true;
}
// NOTE(fusion): Check if out of range.
if(std::abs(DestX) > this->VisibleX || std::abs(DestY) > this->VisibleY){
return false;
}
// NOTE(fusion): Find shortest path from the destination to the origin.
this->ClearMap();
this->FirstToExpand = this->Map->at(DestX, DestY);
this->FirstToExpand->Waylength = 0;
while(this->FirstToExpand != NULL){
this->Expand(this->FirstToExpand);
}
// NOTE(fusion): Check if the origin was reached from the destination.
TShortwayPoint *Node = this->Map->at(0, 0);
if(Node->Waylength == INT_MAX){
return false;
}
// NOTE(fusion): Walk back from the origin to reconstruct the path.
Node = Node->Predecessor;
while(Node != NULL && MaxSteps > 0){
int MaxDistance = std::max<int>(
std::abs(Node->x - DestX),
std::abs(Node->y - DestY));
if(!MustReach && MaxDistance <= 1){
break;
}
TToDoEntry TD = {};
TD.Code = TDGo;
TD.Go.x = this->StartX + Node->x;
TD.Go.y = this->StartY + Node->y;
TD.Go.z = this->StartZ;
Creature->ToDoAdd(TD);
Node = Node->Predecessor;
MaxSteps -= 1;
}
return true;
}
// TCreature
// =============================================================================
bool TCreature::SetOnMap(void){
int LoginX = this->posx;
int LoginY = this->posy;
int LoginZ = this->posz;
bool Player = (this->Type == PLAYER);
if(!SearchLoginField(&LoginX, &LoginY, &LoginZ, 1, Player)){
bool Found = false;
if(IsHouse(LoginX, LoginY, LoginZ)){
uint16 HouseID = GetHouseID(LoginX, LoginY, LoginZ);
GetExitPosition(HouseID, &LoginX, &LoginY, &LoginZ);
Found = SearchLoginField(&LoginX, &LoginY, &LoginZ, 1, Player);
}
if(!Found){
LoginX = this->startx;
LoginY = this->starty;
LoginZ = this->startz;
}
}
Object Con = GetMapContainer(LoginX, LoginY, LoginZ);
if(Con == NONE){
error("TCreature::SetOnMap: Kartencontainer für Punkt [%d,%d,%d] existiert nicht.\n",
LoginX, LoginY, LoginZ);
return false;
}
this->posx = LoginX;
this->posy = LoginY;
this->posz = LoginZ;
// NOTE(fusion): `Create` automatically sets `this->CrObject` and creates
// its body container slots.
bool Result = true;
try{
Create(Con, TYPEID_CREATURE_CONTAINER, this->ID);
}catch(RESULT r){
error("TCreature::SetOnMap: Kann Kreatur nicht setzen ([%d,%d,%d] - Exception %d).\n",
LoginX, LoginY, LoginZ, r);
if(this->Type == PLAYER){
SendResult(this->Connection, r);
}
Result = false;
}
return Result;
}
bool TCreature::DelOnMap(void){
Object Obj = this->CrObject;
if(Obj == NONE){
return true;
}
// TODO(fusion): I feel `Delete` should also manage `this->CrObject` automatically.
bool Result = true;
this->CrObject = NONE;
try{
Delete(Obj, -1);
}catch(RESULT r){
error("TCreature::DelOnMap: Error Deleting CreatureObject: %d\n", r);
Result = false;
}
return Result;
}
void TCreature::Go(int DestX, int DestY, int DestZ){
// NOTE(fusion): This is the execution function for `ToDoGo` which computes
// the path step by step. If the destination is outside the range of a single
// step, then it is an error.
int OrigX = this->posx;
int OrigY = this->posy;
int OrigZ = this->posz;
int Distance = std::max<int>(std::abs(OrigX - DestX), std::abs(OrigY - DestY));
if(Distance > 1 || OrigZ != DestZ){
throw NOTACCESSIBLE;
}
// TODO(fusion): See note in `TPlayer::CheckState`.
int DrunkLevel = this->Skills[SKILL_DRUNKEN]->TimerValue();
if(DrunkLevel > 0 && this->Skills[SKILL_DRUNKEN]->Get() == 0){
int StaggerChance = std::max<int>(7 - DrunkLevel, 1);
if(rand() % StaggerChance == 0){
DestX = OrigX;
DestY = OrigY;
switch(rand() % 4){
case DIRECTION_NORTH: DestY -= 1; break;
case DIRECTION_EAST: DestX += 1; break;
case DIRECTION_SOUTH: DestY += 1; break;
case DIRECTION_WEST: DestX -= 1; break;
}
if(this->ToDoClear() && this->Type == PLAYER){
SendSnapback(this->Connection);
}
int TalkMode = (this->Type == MONSTER) ? TALK_ANIMAL_LOW : TALK_SAY;
this->ToDoTalk(TalkMode, NULL, "Hicks!", false);
this->ToDoStart();
}
}
if(!this->MovePossible(DestX, DestY, DestZ, true, false)){
bool DiagonalMove = (OrigX != DestX && OrigY != DestY);
if(this->Type == PLAYER && !DiagonalMove){
// TODO(fusion): These are quite similar to `MagicClimbing`. Perhaps
// there is an inlined function here that checks whether climbing is
// possible.
if(DestZ > 0 && GetHeight(OrigX, OrigY, OrigZ) >= 24
&& !CoordinateFlag(OrigX, OrigY, OrigZ - 1, BANK)
&& !CoordinateFlag(OrigX, OrigY, OrigZ - 1, UNPASS)
&& this->MovePossible(DestX, DestY, DestZ - 1, true, true)){
DestZ -= 1;
}else if(DestZ < 15 && GetHeight(DestX, DestY, DestZ + 1) >= 24
&& !CoordinateFlag(DestX, DestY, DestZ, BANK)
&& !CoordinateFlag(DestX, DestY, DestZ, UNPASS)
&& this->MovePossible(DestX, DestY, DestZ + 1, true, true)){
DestZ += 1;
}
}
if(this->posz == DestZ){
throw MOVENOTPOSSIBLE;
}
}
Object Dest = GetMapContainer(DestX, DestY, DestZ);
::Move(this->ID, this->CrObject, Dest, -1, false, NONE);
}
void TCreature::Rotate(int Direction){
this->Direction = Direction;
AnnounceChangedObject(this->CrObject, OBJECT_CHANGED);
}
void TCreature::Rotate(TCreature *Target){
if(Target == NULL){
error("TCreature::Rotate: Target ist NULL.\n");
return;
}
int Direction = this->Direction;
int OffsetX = Target->posx - this->posx;
int OffsetY = Target->posy - this->posy;
int DistanceX = std::abs(OffsetX);
int DistanceY = std::abs(OffsetY);
if(DistanceY > DistanceX){
Direction = (OffsetY < 0) ? DIRECTION_NORTH : DIRECTION_SOUTH;
}else{
Direction = (OffsetX < 0) ? DIRECTION_WEST : DIRECTION_EAST;
}
this->Rotate(Direction);
}
void TCreature::Move(Object Obj, int DestX, int DestY, int DestZ, uint8 Count){
// NOTE(fusion): What a disaster.
if(!Obj.exists()){
throw NOTACCESSIBLE;
}
if(Obj == this->CrObject){
this->Go(DestX, DestY, DestZ);
return;
}
ObjectType ObjType = Obj.getObjectType();
if(ObjType.isCreatureContainer()){
this->Combat.DelayAttack(2000);
}
int MoveCount = (int)Count;
if(ObjType.getFlag(CUMULATIVE)){
int ObjAmount = (int)Obj.getAttribute(AMOUNT);
if(MoveCount > ObjAmount){
MoveCount = ObjAmount;
}
}
if(DestX == 0xFFFF){ // SPECIAL_COORDINATE ?
// NOTE(fusion): Any inventory slot.
if(DestY == 0){
// NOTE(fusion): `CheckInventoryDestination` will throw if it's not
// possible to place the object on the chosen container. We want to
// find a slot that doesn't make it throw while giving priority to
// non hand or ammo slots.
for(int Position = INVENTORY_FIRST;
Position <= INVENTORY_LAST;
Position += 1){
try{
Object Con = GetBodyContainer(this->ID, Position);
CheckInventoryDestination(Obj, Con, false);
}catch(RESULT r){
continue;
}
DestY = Position;
if(Position != INVENTORY_RIGHTHAND
&& Position != INVENTORY_LEFTHAND
&& Position != INVENTORY_AMMO){
break;
}
}
// NOTE(fusion): No appropriate inventory slot was found so we now
// fallback to inventory containers. For whatever reason we don't
// give priority to the bag slot.
if(DestY == 0){
for(int Position = INVENTORY_FIRST;
Position <= INVENTORY_LAST;
Position += 1){
Object Con = GetBodyObject(this->ID, Position);
if(Con != NONE && Con.getObjectType().getFlag(CONTAINER)){
try{
CheckContainerDestination(Obj, Con);
}catch(RESULT r){
continue;
}
DestY = Position;
}
}
}
if(DestY == 0){
throw NOROOM;
}
}
Object DestCon = NONE;
Object DestObj = NONE;
if(DestY >= INVENTORY_FIRST && DestY <= INVENTORY_LAST){
DestCon = GetBodyContainer(this->ID, DestY);
DestObj = GetBodyObject(this->ID, DestY);
}else if(DestY >= CONTAINER_FIRST && DestY <= CONTAINER_LAST){
DestCon = GetBodyContainer(this->ID, DestY);
if(DestZ < 254){
// TODO(fusion): The last argument to `GetObject` is the object
// type we expect to find and it seems that the type id of a map
// container (which is 0) can be used as a wildcard for any object
// it finds.
DestObj = GetObject(this->ID, DestX, DestY, DestZ, DestZ, 0);
}else if(DestZ == 254){
if(DestCon == NONE){
throw NOTACCESSIBLE;
}
DestCon = DestCon.getContainer();
}
}else{
error("TCreature::Move: Ungültiger Containercode %d.\n", DestY);
throw ERROR;
}
if(DestObj != NONE){
ObjectType DestObjType = DestObj.getObjectType();
if(DestObjType.getFlag(CONTAINER)){
DestCon = DestObj;
DestObj = NONE;
}else if(DestObjType.getFlag(CUMULATIVE) && DestObjType == ObjType){
int DestAmount = (int)DestObj.getAttribute(AMOUNT);
int MergeCount = MoveCount;
if((DestAmount + MergeCount) > 100){
MergeCount = 100 - DestAmount;
}
if(MergeCount > 0){
try{
::Merge(this->ID, Obj, DestObj, MergeCount, NONE);
MoveCount -= MergeCount;
if(MoveCount <= 0){
return;
}
}catch(RESULT r){
if(r == TOOHEAVY){
throw;
}
}
}
DestObj = NONE;
}
}
if(DestCon == NONE){
throw OUTOFRANGE;
}
try{
// TODO(fusion): Find out what is this last parameter to `Move`.
::Move(this->ID, Obj, DestCon, MoveCount, false, DestObj);
}catch(RESULT r){
// NOTE(fusion): Attempt to exchange inventory items.
if(DestY >= INVENTORY_FIRST && DestY <= INVENTORY_LAST
&& DestObj != NONE
&& (r == NOROOM
|| r == HANDSNOTFREE
|| r == HANDBLOCKED
|| r == ONEWEAPONONLY)){
Object ObjCon = Obj.getContainer();
::Move(this->ID, DestObj, ObjCon, -1, false, NONE);
::Move(this->ID, Obj, DestCon, MoveCount, false, NONE);
}else{
throw;
}
}
}else{
Object Dest = GetMapContainer(DestX, DestY, DestZ);
if(!Dest.exists()){
throw ERROR;
}
if(ObjType.getFlag(HANG)
&& (CoordinateFlag(DestX, DestY, DestZ, HOOKSOUTH)
|| CoordinateFlag(DestY, DestY, DestZ, HOOKEAST))
&& !ObjectInRange(this->ID, Dest, 1)){
if(this->ToDoClear() && this->Type == PLAYER){
SendSnapback(this->Connection);
}
// NOTE(fusion): Pick up object if it's not in our inventory.
if(GetObjectCreatureID(Obj) != this->ID){
this->ToDoMove(Obj, 0xFFFF, 0, 0, 1);
}
// NOTE(fusion): Walk to destination and try again.
this->ToDoGo(DestX, DestY, DestZ, false, INT_MAX);
this->ToDoMove(Obj, DestX, DestY, DestZ, 1);
this->ToDoStart();
}else{
::Move(this->ID, Obj, Dest, MoveCount, false, NONE);
}
}
}
void TCreature::Trade(Object Obj, uint32 PartnerID){
if(this->Type != PLAYER){
error("TCreature::Trade: Nur Spieler können handeln.\n");
throw ERROR;
}
if(!Obj.exists() || !ObjectAccessible(this->ID, Obj, 1)){
throw NOTACCESSIBLE;
}
if(this->ID == PartnerID){
throw CROSSREFERENCE;
}
TPlayer *Partner = GetPlayer(PartnerID);
if(Partner == NULL || Partner->Type != PLAYER){
throw PLAYERNOTONLINE;
}
if(((TPlayer*)this)->TradeObject != NONE){
throw ALREADYTRADING;
}
if(CountObjects(Obj) > 100){
throw TOOMANYOBJECTS;
}
if(ObjectDistance(this->CrObject, Partner->CrObject) > 2){
throw OUTOFRANGE;
}
if(!ThrowPossible(this->posx, this->posy, this->posz,
Partner->posx, Partner->posy, Partner->posz, 0)){
throw CANNOTTHROW;
}
if(Partner->TradeObject != NONE && Partner->TradePartner != this->ID){
throw PARTNERTRADING;
}
// NOTE(fusion): Check if one object is contained by the other.
if(Partner->TradeObject != NONE){
Object Help = Partner->TradeObject;
while(Help != NONE && !Help.getObjectType().isMapContainer()){
if(Help == Obj){
throw NOTACCESSIBLE;
}
Help = Help.getContainer();
}
Help = Obj;
while(Help != NONE && !Help.getObjectType().isMapContainer()){
if(Help == Partner->TradeObject){
throw NOTACCESSIBLE;
}
Help = Help.getContainer();
}
}
((TPlayer*)this)->TradeObject = Obj;
((TPlayer*)this)->TradePartner = PartnerID;
((TPlayer*)this)->TradeAccepted = false;
if(Partner->TradeObject != NONE){
SendTradeOffer(Partner->Connection, this->Name, false, Obj);
SendTradeOffer(this->Connection, this->Name, true, Obj);
SendTradeOffer(this->Connection, Partner->Name, false, Partner->TradeObject);
}else{
SendMessage(Partner->Connection, TALK_INFO_MESSAGE,
"%s wants to trade with you.", this->Name);
SendTradeOffer(this->Connection, this->Name, true, Obj);
}
}
void TCreature::Use(Object Obj1, Object Obj2, uint8 Dummy){
if(!Obj1.exists()){
throw DESTROYED;
}
if(Obj2 != NONE){
if(!Obj2.exists()){
throw DESTROYED;
}
bool DistUse = Obj1.getObjectType().getFlag(DISTUSE);
if(!DistUse && !ObjectInRange(this->ID, Obj2, 1)){
int ObjX2, ObjY2, ObjZ2;
GetObjectCoordinates(Obj2, &ObjX2, &ObjY2, &ObjZ2);
if(this->ToDoClear() && this->Type == PLAYER){
SendSnapback(this->Connection);
}
// NOTE(fusion): Pick up object 1 if it's not in our inventory.
if(GetObjectCreatureID(Obj1) != this->ID){
this->ToDoMove(Obj1, 0xFFFF, 0, 0, 1);
}
// NOTE(fusion): Walk to object 2 and try again.
this->ToDoGo(ObjX2, ObjY2, ObjZ2, false, INT_MAX);
// TODO(fusion): Missing `Dummy` here? Probably not because this
// could only be triggered if object 2 exists and Dummy is only
// used when opening containers.
this->ToDoUse(2, Obj1, Obj2);
this->ToDoStart();
return;
}
if(DistUse && !ObjectInRange(this->ID, Obj2, 7)){
throw OUTOFRANGE;
}
this->EarliestMultiuseTime = ServerMilliseconds + 1000;
}
::Use(this->ID, Obj1, Obj2, Dummy);
}
void TCreature::Turn(Object Obj){
if(!Obj.exists()){
throw DESTROYED;
}
::Turn(this->ID, Obj);
}
void TCreature::Attack(void){
this->Combat.Attack();
}
void TCreature::Execute(void){
while(true){
if(!this->LockToDo || this->IsDead || this->NextWakeup > ServerMilliseconds){
break;
}
if(this->NrToDo <= this->ActToDo){
this->ToDoClear();
this->IdleStimulus();
break;
}
uint32 Delay = this->CalculateDelay();
if(Delay > 0){
if(this->Stop){
this->ToDoClear();
if(this->Type == PLAYER){
SendSnapback(this->Connection);
}
}else{
this->NextWakeup = ServerMilliseconds + Delay;
ToDoQueue.insert(this->NextWakeup, this->ID);
}
break;
}
TToDoEntry TD = *this->ToDoList.at(this->ActToDo);
this->ActToDo += 1;
try{
switch(TD.Code){
case TDGo:{
this->Go(TD.Go.x, TD.Go.y, TD.Go.z);
break;
}
case TDRotate:{
this->Rotate(TD.Rotate.Direction);
break;
}
case TDMove:{
this->Move(TD.Move.Obj, TD.Move.x, TD.Move.y, TD.Move.z, (uint8)TD.Move.Count);
break;
}
case TDTrade:{
this->Trade(TD.Trade.Obj, TD.Trade.Partner);
break;
}
case TDUse:{
this->Use(TD.Use.Obj1, TD.Use.Obj2, TD.Use.Dummy);
break;
}
case TDTurn:{
this->Turn(TD.Turn.Obj);
break;
}
case TDAttack:{
this->Attack();
break;
}
case TDTalk:{
const char *Text = GetDynamicString(TD.Talk.Text);
if(Text != NULL){
const char *Addressee = GetDynamicString(TD.Talk.Addressee);
Talk(this->ID, TD.Talk.Mode, Addressee, Text, TD.Talk.CheckSpamming);
}else{
error("TCreature::Execute: Text ist NULL bei %s.\n", this->Name);
}
break;
}
case TDChangeState:{
if(this->Type == NPC){
ChangeNPCState(this, TD.ChangeState.NewState, true);
}
break;
}
default:{
break;
}
}
}catch(RESULT r){
bool SnapbackNecessary = (this->ToDoClear() || this->Stop);
if(r == EXHAUSTED){
this->ToDoWait(1000);
this->ToDoStart();
}else{
this->ToDoYield();
}
if(this->Type == PLAYER){
SendResult(this->Connection, r);
if(SnapbackNecessary
&& r != MOVENOTPOSSIBLE
&& r != NOTINVITED
&& r != ENTERPROTECTIONZONE){
SendSnapback(this->Connection);
}
}
break;
}
if(this->Stop){
this->ToDoClear();
if(this->Type == PLAYER){
SendSnapback(this->Connection);
}
break;
}
}
}
uint32 TCreature::CalculateDelay(void){
uint32 Delay = 0;
TToDoEntry *TD = this->ToDoList.at(this->ActToDo);
switch(TD->Code){
case TDWait:{
// TODO(fusion): I'm not sure about having `EarliestWalkTime` here.
uint32 WaitTime = TD->Wait.Time;
if(WaitTime < this->EarliestWalkTime){
WaitTime = this->EarliestWalkTime;
}
if(WaitTime > ServerMilliseconds){
Delay = WaitTime - ServerMilliseconds;
}
break;
}
case TDGo:{
if(this->EarliestWalkTime > ServerMilliseconds){
Delay = this->EarliestWalkTime - ServerMilliseconds;
}
break;
}
case TDUse:{
if(TD->Use.Obj2 != 0){
if(this->EarliestMultiuseTime > ServerMilliseconds){
Delay = this->EarliestMultiuseTime - ServerMilliseconds;
}
}
break;
}
case TDAttack:{
uint32 EarliestAttackTime = this->Combat.EarliestAttackTime;
if(EarliestAttackTime < this->EarliestSpellTime){
EarliestAttackTime = this->EarliestSpellTime;
}
if(EarliestAttackTime > ServerMilliseconds){
Delay = EarliestAttackTime - ServerMilliseconds;
}
break;
}
default:{
break;
}
}
return Delay;
}
bool TCreature::ToDoClear(void){
bool SnapbackNecessary = false;
for(int i = 0; i < this->NrToDo; i += 1){
TToDoEntry *TD = this->ToDoList.at(i);
switch(TD->Code){
case TDGo:{
if(this->ActToDo <= i){
SnapbackNecessary = true;
}
break;
}
case TDTalk:{
DeleteDynamicString(TD->Talk.Text);
DeleteDynamicString(TD->Talk.Addressee);
break;
}
case TDChangeState:{
if(this->ActToDo <= i && this->Type == NPC){
ChangeNPCState(this, TD->ChangeState.NewState, false);
}
break;
}
default:{
break;
}
}
}
this->LockToDo = false;
this->ActToDo = 0;
this->NrToDo = 0;
this->Stop = false;
return SnapbackNecessary;
}
void TCreature::ToDoAdd(TToDoEntry TD){
if(this->LockToDo){
if(this->ToDoClear() && this->Type == PLAYER){
SendSnapback(this->Connection);
}
}
*this->ToDoList.at(this->NrToDo) = TD;
this->NrToDo += 1;
}
void TCreature::ToDoStop(void){
if(this->LockToDo){
this->Stop = true;
}else if(this->Type == PLAYER){
SendSnapback(this->Connection);
}
}
void TCreature::ToDoStart(void){
if(this->NrToDo != 0){
this->LockToDo = true;
this->ActToDo = 0;
uint32 Delay = this->CalculateDelay();
if(Delay < 1){
Delay = 1;
}
uint32 NextWakeup = ServerMilliseconds + Delay;
ToDoQueue.insert(NextWakeup, this->ID);
this->NextWakeup = NextWakeup;
}
}
void TCreature::ToDoYield(void){
if(!this->LockToDo){
this->ToDoWait(0);
this->ToDoStart();
}
}
void TCreature::ToDoWait(int Delay){
TToDoEntry TD = {};
TD.Code = TDWait;
TD.Wait.Time = ServerMilliseconds + Delay;
this->ToDoAdd(TD);
}
void TCreature::ToDoGo(int DestX, int DestY, int DestZ, bool MustReach, int MaxSteps){
if(this->posz > DestZ){
throw UPSTAIRS;
}else if(this->posz < DestZ){
throw DOWNSTAIRS;
}
if(this->LockToDo){
TToDoEntry *Last = this->ToDoList.at(this->NrToDo - 1);
if(Last->Code == TDGo
&& Last->Go.x == DestX
&& Last->Go.y == DestY
&& Last->Go.z == DestZ){
// TODO(fusion): Why? Shouldn't we just return here?
throw NOERROR;
}
}
int DistanceX = std::abs(DestX - this->posx);
int DistanceY = std::abs(DestY - this->posy);
int MaxDistance = std::max<int>(DistanceX, DistanceY);
if(MaxDistance == 0 || (!MustReach && MaxDistance <= 1)){
return;
}
// NOTE(fusion): The number of steps between two points is the same as the
// their manhattan distance, if we exclude diagonal movement. We can skip
// the path finder if we know we're step away from the destination.
if(DistanceX + DistanceY == 1){
TToDoEntry TD = {};
TD.Code = TDGo;
TD.Go.x = DestX;
TD.Go.y = DestY;
TD.Go.z = DestZ;
this->ToDoAdd(TD);
}else{
int VisibleX = (this->Type == PLAYER) ? 7 : 10;
int VisibleY = (this->Type == PLAYER) ? 7 : 10;
TShortway Shortway(this, VisibleX, VisibleY);
if(!Shortway.Calculate(DestX, DestY, MustReach, MaxSteps)){
this->ToDoClear();
if(this->Type == PLAYER){
SendSnapback(this->Connection);
}
throw NOWAY;
}
}
}
void TCreature::ToDoRotate(int Direction){
if(Direction != DIRECTION_NORTH
&& Direction != DIRECTION_EAST
&& Direction != DIRECTION_SOUTH
&& Direction != DIRECTION_WEST){
throw ERROR;
}
TToDoEntry TD = {};
TD.Code = TDRotate;
TD.Rotate.Direction = Direction;
this->ToDoAdd(TD);
}
void TCreature::ToDoMove(int ObjX, int ObjY, int ObjZ, ObjectType Type, uint8 RNum,
int DestX, int DestY, int DestZ, uint8 Count){
Object Obj = GetObject(this->ID, ObjX, ObjY, ObjZ, RNum, Type);
if(!Obj.exists()){
throw NOTACCESSIBLE;
}
// TODO(fusion): This could be an inlined function.
if(ObjX != 0xFFFF){ // SPECIAL_COORDINATE ?
if(this->posz > ObjZ){
throw UPSTAIRS;
}else if(this->posz < ObjZ){
throw DOWNSTAIRS;
}
if(!ObjectInRange(this->ID, Obj, 1)){
this->ToDoGo(ObjX, ObjY, ObjZ, false, INT_MAX);
}
}
int Delay = 100;
if(Obj.getObjectType().isCreatureContainer()){
Object DestBank = GetFirstObject(DestX, DestY, DestZ);
if(DestBank == NONE || !DestBank.getObjectType().getFlag(BANK)){
throw NOTACCESSIBLE;
}
TCreature *Creature = GetCreature(Obj);
if(Creature == NULL){
error("TCreature::ToDoMove: Kreatur existiert nicht.\n");
throw ERROR;
}
Delay = 1000;
if(this->EarliestWalkTime > ServerMilliseconds){
Delay += (int)(this->EarliestWalkTime - ServerMilliseconds);
}
}
this->ToDoWait(Delay);
TToDoEntry TD = {};
TD.Code = TDMove;
TD.Move.Obj = Obj.ObjectID;
TD.Move.x = DestX;
TD.Move.y = DestY;
TD.Move.z = DestZ;
TD.Move.Count = Count;
this->ToDoAdd(TD);
}
void TCreature::ToDoMove(Object Obj, int DestX, int DestY, int DestZ, uint8 Count){
if(!Obj.exists()){
throw NOTACCESSIBLE;
}
if(!ObjectInRange(this->ID, Obj, 1)){
this->ToDoGo(DestX, DestY, DestZ, false, INT_MAX);
}
int Delay = 100;
if(Obj.getObjectType().isCreatureContainer()){
// TODO(fusion): We don't add the delay until the earliest walk time
// like in the other variant above.
Delay = 1000;
}
this->ToDoWait(Delay);
TToDoEntry TD = {};
TD.Code = TDMove;
TD.Move.Obj = Obj.ObjectID;
TD.Move.x = DestX;
TD.Move.y = DestY;
TD.Move.z = DestZ;
TD.Move.Count = Count;
this->ToDoAdd(TD);
}
void TCreature::ToDoTrade(int ObjX, int ObjY, int ObjZ, ObjectType Type, uint8 RNum,
uint32 TradePartner){
Object Obj = GetObject(this->ID, ObjX, ObjY, ObjZ, RNum, Type);
if(!Obj.exists()){
throw NOTACCESSIBLE;
}
ObjectType ObjType = Obj.getObjectType();
if(ObjType.getFlag(UNMOVE)){
throw NOTMOVABLE;
}
if(!ObjType.getFlag(TAKE)){
throw NOTTAKABLE;
}
if(ObjType.isCreatureContainer()){
error("TCreature::ToDoTrade: Objekt ist eine Kreatur.\n");
throw ERROR;
}
if(TradePartner == 0){
error("TCreature::ToDoTrade: Handelspartner ist Null.\n");
throw ERROR;
}
TCreature *Creature = GetCreature(TradePartner);
if(Creature == NULL){
throw PLAYERNOTONLINE;
}
if(Creature->Type != PLAYER){
error("TCreature::ToDoTrade: Handelspartner von %s ist kein Spieler.\n", this->Name);
throw ERROR;
}
// TODO(fusion): This could be an inlined function.
if(ObjX != 0xFFFF){ // SPECIAL_COORDINATE ?
if(this->posz > ObjZ){
throw UPSTAIRS;
}else if(this->posz < ObjZ){
throw DOWNSTAIRS;
}
if(!ObjectInRange(this->ID, Obj, 1)){
this->ToDoGo(ObjX, ObjY, ObjZ, false, INT_MAX);
}
}
TToDoEntry TD = {};
TD.Code = TDTrade;
TD.Trade.Obj = Obj.ObjectID;
TD.Trade.Partner = TradePartner;
this->ToDoAdd(TD);
}
void TCreature::ToDoUse(uint8 Count, int ObjX1, int ObjY1, int ObjZ1, ObjectType Type1, uint8 RNum1,
uint8 Dummy, int ObjX2, int ObjY2, int ObjZ2, ObjectType Type2, uint8 RNum2){
Object Obj1 = GetObject(this->ID, ObjX1, ObjY1, ObjZ1, RNum1, Type1);
if(!Obj1.exists()){
throw NOTACCESSIBLE;
}
Object Obj2 = NONE;
if(Count >= 2){
Obj2 = GetObject(this->ID, ObjX2, ObjY2, ObjZ2, RNum2, Type2);
if(!Obj2.exists()){
throw NOTACCESSIBLE;
}
}
// TODO(fusion): This could be an inlined function.
if(ObjX1 != 0xFFFF){ // SPECIAL_COORDINATE ?
if(this->posz > ObjZ1){
throw UPSTAIRS;
}else if(this->posz < ObjZ1){
throw DOWNSTAIRS;
}
if(!ObjectInRange(this->ID, Obj1, 1)){
this->ToDoGo(ObjX1, ObjY1, ObjZ1, false, INT_MAX);
}
}
this->ToDoWait(100);
TToDoEntry TD = {};
TD.Code = TDUse;
TD.Use.Obj1 = Obj1.ObjectID;
TD.Use.Obj2 = Obj2.ObjectID;
// TODO(fusion): This is the value sent by the client on single object use
// that contains the next free container index.
TD.Use.Dummy = Dummy;
this->ToDoAdd(TD);
}
void TCreature::ToDoUse(uint8 Count, Object Obj1, Object Obj2){
if(!Obj1.exists()){
throw NOTACCESSIBLE;
}
if(Count <= 1){
Obj2 = NONE;
}else if(!Obj2.exists()){
throw NOTACCESSIBLE;
}
if(!ObjectInRange(this->ID, Obj1, 1)){
int ObjX1, ObjY1, ObjZ1;
GetObjectCoordinates(Obj1, &ObjX1, &ObjY1, &ObjZ1);
this->ToDoGo(ObjX1, ObjY1, ObjZ1, false, INT_MAX);
}
this->ToDoWait(100);
TToDoEntry TD = {};
TD.Code = TDUse;
TD.Use.Obj1 = Obj1.ObjectID;
TD.Use.Obj2 = Obj2.ObjectID;
// TODO(fusion): Not sure why we set this value at all.
TD.Use.Dummy = Count;
this->ToDoAdd(TD);
}
void TCreature::ToDoTurn(int ObjX, int ObjY, int ObjZ, ObjectType Type, uint8 RNum){
Object Obj = GetObject(this->ID, ObjX, ObjY, ObjZ, RNum, Type);
if(!Obj.exists()){
throw NOTACCESSIBLE;
}
// TODO(fusion): This could be an inlined function.
if(ObjX != 0xFFFF){ // SPECIAL_COORDINATE ?
if(this->posz > ObjZ){
throw UPSTAIRS;
}else if(this->posz < ObjZ){
throw DOWNSTAIRS;
}
if(!ObjectInRange(this->ID, Obj, 1)){
this->ToDoGo(ObjX, ObjY, ObjZ, false, INT_MAX);
}
}
this->ToDoWait(100);
TToDoEntry TD = {};
TD.Code = TDTurn;
TD.Turn.Obj = Obj.ObjectID;
this->ToDoAdd(TD);
}
void TCreature::ToDoAttack(void){
this->Combat.CanToDoAttack();
if(this->Combat.GetDistance() != 1){
this->ToDoWait(100);
}
TToDoEntry TD = {};
TD.Code = TDAttack;
this->ToDoAdd(TD);
}
void TCreature::ToDoTalk(int Mode, const char *Addressee, const char *Text, bool CheckSpamming){
TToDoEntry TD = {};
if(Text == NULL || Text[0] == 0){
error("TCreature::ToDoTalk: Text ist NULL bei %s.\n", this->Name);
// TODO(fusion): The original code would attempt to call `AddDynamicString`
// even after this check but it doesn't check whether `Text` is NULL and
// calls `strlen` immediately on it which could be a problem.
TD.Talk.Text = 0;
}else{
TD.Talk.Text = AddDynamicString(Text);
}
if(Addressee == NULL){
TD.Talk.Addressee = 0;
}else{
TD.Talk.Addressee = AddDynamicString(Addressee);
}
TD.Code = TDTalk;
TD.Talk.Mode = Mode;
TD.Talk.CheckSpamming = CheckSpamming;
this->ToDoAdd(TD);
}
void TCreature::ToDoChangeState(int NewState){
TToDoEntry TD = {};
TD.Code = TDChangeState;
TD.ChangeState.NewState = NewState;
this->ToDoAdd(TD);
}
void TCreature::NotifyGo(void){
// IMPORTANT(fusion): This and the function that does move `this->CrObject`
// should be the only ones where the output of `GetObjectCoordinates` will
// differ from `this->posx`, `this->posy`, and `this->posz`.
int DestX, DestY, DestZ;
GetObjectCoordinates(this->CrObject, &DestX, &DestY, &DestZ);
MoveChainCreature(this, DestX, DestY);
int OrigX = this->posx;
int OrigY = this->posy;
int OrigZ = this->posz;
bool DiagonalMove = (DestX != OrigX && DestY != OrigY && DestZ == OrigZ);
// IMPORTANT(fusion): `SendFloors` and `SendRow` will use the current creature
// position to know where to pull fields from, so we need to keep them updated
// as we go.
if(this->Type == PLAYER && this->Connection != NULL){
int DistanceX = std::abs(DestX - OrigX);
int DistanceY = std::abs(DestY - OrigY);
int DistanceZ = std::abs(DestZ - OrigZ);
if(DistanceX <= 1 && DistanceY <= 1 && DistanceZ <= 1){
while(this->posz < DestZ){
this->posx -= 1;
this->posy -= 1;
this->posz += 1;
SendFloors(this->Connection, false);
}
while(this->posz > DestZ){
this->posx += 1;
this->posy += 1;
this->posz -= 1;
SendFloors(this->Connection, true);
}
while(this->posx < DestX){
this->posx += 1;
SendRow(this->Connection, DIRECTION_EAST);
}
while(this->posx > DestX){
this->posx -= 1;
SendRow(this->Connection, DIRECTION_WEST);
}
while(this->posy < DestY){
this->posy += 1;
SendRow(this->Connection, DIRECTION_SOUTH);
}
while(this->posy > DestY){
this->posy -= 1;
SendRow(this->Connection, DIRECTION_NORTH);
}
}else{
this->posx = DestX;
this->posy = DestY;
this->posz = DestZ;
SendFullScreen(this->Connection);
}
}else{
this->posx = DestX;
this->posy = DestY;
this->posz = DestZ;
}
if(this->Type == PLAYER){
// NOTE(fusion): Check open containers.
for(int ContainerNr = 0;
ContainerNr < NARRAY(TPlayer::OpenContainer);
ContainerNr += 1){
Object Con = ((TPlayer*)this)->GetOpenContainer(ContainerNr);
if(Con == NONE){
continue;
}
if(!Con.exists()){
error("TCreature::NotifyGo: OpenContainer existiert nicht. (%s, [%d,%d,%d]->[%d,%d,%d])\n",
this->Name, OrigX, OrigY, OrigZ, DestX, DestY, DestZ);
continue;
}
if(!ObjectAccessible(this->ID, Con, 1)){
((TPlayer*)this)->SetOpenContainer(ContainerNr, NONE);
SendCloseContainer(this->Connection, ContainerNr);
}
}
// NOTE(fusion): Check trade.
Object TradeObject = ((TPlayer*)this)->TradeObject;
if(TradeObject != NONE){
TPlayer *Partner = GetPlayer(((TPlayer*)this)->TradePartner);
if(!TradeObject.exists()){
error("TCreature::NotifyGo: Handelsobjekt existiert nicht mehr.\n");
error("# Händler %s an [%d,%d,%d]\n", this->Name, DestX, DestY, DestZ);
if(Partner != NULL){
error("# Partner %s an [%d,%d,%d]\n", Partner->Name,
Partner->posx, Partner->posy, Partner->posz);
}
}
if(Partner == NULL || !ObjectAccessible(this->ID, TradeObject, 1)
|| ObjectDistance(this->CrObject, Partner->CrObject) > 2
|| !ThrowPossible(this->posx, this->posy, this->posz,
Partner->posx, Partner->posy, Partner->posz, 0)){
SendCloseTrade(this->Connection);
SendMessage(this->Connection, TALK_FAILURE_MESSAGE, "Trade cancelled.");
((TPlayer*)this)->RejectTrade();
}
}
}
// TODO(fusion): This is probably an inlined function to get the first object
// with some specific flag on a field.
int Waypoints;
Object Bank = GetFirstObject(DestX, DestY, DestZ);
while(Bank != NONE){
ObjectType BankType = Bank.getObjectType();
if(BankType.getFlag(BANK)){
Waypoints = BankType.getAttribute(WAYPOINTS);
break;
}
Bank = Bank.getNextObject();
}
if(Bank != NONE){
// NOTE(fusion): Diagonal movement has three times the delay of a regular one.
if(DiagonalMove){
Waypoints *= 3;
}
// NOTE(fusion): Compute the step interval and quantize it to be a multiple
// of `Beat`, rounding up.
int Delay = (Waypoints * 1000) / this->GetSpeed();
int BeatCount = (Delay + Beat - 1) / Beat;
this->EarliestWalkTime = ServerMilliseconds + BeatCount * Beat;
}else{
error("TCreature::NotifyGo: Auf Feld [%d,%d,%d] befindet sich kein Bank.\n",
DestX, DestY, DestZ);
}
}
void TCreature::NotifyTurn(Object DestCon){
int DestX, DestY, DestZ;
GetObjectCoordinates(DestCon, &DestX, &DestY, &DestZ);
// NOTE(fusion): This is somewhat similar to `TCreature::Rotate`.
int OffsetX = DestX - this->posx;
int OffsetY = DestY - this->posy;
if(OffsetX > 0){
this->Direction = DIRECTION_EAST;
}else if(OffsetX < 0){
this->Direction = DIRECTION_WEST;
}else if(OffsetY < 0){
this->Direction = DIRECTION_NORTH;
}else if(OffsetY > 0){
this->Direction = DIRECTION_SOUTH;
}
}
void TCreature::NotifyCreate(void){
InsertChainCreature(this, 0, 0);
}
void TCreature::NotifyDelete(void){
if(this->Type == PLAYER){
if(this->Connection != NULL){
this->Connection->Logout(30, true);
this->LoggingOut = true;
}
((TPlayer*)this)->RejectTrade();
}
DeleteChainCreature(this);
}
void TCreature::NotifyChangeInventory(void){
if(this->CrObject == NONE){
return;
}
if(!this->CrObject.exists()){
error("TCreature::NotifyChangeInventory: Kreatur-Objekt existiert nicht.\n");
error("# Kreatur: %s, Position: %d/%d/%d.\n",
this->Name, this->posx, this->posy, this->posz);
return;
}
this->Combat.CheckCombatValues();
if(this->Type == PLAYER){
int NewDelta[NARRAY(this->Skills)];
for(int Position = INVENTORY_FIRST;
Position <= INVENTORY_LAST;
Position += 1){
Object Obj = GetBodyObject(this->ID, Position);
if(Obj == NONE){
continue;
}
ObjectType ObjType = Obj.getObjectType();
if(!ObjType.getFlag(SKILLBOOST)){
continue;
}
if(!ObjType.getFlag(CLOTHES)){
error("TCreature::NotifyChangeInventory: Objekt %d hat SKILLBOOST, aber nicht CLOTHES.\n",
ObjType.TypeID);
continue;
}
if((int)ObjType.getAttribute(BODYPOSITION) != Position){
continue;
}
int SkillNr = (int)ObjType.getAttribute(SKILLNUMBER);
if(SkillNr < 0 || SkillNr >= NARRAY(this->Skills)){
error("TCreature::NotifyChangeInventory: Objekt %d hat ungültige SKILLNUMBER %d.\n",
ObjType.TypeID, SkillNr);
continue;
}
// IMPORTANT(fusion): The value for the skill modification is stored
// as uint32 but is bitcast from a signed integer on load, meaning that
// that bitcasting it back should retrieve the original signed value.
// See `LoadObjects`.
NewDelta[SkillNr] += (int)ObjType.getAttribute(SKILLMODIFICATION);
}
bool SkillsChanged = false;
for(int SkillNr = 0;
SkillNr < NARRAY(this->Skills);
SkillNr += 1){
TSkill *Skill = this->Skills[SkillNr];
if(Skill->DAct != NewDelta[SkillNr]){
SkillsChanged = true;
Skill->DAct = NewDelta[SkillNr];
if(SkillNr == SKILL_GO_STRENGTH){
AnnounceChangedCreature(this->ID, CREATURE_SPEED_CHANGED);
}else if(SkillNr == SKILL_ILLUSION){
if(NewDelta[SKILL_ILLUSION] > 0){
if(this->Outfit.OutfitID != 0 || this->Outfit.ObjectType != 0){
if(Skill->TimerValue() != 0){
this->SetTimer(SKILL_ILLUSION, 0, 0, 0, -1);
}
this->Outfit.OutfitID = 0;
this->Outfit.ObjectType = 0;
}
}else if(Skill->TimerValue() == 0){
this->Outfit = this->OrgOutfit;
}
AnnounceChangedCreature(this->ID, CREATURE_OUTFIT_CHANGED);
}
}
}
if(SkillsChanged){
SendPlayerSkills(this->Connection);
((TPlayer*)this)->CheckState();
}
SendPlayerData(this->Connection);
}
}