#include "operate.hh" #include "config.hh" #include "info.hh" #include "stubs.hh" // TODO(fusion): The radii parameters for `TFindCreatures` are commonly around // 16 and 14 for x and y respectively so there is probably some constant involved. // Also, since we're talking about radii, these values are quite large when you // consider the regular client's viewport dimensions of 15x11 visible fields or // 18x14 total fields. void AnnounceMovingCreature(uint32 CreatureID, Object Con){ TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("AnnounceMovingCreature: Kreatur %d existiert nicht.\n", CreatureID); return; } int ConX, ConY, ConZ; GetObjectCoordinates(Con, &ConX, &ConY, &ConZ); int SearchRadiusX = 16 + (std::abs(Creature->posx - ConX) / 2) + 1; int SearchRadiusY = 14 + (std::abs(Creature->posy - ConY) / 2) + 1; int SearchCenterX = (Creature->posx + ConX) / 2; int SearchCenterY = (Creature->posy + ConY) / 2; TFindCreatures Search(SearchRadiusX, SearchRadiusY, SearchCenterX, SearchCenterY, FIND_PLAYERS); while(true){ uint32 CharacterID = Search.getNext(); if(CharacterID == 0){ break; } TPlayer *Player = GetPlayer(CharacterID); if(Player == NULL || Player->Connection == NULL){ continue; } SendMoveCreature(Player->Connection, CreatureID, ConX, ConY, ConZ); } } void AnnounceChangedCreature(uint32 CreatureID, int Type){ TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("AnnounceChangedCreature: Kreatur %d existiert nicht (Type=%d).\n", CreatureID, Type); return; } for(TKnownCreature *KnownCreature = Creature->FirstKnowingConnection; KnownCreature != NULL; KnownCreature = KnownCreature->Next){ TConnection *KnowingConnection = KnownCreature->Connection; if(KnowingConnection->State != CONNECTION_GAME || KnownCreature->State == KNOWNCREATURE_OUTDATED){ continue; } if(KnowingConnection->IsVisible(Creature->posx, Creature->posy, Creature->posz)){ switch(Type){ case CREATURE_HEALTH_CHANGED: SendCreatureHealth(KnowingConnection, CreatureID); break; case CREATURE_LIGHT_CHANGED: SendCreatureLight(KnowingConnection, CreatureID); break; case CREATURE_OUTFIT_CHANGED: SendCreatureOutfit(KnowingConnection, CreatureID); break; case CREATURE_SPEED_CHANGED: SendCreatureSpeed(KnowingConnection, CreatureID); break; case CREATURE_SKULL_CHANGED: SendCreatureSkull(KnowingConnection, CreatureID); break; case CREATURE_PARTY_CHANGED: SendCreatureParty(KnowingConnection, CreatureID); break; } }else{ KnownCreature->State = KNOWNCREATURE_OUTDATED; } } } void AnnounceChangedField(Object Obj, int Type){ if(!Obj.exists()){ error("AnnounceChangedField: Übergebenes Objekt existiert nicht.\n"); return; } int ObjX, ObjY, ObjZ; GetObjectCoordinates(Obj, &ObjX, &ObjY, &ObjZ); TFindCreatures Search(16, 14, ObjX, ObjY, FIND_PLAYERS); while(true){ uint32 CharacterID = Search.getNext(); if(CharacterID == 0){ break; } TPlayer *Player = GetPlayer(CharacterID); if(Player == NULL || Player->Connection == NULL){ continue; } if(!Player->Connection->IsVisible(ObjX, ObjY, ObjZ)){ continue; } switch(Type){ case OBJECT_DELETED: SendDeleteField(Player->Connection, ObjX, ObjY, ObjZ, Obj); break; case OBJECT_CREATED: SendAddField(Player->Connection, ObjX, ObjY, ObjZ, Obj); break; case OBJECT_CHANGED: SendChangeField(Player->Connection, ObjX, ObjY, ObjZ, Obj); break; default:{ error("AnnounceChangedField: Ungültiger Typ %d.\n", Type); return; } } } } void AnnounceChangedContainer(Object Obj, int Type){ if(!Obj.exists()){ error("AnnounceChangedContainer: Übergebenes Objekt existiert nicht.\n"); return; } Object Con = Obj.getContainer(); TFindCreatures Search(1, 1, Obj, FIND_PLAYERS); while(true){ uint32 CharacterID = Search.getNext(); if(CharacterID == 0){ break; } TPlayer *Player = GetPlayer(CharacterID); if(Player == NULL || Player->Connection == NULL){ continue; } for(int ContainerNr = 0; ContainerNr <= NARRAY(Player->OpenContainer); ContainerNr += 1){ if(Player->GetOpenContainer(ContainerNr) != Con){ continue; } switch(Type){ case OBJECT_DELETED: SendDeleteInContainer(Player->Connection, ContainerNr, Obj); break; case OBJECT_CREATED: SendCreateInContainer(Player->Connection, ContainerNr, Obj); break; case OBJECT_CHANGED: SendChangeInContainer(Player->Connection, ContainerNr, Obj); break; default:{ error("AnnounceChangedContainer: Ungültiger Typ %d.\n", Type); return; } } } } } void AnnounceChangedInventory(Object Obj, int Type){ if(!Obj.exists()){ error("AnnounceChangedInventory: Übergebenes Objekt existiert nicht.\n"); return; } uint32 OwnerID = GetObjectCreatureID(Obj); TCreature *Creature = GetCreature(OwnerID); if(Creature == NULL){ error("AnnounceChangedInventory: Objekt liegt in keinem Inventory.\n"); return; } if(Creature->Type != PLAYER){ return; } int Position = GetObjectBodyPosition(Obj); switch(Type){ case OBJECT_DELETED: SendDeleteInventory(Creature->Connection, Position); break; case OBJECT_CREATED: SendSetInventory(Creature->Connection, Position, Obj); break; case OBJECT_CHANGED: SendSetInventory(Creature->Connection, Position, Obj); break; default:{ error("AnnounceChangedInventory: Ungültiger Typ %d.\n", Type); return; } } } void AnnounceChangedObject(Object Obj, int Type){ if(!Obj.exists()){ error("AnnounceChangedObject: Übergebenes Objekt existiert nicht.\n"); return; } Object Con = Obj.getContainer(); ObjectType ConType = Con.getObjectType(); if(ConType.isMapContainer()){ AnnounceChangedField(Obj, Type); }else if(ConType.isBodyContainer()){ AnnounceChangedInventory(Obj, Type); }else{ AnnounceChangedContainer(Obj, Type); } } void AnnounceGraphicalEffect(int x, int y, int z, int Type){ if(!IsOnMap(x, y, z)){ return; } TFindCreatures Search(16, 14, x, y, FIND_PLAYERS); while(true){ uint32 CharacterID = Search.getNext(); if(CharacterID == 0){ break; } TPlayer *Player = GetPlayer(CharacterID); if(Player == NULL || Player->Connection == NULL){ continue; } if(!Player->Connection->IsVisible(x, y, z)){ continue; } SendGraphicalEffect(Player->Connection, x, y, z, Type); } } void AnnounceTextualEffect(int x, int y, int z, int Color, const char *Text){ if(!IsOnMap(x, y, z)){ return; } TFindCreatures Search(16, 14, x, y, FIND_PLAYERS); while(true){ uint32 CharacterID = Search.getNext(); if(CharacterID == 0){ break; } TPlayer *Player = GetPlayer(CharacterID); if(Player == NULL || Player->Connection == NULL){ continue; } if(!Player->Connection->IsVisible(x, y, z)){ continue; } SendTextualEffect(Player->Connection, x, y, z, Color, Text); } } void AnnounceMissile(int OrigX, int OrigY, int OrigZ, int DestX, int DestY, int DestZ, int Type){ if(!IsOnMap(OrigX, OrigY, OrigZ) || !IsOnMap(DestX, DestY, DestZ)){ return; } int SearchRadiusX = 16 + (std::abs(OrigX - DestX) / 2) + 1; int SearchRadiusY = 14 + (std::abs(OrigY - DestY) / 2) + 1; int SearchCenterX = (OrigX + DestX) / 2; int SearchCenterY = (OrigY + DestY) / 2; TFindCreatures Search(SearchRadiusX, SearchRadiusY, SearchCenterX, SearchCenterY, FIND_PLAYERS); while(true){ uint32 CharacterID = Search.getNext(); if(CharacterID == 0){ break; } TPlayer *Player = GetPlayer(CharacterID); if(Player == NULL || Player->Connection == NULL){ continue; } if(!Player->Connection->IsVisible(OrigX, OrigY, OrigY) && !Player->Connection->IsVisible(DestX, DestY, DestZ)){ continue; } SendMissileEffect(Player->Connection, OrigX, OrigY, OrigY, DestX, DestY, DestZ, Type); } } void CheckTopMoveObject(uint32 CreatureID, Object Obj, Object Ignore){ if(!Obj.exists()){ error("CheckTopMoveObject: Objekt existiert nicht.\n"); throw ERROR; } if(CreatureID == 0 || !IsCreaturePlayer(CreatureID)){ return; } Object Con = Obj.getContainer(); ObjectType ConType = Con.getObjectType(); if(!ConType.isMapContainer()){ return; } ObjectType ObjType = Obj.getObjectType(); if(ObjType.isCreatureContainer() && Obj.getCreatureID() == CreatureID){ return; } Object Best = NONE; bool BestIsCreature = false; Object Help = GetFirstContainerObject(Con); while(Help != NONE){ if(Help != Ignore){ ObjectType HelpType = Help.getObjectType(); // NOTE(fusion): We're looking for the top move object. The creature // check here makes sure only the first creature found is kept as the // best candidate, since creatures on a map container are in a sequence. if(Best == NONE || (!HelpType.getFlag(UNMOVE) && (!BestIsCreature || !HelpType.isCreatureContainer()))){ Best = Help; BestIsCreature = HelpType.isCreatureContainer(); } if(GetObjectPriority(Help) == PRIORITY_LOW){ break; } } Help = Help.getNextObject(); } if(Obj != Best){ throw NOTACCESSIBLE; } } void CheckTopUseObject(uint32 CreatureID, Object Obj){ if(!Obj.exists()){ error("CheckTopUseObject: Objekt existiert nicht.\n"); throw ERROR; } if(CreatureID == 0 || !IsCreaturePlayer(CreatureID)){ return; } Object Con = Obj.getContainer(); ObjectType ConType = Con.getObjectType(); if(!ConType.isMapContainer()){ return; } Object Best = NONE; Object Help = GetFirstContainerObject(Con); while(Help != NONE){ ObjectType HelpType = Help.getObjectType(); if(Best == NONE || (!HelpType.isCreatureContainer() && !HelpType.getFlag(LIQUIDPOOL))){ Best = Help; } if(HelpType.getFlag(FORCEUSE) || GetObjectPriority(Help) == PRIORITY_LOW){ break; } Help = Help.getNextObject(); } if(Obj != Best){ throw NOTACCESSIBLE; } } void CheckTopMultiuseObject(uint32 CreatureID, Object Obj){ if(!Obj.exists()){ error("CheckTopMultiuseObject: Objekt existiert nicht.\n"); throw ERROR; } if(CreatureID == 0 || !IsCreaturePlayer(CreatureID)){ return; } Object Con = Obj.getContainer(); ObjectType ConType = Con.getObjectType(); if(!ConType.isMapContainer()){ return; } Object Best = NONE; Object Help = GetFirstContainerObject(Con); while(Help != NONE){ ObjectType HelpType = Help.getObjectType(); if(Best == NONE || !HelpType.getFlag(LIQUIDPOOL)){ Best = Help; } if(HelpType.getFlag(FORCEUSE) || GetObjectPriority(Help) >= PRIORITY_CREATURE){ break; } Help = Help.getNextObject(); } if(Obj != Best){ throw NOTACCESSIBLE; } } void CheckMoveObject(uint32 CreatureID, Object Obj, bool Take){ if(CreatureID == 0){ return; } if(!ObjectAccessible(CreatureID, Obj, 1)){ throw NOTACCESSIBLE; } ObjectType ObjType = Obj.getObjectType(); if(ObjType.getFlag(UNMOVE)){ throw NOTMOVABLE; } if(ObjType.isCreatureContainer() && Obj.getCreatureID() != CreatureID){ TCreature *MovingCreature = GetCreature(Obj); if(MovingCreature == NULL){ error("CheckMoveObject: Kreatur existiert nicht.\n"); throw ERROR; } if(GetRaceUnpushable(MovingCreature->Race) && (WorldType != NON_PVP || !MovingCreature->IsPeaceful())){ throw NOTMOVABLE; } } if(Take && !ObjType.getFlag(TAKE)){ throw NOTTAKABLE; } } // NOTE(fusion): This is a helper function for `CheckMapDestination` and `CheckMapPlace` // and improves the readability of otherwise two convoluted functions. static bool IsMapBlocked(int DestX, int DestY, int DestZ, ObjectType Type){ if(Type.isCreatureContainer()){ return false; } bool HasBank = CoordinateFlag(DestX, DestY, DestZ, BANK); if(HasBank && !CoordinateFlag(DestX, DestY, DestZ, UNPASS)){ return false; } if(!Type.getFlag(UNPASS)){ if(HasBank && !CoordinateFlag(DestX, DestY, DestZ, UNLAY)){ return false; } if(Type.getFlag(HANG)){ bool HasHook = CoordinateFlag(DestX, DestY, DestZ, HOOKSOUTH) || CoordinateFlag(DestX, DestY, DestZ, HOOKEAST); if(HasHook && !CoordinateFlag(DestX, DestY, DestZ, HANG)){ return false; } } } return true; } void CheckMapDestination(uint32 CreatureID, Object Obj, Object MapCon){ if(CreatureID == 0){ return; } int DestX, DestY, DestZ; ObjectType ObjType = Obj.getObjectType(); GetObjectCoordinates(MapCon, &DestX, &DestY, &DestZ); if(IsMapBlocked(DestX, DestY, DestZ, ObjType)){ throw NOROOM; } int OrigX, OrigY, OrigZ; GetObjectCoordinates(Obj, &OrigX, &OrigY, &OrigZ); if(ObjType.isCreatureContainer()){ if(std::abs(OrigX - DestX) > 1 || std::abs(OrigY - DestY) > 1 || std::abs(OrigZ - DestZ) > 1){ throw OUTOFRANGE; } if(DestZ == (OrigZ - 1)){ if(GetHeight(OrigX, OrigY, OrigZ) < 24){ // JUMP_HEIGHT ? throw NOROOM; } }else if(DestZ == (OrigZ + 1)){ if(GetHeight(DestX, DestY, DestZ) < 24){ // JUMP_HEIGHT ? throw NOWAY; } } TCreature *MovingCreature = GetCreature(Obj); if(MovingCreature == NULL){ error("CheckMapDestination: Moving creature is NULL."); throw ERROR; } if(OrigZ == DestZ || MovingCreature->Type != MONSTER){ if(!MovingCreature->MovePossible(DestX, DestY, DestZ, true, OrigZ != DestZ)){ if(CreatureID == MovingCreature->ID){ throw MOVENOTPOSSIBLE; }else{ throw NOROOM; } } if(CreatureID != MovingCreature->ID){ if(CoordinateFlag(DestX, DestY, DestZ, AVOID)){ throw NOROOM; } if(IsProtectionZone(OrigX, OrigY, OrigZ) && !IsProtectionZone(DestX, DestY, DestZ)){ throw PROTECTIONZONE; } } } }else{ if(!ObjType.getFlag(TAKE) && !ObjectInRange(CreatureID, MapCon, 2)){ throw OUTOFRANGE; } } // TODO(fusion): This looks awfully similar to `ObjectAccessible` outside // from the exceptions and floor check. if(ObjType.getFlag(HANG)){ bool HookSouth = CoordinateFlag(DestX, DestY, DestZ, HOOKSOUTH); bool HookEast = CoordinateFlag(DestX, DestY, DestZ, HOOKEAST); if(HookSouth || HookEast){ TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("CheckMapDestination: Ausführende Kreatur existiert nicht.\n"); throw ERROR; } if(Creature->posz > DestZ){ throw UPSTAIRS; }else if(Creature->posz < DestZ){ throw DOWNSTAIRS; } if(HookSouth){ if(Creature->posy < DestY || Creature->posy > (DestY + 1) || Creature->posx < (DestX - 1) || Creature->posx > (DestX + 1)){ throw OUTOFRANGE; } } if(HookEast){ if(Creature->posx < DestX || Creature->posx > (DestX + 1) || Creature->posy < (DestY - 1) || Creature->posy > (DestY + 1)){ throw OUTOFRANGE; } } return; } } if(!ThrowPossible(OrigX, OrigY, OrigZ, DestX, DestY, DestZ, 1)){ throw CANNOTTHROW; } } void CheckMapPlace(uint32 CreatureID, ObjectType Type, Object MapCon){ int DestX, DestY, DestZ; GetObjectCoordinates(MapCon, &DestX, &DestY, &DestZ); if(!Type.getFlag(UNMOVE) && !CoordinateFlag(DestX, DestY, DestZ, BANK)){ if(!Type.getFlag(HANG)){ throw NOROOM; } if(!CoordinateFlag(DestX, DestY, DestZ, HOOKSOUTH) && !CoordinateFlag(DestX, DestY, DestZ, HOOKEAST)){ throw NOROOM; } } // TODO(fusion): The original function used a slightly modified version of // `IsMapBlocked` which wouldn't check if there was already a HANG item at // the destination but since this function seems to only be called from // `Create` which sets `CreatureID` to zero, I assume this was probably an // oversight? if(CreatureID != 0 && IsMapBlocked(DestX, DestY, DestZ, Type)){ throw NOROOM; } } void CheckContainerDestination(Object Obj, Object Con){ if(IsHeldByContainer(Con, Obj)){ throw CROSSREFERENCE; } ObjectType ConType = Con.getObjectType(); if(!ConType.getFlag(CHEST)){ int ConObjects = CountObjectsInContainer(Con); int ConCapacity = (int)ConType.getAttribute(CAPACITY); if(ConObjects >= ConCapacity){ throw CONTAINERFULL; } } } void CheckContainerPlace(ObjectType Type, Object Con, Object OldObj){ if(Type == GetSpecialObject(DEPOT_CHEST)){ return; } ObjectType ConType = Con.getObjectType(); if(!ConType.getFlag(CHEST)){ if(Type.getFlag(UNMOVE)){ throw NOTMOVABLE; } if(!Type.getFlag(TAKE)){ throw NOTTAKABLE; } if(OldObj == NONE){ int ConObjects = CountObjectsInContainer(Con); int ConCapacity = (int)ConType.getAttribute(CAPACITY); if(ConObjects >= ConCapacity){ throw CONTAINERFULL; } } } } void CheckDepotSpace(uint32 CreatureID, Object Source, Object Destination, int Count){ if(CreatureID == 0){ return; } TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("CheckDepotSpace: Kreatur %u existiert nicht.\n", CreatureID); throw ERROR; } if(Destination == NONE){ error("CheckDepotSpace: Ziel existiert nicht.\n"); throw ERROR; } if(Creature->Type == PLAYER){ TPlayer *Player = (TPlayer*)Creature; if(Player->Depot == NONE || Count <= Player->DepotSpace){ return; } if(!IsHeldByContainer(Source, Player->Depot) && IsHeldByContainer(Destination, Player->Depot)){ throw CONTAINERFULL; } } } void CheckInventoryDestination(Object Obj, Object Con, bool Split){ ObjectType ObjType = Obj.getObjectType(); int ConPosition = GetObjectBodyPosition(Con); bool HandContainer = ConPosition == INVENTORY_RIGHTHAND || ConPosition == INVENTORY_LEFTHAND; if(!HandContainer && ConPosition != INVENTORY_AMMO){ if(!ObjType.getFlag(CLOTHES)){ throw WRONGPOSITION; } int ObjPosition = (int)ObjType.getAttribute(BODYPOSITION); if(ObjPosition == 0){ throw WRONGPOSITION2; }else if(ObjPosition != ConPosition){ throw WRONGCLOTHES; } } uint32 CreatureID = GetObjectCreatureID(Con); if(HandContainer && ObjType.isTwoHanded()){ Object RightHand = GetBodyObject(CreatureID, INVENTORY_RIGHTHAND); if(RightHand != NONE && RightHand != Obj){ throw HANDSNOTFREE; } Object LeftHand = GetBodyObject(CreatureID, INVENTORY_LEFTHAND); if(LeftHand != NONE && LeftHand != Obj){ throw HANDSNOTFREE; } return; } if(GetBodyObject(CreatureID, ConPosition) != NONE){ throw NOROOM; } if(HandContainer){ for(int Position = INVENTORY_HAND_FIRST; Position <= INVENTORY_HAND_LAST; Position += 1){ Object Other = GetBodyObject(CreatureID, Position); if(Other != NONE){ ObjectType OtherType = Other.getObjectType(); if(OtherType.isTwoHanded()){ throw HANDBLOCKED; } if(Split || Other != Obj){ if(OtherType.isWeapon() && ObjType.isWeapon()){ throw ONEWEAPONONLY; } } } } } } void CheckInventoryPlace(ObjectType Type, Object Con, Object OldObj){ // TODO(fusion): This is awfully similar to `CheckInventoryDestination` with // a few subtle differences. Perhaps they use the same underlying function? int ConPosition = GetObjectBodyPosition(Con); bool HandContainer = ConPosition == INVENTORY_RIGHTHAND || ConPosition == INVENTORY_LEFTHAND; if(!HandContainer && ConPosition != INVENTORY_AMMO){ if(!Type.getFlag(CLOTHES)){ throw WRONGPOSITION; } int TypePosition = (int)Type.getAttribute(BODYPOSITION); if(TypePosition == 0){ throw WRONGPOSITION2; }else if(TypePosition != ConPosition){ throw WRONGCLOTHES; } } uint32 CreatureID = GetObjectCreatureID(Con); if(HandContainer && Type.isTwoHanded()){ Object RightHand = GetBodyObject(CreatureID, INVENTORY_RIGHTHAND); if(RightHand != NONE && RightHand != OldObj){ throw HANDSNOTFREE; } Object LeftHand = GetBodyObject(CreatureID, INVENTORY_LEFTHAND); if(LeftHand != NONE && LeftHand != OldObj){ throw HANDSNOTFREE; } return; } if(GetBodyObject(CreatureID, ConPosition) != NONE && GetBodyObject(CreatureID, ConPosition) != OldObj){ throw NOROOM; } if(HandContainer){ for(int Position = INVENTORY_HAND_FIRST; Position <= INVENTORY_HAND_LAST; Position += 1){ Object Other = GetBodyObject(CreatureID, Position); if(Other != NONE && Other != OldObj){ ObjectType OtherType = Other.getObjectType(); if(OtherType.isTwoHanded()){ throw HANDBLOCKED; } if(OtherType.isWeapon() && Type.isWeapon()){ throw ONEWEAPONONLY; } } } } } void CheckWeight(uint32 CreatureID, Object Obj, int Count){ if(GetObjectCreatureID(Obj) == CreatureID){ return; } TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("CheckWeight: Kreatur %d existiert nicht.\n", CreatureID); throw ERROR; } ObjectType ObjType = Obj.getObjectType(); if(!ObjType.getFlag(TAKE)){ throw TOOHEAVY; } if(Creature->Type == PLAYER){ if(CheckRight(CreatureID, UNLIMITED_CAPACITY)){ return; } if(CheckRight(CreatureID, ZERO_CAPACITY)){ throw TOOHEAVY; } } TSkill *CarryStrength = Creature->Skills[SKILL_CARRY_STRENGTH]; if(CarryStrength == NULL){ error("CheckWeight: Skill CARRYSTRENGTH existiert nicht.\n"); throw ERROR; } int ObjWeight; if(ObjType.getFlag(CUMULATIVE)){ ObjWeight = GetWeight(Obj, Count); }else{ ObjWeight = GetCompleteWeight(Obj); } int InventoryWeight = GetInventoryWeight(CreatureID); int MaxWeight = CarryStrength->Get() * 100; if((InventoryWeight + ObjWeight) > MaxWeight){ throw TOOHEAVY; } } void CheckWeight(uint32 CreatureID, ObjectType Type, uint32 Value, int OldWeight){ TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("CheckWeight: Kreatur %d existiert nicht.\n", CreatureID); throw ERROR; } if(!Type.getFlag(TAKE)){ throw TOOHEAVY; } if(Creature->Type == PLAYER){ if(CheckRight(CreatureID, UNLIMITED_CAPACITY)){ return; } if(CheckRight(CreatureID, ZERO_CAPACITY)){ throw TOOHEAVY; } } TSkill *CarryStrength = Creature->Skills[SKILL_CARRY_STRENGTH]; if(CarryStrength == NULL){ error("CheckWeight: Skill CARRYSTRENGTH existiert nicht.\n"); throw ERROR; } int TypeWeight = (int)Type.getAttribute(WEIGHT); if(Type.getFlag(CUMULATIVE) && (int)Value > 1){ TypeWeight *= (int)Value; } if(TypeWeight > OldWeight){ int InventoryWeight = GetInventoryWeight(CreatureID); int MaxWeight = CarryStrength->Get() * 100; if((InventoryWeight + TypeWeight) > MaxWeight){ throw TOOHEAVY; } } } void NotifyCreature(uint32 CreatureID, Object Obj, bool Inventory){ if(CreatureID == 0){ return; } ObjectType ObjType = Obj.getObjectType(); if(!ObjType.isCreatureContainer()){ TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("NotifyCreature: Kreatur existiert nicht.\n"); return; } if(Inventory && ObjType.getFlag(LIGHT)){ AnnounceChangedCreature(CreatureID, CREATURE_LIGHT_CHANGED); } Creature->NotifyChangeInventory(); } } void NotifyCreature(uint32 CreatureID, ObjectType Type, bool Inventory){ if(CreatureID == 0){ return; } // TODO(fusion): Shouldn't we check if `Type` is creature container too? TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("NotifyCreature: Kreatur existiert nicht.\n"); return; } if(Inventory && Type.getFlag(LIGHT)){ AnnounceChangedCreature(CreatureID, CREATURE_LIGHT_CHANGED); } Creature->NotifyChangeInventory(); } void NotifyAllCreatures(Object Obj, int Type, Object OldCon){ if(!Obj.exists()){ error("NotifyAllCreatures: Übergebenes Objekt existiert nicht.\n"); return; } ObjectType ObjType = Obj.getObjectType(); if(!ObjType.isCreatureContainer()){ return; } int ObjX, ObjY, ObjZ; uint32 CreatureID = Obj.getCreatureID(); GetObjectCoordinates(Obj, &ObjX, &ObjY, &ObjZ); constexpr int StimulusRadius = 10; int SearchRadiusX = StimulusRadius; int SearchRadiusY = StimulusRadius; int SearchCenterX = ObjX; int SearchCenterY = ObjY; if(Type == OBJECT_MOVED){ if(!OldCon.exists()){ error("NotifyAllCreatures: Übergebener alter Container existiert nicht.\n"); return; } // NOTE(fusion): Report stimulus as `OBJECT_CHANGED`. Type = OBJECT_CHANGED; // NOTE(fusion): Perform a secondary search if the distances are beyond // the stimulus diameter or, extend the primary search otherwise. int OldX, OldY, OldZ; GetObjectCoordinates(OldCon, &OldX, &OldY, &OldZ); int DistanceX = std::abs(ObjX - OldX); int DistanceY = std::abs(ObjY - OldY); if(DistanceX > (StimulusRadius * 2) || DistanceY > (StimulusRadius * 2)){ TFindCreatures Search(StimulusRadius, StimulusRadius, OldX, OldY, FIND_ALL); while(true){ uint32 SpectatorID = Search.getNext(); if(SpectatorID == 0){ break; } // TODO(fusion): Check if the spectator is NULL? TCreature *Spectator = GetCreature(SpectatorID); Spectator->CreatureMoveStimulus(CreatureID, Type); } }else{ SearchRadiusX = StimulusRadius + (DistanceX + 1) / 2; SearchRadiusY = StimulusRadius + (DistanceY + 1) / 2; SearchCenterX = (ObjX + OldX) / 2; SearchCenterY = (ObjY + OldY) / 2; } } TFindCreatures Search(SearchRadiusX, SearchRadiusY, SearchCenterX, SearchCenterY, FIND_ALL); while(true){ uint32 SpectatorID = Search.getNext(); if(SpectatorID == 0){ break; } // TODO(fusion): Check if the spectator is NULL? TCreature *Spectator = GetCreature(SpectatorID); Spectator->CreatureMoveStimulus(CreatureID, Type); } } void NotifyTrades(Object Obj){ if(!Obj.exists()){ error("NotifyTrades: Übergebenes Objekt existiert nicht.\n"); return; } ObjectType ObjType = Obj.getObjectType(); if(ObjType.isCreatureContainer()){ return; } // TODO(fusion): These radii seem to be correct but I'm skeptical because // you're required to be close to the trade object to even start the trade. // Maybe you can walk around after a trade is started? TFindCreatures Search(12, 10, Obj, FIND_PLAYERS); while(true){ uint32 CharacterID = Search.getNext(); if(CharacterID == 0){ break; } TPlayer *Player = GetPlayer(CharacterID); if(Player == NULL){ error("NotifyTrade: Spieler existiert nicht.\n"); continue; } Object TradeObject = Player->InspectTrade(true, 0); if(TradeObject != NONE){ if(IsHeldByContainer(Obj, TradeObject) || IsHeldByContainer(TradeObject, Obj)){ SendCloseTrade(Player->Connection); SendMessage(Player->Connection, TALK_FAILURE_MESSAGE, "Trade cancelled."); Player->RejectTrade(); } } } } void NotifyDepot(uint32 CreatureID, Object Obj, int Count){ if(CreatureID == 0){ return; } TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("NotifyDepot: Kreatur %u existiert nicht.\n", CreatureID); // TODO(fusion): We should probably also throw here. //throw ERROR; return; } if(!Obj.exists()){ error("NotifyDepot: Objekt existiert nicht.\n"); throw ERROR; } if(Creature->Type == PLAYER){ TPlayer *Player = (TPlayer*)Creature; if(Player->Depot == NONE){ return; } if(IsHeldByContainer(Obj, Player->Depot)){ Player->DepotSpace += Count; print(3, "Neuer Depot-Freiraum für %s: %d\n", Player->Name, Player->DepotSpace); } } } void CloseContainer(Object Con, bool Force){ if(!Con.exists()){ error("CloseContainer: Übergebener Container existiert nicht.\n"); return; } ObjectType ConType = Con.getObjectType(); if(ConType.isCreatureContainer()){ return; } // TODO(fusion): Similar to `NotifyTrades`. TFindCreatures Search(12, 10, Con, FIND_PLAYERS); while(true){ uint32 CharacterID = Search.getNext(); if(CharacterID == 0){ break; } TPlayer *Player = GetPlayer(CharacterID); if(Player == NULL){ error("CloseContainer: Spieler existiert nicht.\n"); continue; } for(int ContainerNr = 0; ContainerNr < NARRAY(Player->OpenContainer); ContainerNr += 1){ Object OpenCon = Player->GetOpenContainer(ContainerNr); if(IsHeldByContainer(OpenCon, Con)){ if(Force || !ObjectAccessible(Player->ID, Con, 1)){ Player->SetOpenContainer(ContainerNr, NONE); SendCloseContainer(Player->Connection, ContainerNr); }else{ // TODO(fusion): Why are we doing this? Is the container being refreshed? SendContainer(Player->Connection, ContainerNr); } } } } } Object Create(Object Con, ObjectType Type, uint32 Value){ if(!Con.exists()){ error("Create: Zielobjekt existiert nicht.\n"); throw ERROR; } ObjectType ConType = Con.getObjectType(); if(!ConType.getFlag(CONTAINER) && !ConType.getFlag(CHEST)){ error("Create: Zielobjekt ist kein Container.\n"); throw ERROR; } // TODO(fusion): The map container does indeed use type id zero but it seems // it is also considered a "no type" id so we might also want to add an alias // to it as `isValid()`, `isVoid()`, `isNone()`, or `isNull()` perhaps. if(Type.isMapContainer()){ error("Create: Objekttyp existiert nicht.\n"); throw ERROR; } if(ConType.isMapContainer()){ CheckMapPlace(0, Type, Con); }else if(ConType.isBodyContainer()){ CheckInventoryPlace(Type, Con, NONE); }else{ CheckContainerPlace(Type, Con, NONE); } uint32 ConOwnerID = GetObjectCreatureID(Con); if(ConOwnerID != 0){ CheckWeight(ConOwnerID, Type, Value, 0); } // NOTE(fusion): Attempt to merge with top object. if(ConType.isMapContainer() && Type.getFlag(CUMULATIVE)){ int ConX, ConY, ConZ; GetObjectCoordinates(Con, &ConX, &ConY, &ConZ); Object Top = GetTopObject(ConX, ConY, ConZ, true); if(Top != NONE && Top.getObjectType() == Type){ uint32 Count = Top.getAttribute(AMOUNT); if(Value != 0){ Count += Value; }else{ Count += 1; } if(Count <= 100){ Change(Top, AMOUNT, Count); return Top; } } } // TODO(fusion): I feel these usages of `Value` should be exclusive? Object Obj = SetObject(Con, Type, (Type.isCreatureContainer() ? Value : 0)); if(Type.getFlag(CUMULATIVE)){ ChangeObject(Obj, AMOUNT, Value); } if(Type.getFlag(MAGICFIELD)){ ChangeObject(Obj, RESPONSIBLE, Value); } if(Type.getFlag(LIQUIDPOOL)){ ChangeObject(POOLLIQUIDTYPE, Value); } if(Type.getFlag(LIQUIDCONTAINER)){ ChangeObject(CONTAINERLIQUIDTYPE, Value); } if(Type.getFlag(KEY)){ ChangeObject(KEYNUMBER, Value); } if(Type.getFlag(RUNE)){ ChangeObject(CHARGES, Value); } if(Type.isCreatureContainer()){ // BUG(fusion): We should just check this before creating the object. // Also, using `Delete` instead of `DeleteObject` here is problematic // because the object's placement wasn't yet broadcasted. TCreature *Creature = GetCreature(Value); if(Creature == NULL){ error("Create: Ungültige Kreatur-ID %u\n", Value); Delete(Obj, -1); throw ERROR; } // IMPORTANT(fusion): Creature body containers are created here and their // type ids match inventory slots exactly. We're looping in reverse because // `SetObject` will insert objects at the front of the object chain. for(int Position = INVENTORY_LAST; Position >= INVENTORY_FIRST; Position -= 1){ SetObject(Obj, ObjectType(Position), 0); } Creature->NotifyCreate(); } AnnounceChangedObject(Obj, OBJECT_CREATED); NotifyTrades(Obj); NotifyCreature(ConOwnerID, Obj, ConType.isBodyContainer()); MovementEvent(Obj, Con, Con); // TODO(fusion): This one doesn't make a lot of sense. CollisionEvent(Obj, Con); NotifyAllCreatures(Obj, OBJECT_CREATED, NONE); return Obj; } Object Copy(Object Con, Object Source){ if(!Con.exists()){ error("Copy: Ziel existiert nicht.\n"); throw ERROR; } ObjectType ConType = Con.getObjectType(); if(!ConType.getFlag(CONTAINER) && !ConType.getFlag(CHEST)){ error("Copy: Ziel ist kein Container.\n"); throw ERROR; } if(!Source.exists()){ error("Copy: Quelle existiert nicht.\n"); throw ERROR; } ObjectType SourceType = Source.getObjectType(); if(SourceType.isCreatureContainer()){ error("Copy: Quelle ist eine Kreatur.\n"); throw ERROR; } if(ConType.isMapContainer()){ CheckMapDestination(0, Source, Con); }else if(ConType.isBodyContainer()){ // TODO(fusion): Not sure about this third param. CheckInventoryDestination(Source, Con, true); }else{ CheckContainerDestination(Source, Con); } uint32 ConOwnerID = GetObjectCreatureID(Con); if(ConOwnerID != 0){ CheckWeight(ConOwnerID, Source, -1); } // TODO(fusion): Using `Copy` recursively here should work because the object // is placed when `CopyObject` is called. The thing is, announce and notify // functions will do meaningless work there (I think, check what happens when // we're actually running). Object Obj = CopyObject(Con, Source); if(SourceType.getFlag(CONTAINER)){ Object Help = GetFirstContainerObject(Source); while(Help != NONE){ Copy(Obj, Help); Help = Help.getNextObject(); } } AnnounceChangedObject(Obj, OBJECT_CREATED); NotifyTrades(Obj); NotifyCreature(ConOwnerID, Obj, ConType.isBodyContainer()); MovementEvent(Obj, Con, Con); // TODO(fusion): Same as `Create`. CollisionEvent(Obj, Con); NotifyAllCreatures(Obj, OBJECT_CREATED, NONE); return Obj; } void Move(uint32 CreatureID, Object Obj, Object Con, int Count, bool NoMerge, Object Ignore){ if(!Obj.exists()){ error("Move: Übergebenes Objekt existiert nicht.\n"); throw ERROR; } if(!Con.exists()){ int ObjX, ObjY, ObjZ; ObjectType ObjType = Obj.getObjectType(); GetObjectCoordinates(Obj, &ObjX, &ObjY, &ObjZ); error("Move: Zielobjekt existiert nicht.\n"); error("# Objekt %d an [%d,%d,%d]\n", ObjType.TypeID, ObjX, ObjY, ObjZ); throw ERROR; } ObjectType ConType = Con.getObjectType(); if(!ConType.getFlag(CONTAINER)){ error("Move: Zielobjekt ist kein Container.\n"); throw ERROR; } ObjectType ObjType = Obj.getObjectType(); if(ObjType.getFlag(CUMULATIVE) && (Count == 0 || Count > (int)Obj.getAttribute(AMOUNT))){ error("Move: Ungültige Anzahl %d von %d an Teilen bei kumulierbarem Objekt.\n", Count, (int)Obj.getAttribute(AMOUNT)); throw ERROR; } Object OldCon = Obj.getContainer(); if(!NoMerge && ConType.isMapContainer() && ObjType.getFlag(CUMULATIVE) && OldCon != Con){ int ConX, ConY, ConZ; GetObjectCoordinates(Con, &ConX, &ConY, &ConZ); Object Top = GetTopObject(ConX, ConY, ConZ, true); if(Top != NONE){ try{ Merge(CreatureID, Obj, Top, Count, Ignore); return; // <-- EARLY RETURN IF `Merge` SUCCEEDS. }catch(RESULT r){ if(r == DESTROYED){ throw; } } } } // TODO(fusion): The only event inside `Merge` that could modify the object // and still throw some non `DESTROYED` error is the separation event. // From what I've seen, it is used exclusively with non CUMULATIVE objects // like depot switches to perform "step out" events but using them with // CUMULATIVE objects could be problematic as the event is triggered on the // whole object which includes both the remainder and moved parts, meaning // that transforming the object then would already make the merge "invalid". if(!Obj.exists()){ error("Move: Übergebenes Objekt existiert nicht mehr.\n"); } // NOTE(fusion): Refresh object type just in case a merge event modified it. ObjType = Obj.getObjectType(); int ObjCount = 1; if(ObjType.getFlag(CUMULATIVE)){ ObjCount = (int)Obj.getAttribute(AMOUNT); } if(Count == -1){ Count = ObjCount; } bool Split = Count < ObjCount; uint32 ObjOwnerID = GetObjectCreatureID(Obj); uint32 ConOwnerID = GetObjectCreatureID(Con); CheckTopMoveObject(CreatureID, Obj, Ignore); if(ConType.isMapContainer()){ CheckMoveObject(CreatureID, Obj, false); CheckMapDestination(CreatureID, Obj, Con); }else{ CheckMoveObject(CreatureID, Obj, true); if(ConType.isBodyContainer()){ CheckInventoryDestination(Obj, Con, Split); }else{ CheckContainerDestination(Obj, Con); } if(Split){ CheckDepotSpace(CreatureID, NONE, Con, 1); }else{ CheckDepotSpace(CreatureID, Obj, Con, CountObjects(Obj)); } if(ConOwnerID != 0){ CheckWeight(ConOwnerID, Obj, Count); } } // NOTE(fusion): `Obj` becomes the split part while `Remainder`, the remaining part. Object Remainder = NONE; if(Split){ Remainder = Obj; Obj = SplitObject(Obj, Count); } if(OldCon != Con){ SeparationEvent(Obj, OldCon); } // TODO(fusion): There is surely a way to reduce these creature checks? // perhaps split them into two linear paths, with a few duplications but // easier to read. if(!ObjType.isCreatureContainer() && Remainder == NONE){ AnnounceChangedObject(Obj, OBJECT_DELETED); NotifyTrades(Obj); NotifyDepot(CreatureID, Obj, CountObjects(Obj)); } // TODO(fusion): This could probably be moved to the end of the function // along with the other `CloseContainer`. // Probably the other way around, since events could modify the object // and I don't think it makes a difference if we close it before or after // performing the move. // Except it does because the object's position will be different lol. if(ObjType.getFlag(CONTAINER) && CreatureID == 0){ CloseContainer(Obj, true); } if(ObjType.isCreatureContainer()){ uint32 MovingCreatureID = Obj.getCreatureID(); TCreature *Creature = GetCreature(MovingCreatureID); if(Creature != NULL){ Creature->NotifyTurn(Con); }else{ error("Move: Kreatur mit ungültiger ID %d.\n", MovingCreatureID); } AnnounceMovingCreature(MovingCreatureID, Con); } MoveObject(Obj, Con); if(!ObjType.isCreatureContainer()){ AnnounceChangedObject(Obj, OBJECT_CREATED); NotifyTrades(Obj); NotifyDepot(CreatureID, Obj, -CountObjects(Obj)); } if(Remainder != NONE){ AnnounceChangedObject(Remainder, OBJECT_CHANGED); NotifyTrades(Remainder); } if(ObjType.isCreatureContainer()){ uint32 MovingCreatureID = Obj.getCreatureID(); TCreature *Creature = GetCreature(MovingCreatureID); if(Creature != NULL){ Creature->NotifyGo(); }else{ error("Move: Kreatur mit ungültiger ID %d.\n", MovingCreatureID); } } NotifyCreature(ObjOwnerID, Obj, OldCon.getObjectType().isBodyContainer()); NotifyCreature(ConOwnerID, Obj, Con.getObjectType().isBodyContainer()); if(ObjType.getFlag(CONTAINER)){ CloseContainer(Obj, false); } MovementEvent(Obj, OldCon, Con); CollisionEvent(Obj, Con); NotifyAllCreatures(Obj, OBJECT_MOVED, OldCon); } void Merge(uint32 CreatureID, Object Obj, Object Dest, int Count, Object Ignore){ if(!Obj.exists()){ error("Merge: Übergebenes Objekt existiert nicht.\n"); throw ERROR; } if(!Dest.exists()){ error("Merge: Zielobjekt existiert nicht.\n"); throw ERROR; } ObjectType ObjType = Obj.getObjectType(); if(ObjType.getFlag(CUMULATIVE) && (Count == 0 || Count > (int)Obj.getAttribute(AMOUNT))){ error("Merge: Ungültige Anzahl %d an Teilen bei kumulierbarem Objekt.\n", Count); throw ERROR; } if(Obj == Dest){ return; } if(ObjType != Dest.getObjectType()){ throw NOMATCH; } if(!ObjType.getFlag(CUMULATIVE)){ throw NOTCUMULABLE; } int ObjCount = Obj.getAttribute(AMOUNT); int DestCount = Dest.getAttribute(AMOUNT); if(Count == -1){ Count = ObjCount; } if((Count + DestCount) > 100){ throw TOOMANYPARTS; } Object DestCon = Dest.getContainer(); uint32 DestOwnerID = GetObjectCreatureID(Dest); CheckTopMoveObject(CreatureID, Obj, Ignore); if(DestCon.getObjectType().isMapContainer()){ CheckMoveObject(CreatureID, Obj, false); CheckMapDestination(CreatureID, Obj, DestCon); }else{ CheckMoveObject(CreatureID, Obj, true); CheckDepotSpace(CreatureID, Obj, DestCon, 0); if(DestOwnerID != 0){ CheckWeight(DestOwnerID, Obj, Count); } } Object ObjCon = Obj.getContainer(); uint32 ObjOwnerID = GetObjectCreatureID(Obj); // TODO(fusion): What is even going on here? if(ObjType.getFlag(MOVEMENTEVENT)){ error("Merge: Movement-Event für kumulierbares Objekt %d wird nicht ausgelöst.\n", ObjType.TypeID); } if(ObjCon != DestCon){ SeparationEvent(Obj, ObjCon); } if(Count < ObjCount){ ChangeObject(Obj, AMOUNT, ObjCount - Count); AnnounceChangedObject(Obj, OBJECT_CHANGED); NotifyTrades(Obj); ChangeObject(Dest, AMOUNT, DestCount + Count); }else{ AnnounceChangedObject(Obj, OBJECT_DELETED); NotifyTrades(Obj); NotifyDepot(CreatureID, Obj, 1); MergeObjects(Obj, Dest); } AnnounceChangedObject(Dest, OBJECT_CHANGED); NotifyTrades(Dest); NotifyCreature(ObjOwnerID, Dest, ObjCon.getObjectType().isBodyContainer()); NotifyCreature(DestOwnerID, Dest, DestCon.getObjectType().isBodyContainer()); CollisionEvent(Dest, DestCon); NotifyAllCreatures(Dest, OBJECT_CHANGED, NONE); } void Change(Object Obj, ObjectType NewType, uint32 Value){ if(Obj.exists()){ error("Change: Übergebenes Objekt existiert nicht.\n"); throw ERROR; } ObjectType OldType = Obj.getObjectType(); if(OldType.isCreatureContainer() || NewType.isCreatureContainer()){ error("Change: Kann keine Kreaturen verändern.\n"); throw ERROR; } // TODO(fusion): Same thing as `Create`. The zero type id is also used as // a "no type" id. if(NewType.isMapContainer()){ Delete(Obj, -1); return; } if(OldType.getFlag(CONTAINER)){ if(!NewType.getFlag(CONTAINER) && GetFirstContainerObject(Obj) != NONE){ error("Change: Zielobjekt %d für %d ist kein Container mehr.\n", NewType.TypeID, OldType.TypeID); throw EMPTYCONTAINER; } if(NewType.getFlag(CONTAINER)){ int ObjectCount = CountObjectsInContainer(Obj); int NewCapacity = (int)NewType.getAttribute(CAPACITY); if(ObjectCount < NewCapacity){ error("Change: Zielobjekt %d für %d ist kleinerer Container.\n", NewType.TypeID, OldType.TypeID); throw EMPTYCONTAINER; } } } if(OldType.getFlag(CHEST) && !NewType.getFlag(CHEST)){ error("Change: Schatztruhe muß Schatztruhe bleiben.\n"); throw ERROR; } if(OldType.getFlag(CUMULATIVE) && OldType != NewType && Obj.getAttribute(AMOUNT) > 1){ throw SPLITOBJECT; } Object Con = Obj.getContainer(); ObjectType ConType = Con.getObjectType(); uint32 ObjOwnerID = GetObjectCreatureID(Obj); if(ObjOwnerID != 0 && OldType != NewType){ if(ConType.isBodyContainer()){ CheckInventoryPlace(NewType, Con, Obj); }else{ CheckContainerPlace(NewType, Con, Obj); } uint32 Count = 1; if(OldType.getFlag(CUMULATIVE)){ Count = Obj.getAttribute(AMOUNT); } int OldWeight = GetWeight(Obj, -1); CheckWeight(ObjOwnerID, NewType, Count, OldWeight); } if(OldType.getFlag(CONTAINER) && !NewType.getFlag(CONTAINER)){ CloseContainer(Obj, true); } ChangeObject(Obj, NewType); // TODO(fusion): Same thing as `Create`. I feel these usages of `Value` // should be exclusive. if(NewType.getFlag(CUMULATIVE) && !OldType.getFlag(CUMULATIVE)){ ChangeObject(Obj, AMOUNT, Value); } if(NewType.getFlag(MAGICFIELD) && !OldType.getFlag(MAGICFIELD)){ ChangeObject(Obj, RESPONSIBLE, Value); } if(NewType.getFlag(LIQUIDPOOL) && !OldType.getFlag(LIQUIDPOOL)){ ChangeObject(Obj, POOLLIQUIDTYPE, Value); } if(NewType.getFlag(LIQUIDCONTAINER) && !OldType.getFlag(LIQUIDCONTAINER)){ ChangeObject(Obj, CONTAINERLIQUIDTYPE, Value); } if(NewType.getFlag(KEY) && !OldType.getFlag(KEY)){ ChangeObject(Obj, KEYNUMBER, Value); } if(NewType.getFlag(RUNE) && !OldType.getFlag(RUNE)){ ChangeObject(Obj, CHARGES, Value); } AnnounceChangedObject(Obj, OBJECT_CHANGED); NotifyTrades(Obj); NotifyCreature(ObjOwnerID, Obj, ConType.isBodyContainer()); NotifyAllCreatures(Obj, OBJECT_CHANGED, NONE); } void Change(Object Obj, INSTANCEATTRIBUTE Attribute, uint32 Value){ if(!Obj.exists()){ error("Change: Übergebenes Objekt existiert nicht.\n"); throw ERROR; } if(Attribute == CONTENT || Attribute == CHESTQUESTNUMBER || Attribute == DOORLEVEL || Attribute == DOORQUESTNUMBER || Attribute == DOORQUESTVALUE || Attribute == ABSTELEPORTDESTINATION || Attribute == REMAININGEXPIRETIME){ error("Change: Kann Attribut %d nicht ändern.\n", Attribute); throw ERROR; } uint32 ObjOwnerID = GetObjectCreatureID(Obj); if(ObjOwnerID != 0 && Attribute == AMOUNT && Value > Obj.getAttribute(AMOUNT)){ int OldWeight = GetWeight(Obj, -1); CheckWeight(ObjOwnerID, Obj.getObjectType(), Value, OldWeight); } ChangeObject(Obj, Attribute, Value); if(Attribute == AMOUNT || Attribute == CONTAINERLIQUIDTYPE || Attribute == POOLLIQUIDTYPE){ AnnounceChangedObject(Obj, OBJECT_CHANGED); } NotifyTrades(Obj); NotifyCreature(ObjOwnerID, Obj, Obj.getContainer().getObjectType().isBodyContainer()); } void Delete(Object Obj, int Count){ if(!Obj.exists()){ error("Delete: Übergebenes Objekt existiert nicht.\n"); throw ERROR; } ObjectType ObjType = Obj.getObjectType(); if(ObjType.getFlag(CUMULATIVE) && Count > (int)Obj.getAttribute(AMOUNT)){ error("Delete: Ungültige Anzahl %d an Teilen bei kumulierbarem Objekt.\n", Count); throw ERROR; } Object Remainder = NONE; if(ObjType.getFlag(CUMULATIVE) && Count != -1 && Count < (int)Obj.getAttribute(AMOUNT)){ Remainder = Obj; Obj = SplitObject(Obj, Count); }else{ NotifyTrades(Obj); } if(ObjType.getFlag(CONTAINER) || ObjType.getFlag(CHEST)){ if(ObjType.getFlag(CONTAINER)){ CloseContainer(Obj, true); } Object Help = GetFirstContainerObject(Obj); while(Help != NONE){ Object Next = Help.getNextObject(); DeleteObject(Help); Help = Next; } } Object Con = Obj.getContainer(); uint32 ObjOwnerID = GetObjectCreatureID(Obj); SeparationEvent(Obj, Con); // BUG(fusion): Object may have been destroyed by the separation event? This // looks suspicious because even if the object is properly destroyed, its // remainder could be left unannounced. // If it's a container, its objects don't get a separation event trigger, // and the separation event trigger on the container itself is only triggered // after it is empty. if(Obj.exists()){ if(Remainder == NONE){ AnnounceChangedObject(Obj, OBJECT_DELETED); } NotifyAllCreatures(Obj, OBJECT_DELETED, NONE); if(ObjType.isCreatureContainer()){ TCreature *Creature = GetCreature(ObjOwnerID); if(Creature != NULL){ Creature->NotifyDelete(); }else{ error("Delete: Kreatur mit ungültiger ID %d.\n", ObjOwnerID); } } DeleteObject(Obj); if(Remainder != NONE){ AnnounceChangedObject(Remainder, OBJECT_CHANGED); NotifyTrades(Remainder); } NotifyCreature(ObjOwnerID, ObjType, Con.getObjectType().isBodyContainer()); } } void Empty(Object Con, int Remainder){ if(!Con.exists()){ error("Empty: Übergebener Container existiert nicht.\n"); throw ERROR; } if(Remainder < 0){ error("Empty: Ungültiger Rest %d.\n", Remainder); throw ERROR; } ObjectType ConType = Con.getObjectType(); if(!ConType.getFlag(CONTAINER)){ error("Empty: Übergebenes Objekt ist kein Container.\n"); throw ERROR; } CloseContainer(Con, true); bool IsCorpse = ConType.getFlag(CORPSE); int ObjectCount = CountObjectsInContainer(Con); Object Help = GetFirstContainerObject(Con); while(Help != NONE && ObjectCount > Remainder){ Object Next = Help.getNextObject(); if(IsCorpse){ Delete(Help, -1); }else{ Move(0, Help, Con.getContainer(), -1, false, NONE); } Help = Next; ObjectCount -= 1; } } void GraphicalEffect(int x, int y, int z, int Type){ AnnounceGraphicalEffect(x, y, z, Type); } void GraphicalEffect(Object Obj, int Type){ if(Obj.exists()){ int ObjX, ObjY, ObjZ; GetObjectCoordinates(Obj, &ObjX, &ObjY, &ObjZ); AnnounceGraphicalEffect(ObjX, ObjY, ObjZ, Type); } } void TextualEffect(Object Obj, int Color, const char *Format, ...){ if(!Obj.exists()){ error("TextualEffect: Übergebener Mapcontainer existiert nicht.\n"); return; } char Buffer[10]; va_list ap; va_start(ap, Format); vsnprintf(Buffer, sizeof(Buffer), Format, ap); va_end(ap); int ObjX, ObjY, ObjZ; GetObjectCoordinates(Obj, &ObjX, &ObjY, &ObjZ); AnnounceTextualEffect(ObjX, ObjY, ObjZ, Color, Buffer); } void Missile(Object Start, Object Dest, int Type){ if(!Start.exists()){ error("Missile: Übergebener Startpunkt existiert nicht.\n"); return; } if(!Dest.exists()){ error("Missile: Übergebener Zielpunkt existiert nicht.\n"); return; } int StartX, StartY, StartZ; int DestX, DestY, DestZ; GetObjectCoordinates(Start, &StartX, &StartY, &StartZ); GetObjectCoordinates(Dest, &DestX, &DestY, &DestZ); AnnounceMissile(StartX, StartY, StartZ, DestX, DestY, DestZ, Type); }