From 3cc587ab8df80d42d9dab1a73a37701f565a3499 Mon Sep 17 00:00:00 2001 From: fusion32 Date: Sun, 15 Jun 2025 17:35:06 -0300 Subject: more work on `operate.cc` --- src/operate.cc | 1025 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1015 insertions(+), 10 deletions(-) (limited to 'src/operate.cc') diff --git a/src/operate.cc b/src/operate.cc index 1e1f9a6..2224ed4 100644 --- a/src/operate.cc +++ b/src/operate.cc @@ -4,6 +4,23 @@ #include "stubs.hh" +#include +#include + +static fifo Statements(1024); +static fifo Listeners(1024); + +static vector Channel(0, 18, 10); +static int Channels; +static int CurrentChannelID; +static int CurrentSubscriberNumber; + +static vector Party(0, 100, 50); +static int Parties; + +// World Operations +// ============================================================================= + // 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 @@ -383,7 +400,9 @@ void CheckTopMultiuseObject(uint32 CreatureID, Object Obj){ Best = Help; } - if(HelpType.getFlag(FORCEUSE) || GetObjectPriority(Help) >= PRIORITY_CREATURE){ + if(HelpType.getFlag(FORCEUSE) + || GetObjectPriority(Help) == PRIORITY_CREATURE + || GetObjectPriority(Help) == PRIORITY_LOW){ break; } @@ -2145,6 +2164,14 @@ void Look(uint32 CreatureID, Object Obj){ } void Talk(uint32 CreatureID, int Mode, const char *Addressee, const char *Text, bool CheckSpamming){ + // TODO(fusion): `Text` was originally `char*`, but I wanted to improve + // string handling overall and modifying string parameters is a bad idea, + // specially when you don't know their capacity. + // It is only modified when a player uses `TALK_YELL` to make it upper + // case, which shouldn't be a problem, but if we plan to do any cleanup + // afterwards, we need to get this right from the beginning. + char YellBuffer[256]; + TCreature *Creature = GetCreature(CreatureID); if(Creature == NULL){ error("Talk: Übergebene Kreatur %d existiert nicht.\n", CreatureID); @@ -2217,11 +2244,9 @@ void Talk(uint32 CreatureID, int Mode, const char *Addressee, const char *Text, throw EXHAUSTED; } - // TODO(fusion): This would be valid if `Text` was `char*`, but I - // wanted to improve the quality of string handling and parameters - // across the board, and now we need some scratch buffer to actually - // modify the text. - strUpper(Text); + strcpy(YellBuffer, Text); + strUpper(YellBuffer); + Text = YellBuffer; } if(Mode == TALK_CHANNEL_CALL && Channel == 5){ @@ -2277,7 +2302,7 @@ void Talk(uint32 CreatureID, int Mode, const char *Addressee, const char *Text, } if(Mode == TALK_PRIVATE_MESSAGE){ - Muting = Player->RecordMessage(Receiver->ID); + int Muting = Player->RecordMessage(Receiver->ID); if(Muting > 0){ SendMessage(Player->Connection, TALK_FAILURE_MESSAGE, "You have addressed too many players. You are muted for %d second%s.", @@ -2320,7 +2345,7 @@ void Talk(uint32 CreatureID, int Mode, const char *Addressee, const char *Text, TFindCreatures Search(Radius, Radius, Creature->posx, Creature->posy, FIND_PLAYERS); while(true){ - uint32 SpectatorID = Search.findNext(); + uint32 SpectatorID = Search.getNext(); if(SpectatorID == 0){ break; } @@ -2413,8 +2438,8 @@ void Talk(uint32 CreatureID, int Mode, const char *Addressee, const char *Text, continue; } - // TODO(fusion): We should probably review this. What if both player - // guild names are empty? Is it checked elsewhere? + // TODO(fusion): We should probably review this. You'd assume creature + // should be a player when talking to any channel. if(Channel == 0 && (Creature->Type != PLAYER || strcmp(((TPlayer*)Creature)->Guild, Subscriber->Guild) != 0)){ continue; @@ -2449,3 +2474,983 @@ void Talk(uint32 CreatureID, int Mode, const char *Addressee, const char *Text, } } } + +void Use(uint32 CreatureID, Object Obj1, Object Obj2, uint8 Info){ + if(!Obj1.exists()){ + error("Use: Übergebenes Objekt Obj 1 existiert nicht.\n"); + throw ERROR; + } + + if(Obj2 != NONE && !Obj2.exists()){ + error("Use: Übergebenes Objekt Obj 2 existiert nicht.\n"); + throw ERROR; + } + + CheckTopUseObject(CreatureID, Obj1); + if(!ObjectAccessible(CreatureID, Obj1, 1)){ + throw NOTACCESSIBLE; + } + + ObjectType ObjType1 = Obj1.getObjectType(); + if(Obj2 != NONE){ + // TODO(fusion): Is this correct? + if(!ObjType1.getFlag(DISTUSE)){ + CheckTopMultiuseObject(CreatureID, Obj2); + } + + if(!ObjectAccessible(CreatureID, Obj2, 1)){ + if(!ObjType1.getFlag(DISTUSE)){ + throw NOTACCESSIBLE; + } + + TCreature *Creature = GetCreature(CreatureID); + if(Creature == NULL){ + error("Use: Verursachende Kreatur existiert nicht.\n"); + throw ERROR; + } + + int ObjX2, ObjY2, ObjZ2; + GetObjectCoordinates(Obj2, &ObjX2, &ObjY2, &ObjZ2); + if(Creature->posz > ObjZ2){ + throw UPSTAIRS; + }else if(Creature->posz < ObjZ2){ + throw DOWNSTAIRS; + } + + if(!ThrowPossible(Creature->posx, Creature->posy, Creature->posz, ObjX2, ObjY2, ObjZ2, 0)){ + throw CANNOTTHROW; + } + } + } + + if(ObjType1.getFlag(CONTAINER)){ + UseContainer(CreatureID, Obj1, Info); + }else if(ObjType1.getFlag(CHEST)){ + UseChest(CreatureID, Obj1); + }else if(ObjType1.getFlag(LIQUIDCONTAINER)){ + UseLiquidContainer(CreatureID, Obj1, Obj2); + }else if(ObjType1.getFlag(FOOD)){ + UseFood(CreatureID, Obj1); + }else if(ObjType1.getFlag(WRITE) || ObjType1.getFlag(WRITEONCE)){ + UseTextObject(CreatureID, Obj1); + }else if(ObjType1.getFlag(INFORMATION)){ + UseAnnouncer(CreatureID, Obj1); + }else if(ObjType1.getFlag(RUNE)){ + UseMagicItem(CreatureID, Obj1, Obj2); + }else if(ObjType1.getFlag(KEY)){ + UseKeyDoor(CreatureID, Obj1, Obj2); + }else if(ObjType1.getFlag(NAMEDOOR)){ + UseNameDoor(CreatureID, Obj1); + }else if(ObjType1.getFlag(LEVELDOOR)){ + UseLevelDoor(CreatureID, Obj1); + }else if(ObjType1.getFlag(QUESTDOOR)){ + UseQuestDoor(CreatureID, Obj1); + }else if(ObjType1.isCloseWeapon() + && Obj2 != NONE + && Obj2.getObjectType().getFlag(DESTROY)){ + UseWeapon(CreatureID, Obj1, Obj2); + }else if(ObjType1.getFlag(CHANGEUSE)){ + UseChangeObject(CreatureID, Obj1); + }else if(ObjType1.getFlag(USEEVENT)){ + if(Obj2 != NONE){ + UseObjects(CreatureID, Obj1, Obj2); + }else{ + UseObject(CreatureID, Obj1); + } + }else if(ObjType1.getFlag(TEXT) + && ObjType1.getAttribute(FONTSIZE) == 1){ + UseTextObject(CreatureID, Obj1); + }else{ + throw NOTUSABLE; + } +} + +void Turn(uint32 CreatureID, Object Obj){ + if(!Obj.exists()){ + error("Turn: Übergebenes Objekt existiert nicht.\n"); + throw ERROR; + } + + if(!ObjectAccessible(CreatureID, Obj, 1)){ + throw NOTACCESSIBLE; + } + + ObjectType ObjType = Obj.getObjectType(); + if(!ObjType.getFlag(ROTATE)){ + throw NOTTURNABLE; + } + + ObjectType RotateTarget = ObjType.getAttribute(ROTATETARGET); + if(RotateTarget == ObjType){ + error("Turn: Objekt %d wird durch Drehen zerstört.\n", ObjType.TypeID); + } + + Change(Obj, RotateTarget, 0); +} + +void CreatePool(Object Con, ObjectType Type, uint32 Value){ + if(!Con.exists()){ + error("CreatePool: Übergebener MapContainer existiert nicht.\n"); + throw ERROR; + } + + ObjectType ConType = Con.getObjectType(); + if(!ConType.isMapContainer()){ + error("CreatePool: Übergebenes Objekt ist kein MapContainer.\n"); + throw ERROR; + } + + // NOTE(fusion): There can be no liquid pools on fields with other BOTTOM + // objects, with the exception of other liquid pools, in which case the new + // liquid pool would replace the old one. + Object Help = GetFirstContainerObject(Con); + while(Help != NONE){ + Object Next = Help.getNextObject(); + ObjectType HelpType = Help.getObjectType(); + if(HelpType.getFlag(BOTTOM)){ + if(!HelpType.getFlag(LIQUIDPOOL)){ + throw NOROOM; + } + + try{ + Delete(Help, -1); + }catch(RESULT r){ + error("CreatePool: Exception %d beim Löschen des alten Pools.\n", r); + } + } + Help = Next; + } + + Create(Con, Type, Value); +} + +void EditText(uint32 CreatureID, Object Obj, const char *Text){ + if(!Obj.exists()){ + error("EditText: Übergebenes Objekt existiert nicht.\n"); + throw ERROR; + } + + ObjectType ObjType = Obj.getObjectType(); + if(!ObjType.getFlag(WRITE) && (!ObjType.getFlag(WRITEONCE) || Obj.getAttribute(TEXTSTRING) != 0)){ + error("EditText: Objekt ist nicht beschreibbar.\n"); + throw ERROR; + } + + TCreature *Creature = GetCreature(CreatureID); + if(Creature == NULL){ + error("EditText: Kreatur existiert nicht.\n"); + throw ERROR; + } + + int TextLength = (int)strlen(Text); + int MaxLength = (ObjType.getFlag(WRITE) + ? ObjType.getAttribute(MAXLENGTH) + : ObjType.getAttribute(MAXLENGTHONCE)); + if(TextLength >= MaxLength){ + throw TOOLONG; + } + + // TODO(fusion): Similar to maybe inlined function in `Look`. + // TODO(fusion): Same as in `Look`. `CreatureID` can't be zero here or we'd + // have already returned so its probably some inlined function. + if(CreatureID != 0 && !ObjectAccessible(CreatureID, Obj, 1)){ + throw NOTACCESSIBLE; + } + + uint32 ObjText = Obj.getAttribute(TEXTSTRING); + if((ObjText != 0 && strcmp(GetDynamicString(ObjText), Text) == 0) + || (ObjText == 0 && Text[0] == 0)){ + print(3, "Text hat sich nicht geändert.\n"); + return; + } + + DeleteDynamicString(ObjText); + Change(Obj, TEXTSTRING, AddDynamicString(Text)); + + DeleteDynamicString(Obj.getAttribute(EDITOR)); + Change(Obj, EDITOR, AddDynamicString(Creature->Name)); +} + +Object CreateAtCreature(uint32 CreatureID, ObjectType Type, uint32 Value){ + // TODO(fusion): Bruhh... + + TCreature *Creature = GetCreature(CreatureID); + if(Creature == NULL){ + // TODO(fusion): Different function name? + error("GiveObjectToCreature: Kann Kreatur nicht finden.\n"); + throw ERROR; + } + + try{ + CheckWeight(CreatureID, Type, Value, 0); + }catch(RESULT r){ + return Create(GetMapContainer(Creature->CrObject), Type, Value); + } + + Object Obj = NONE; + bool CheckContainers = Type.getFlag(MOVEMENTEVENT); + for(int i = 0; i < 2; i += 1){ + bool TooHeavy = false; + for(int Position = INVENTORY_FIRST; + Position <= INVENTORY_LAST; + Position += 1){ + try{ + Object BodyObj = GetBodyObject(CreatureID, Position); + if(CheckContainers){ + if(BodyObj != NONE && BodyObj.getObjectType().getFlag(CONTAINER)){ + Obj = Create(BodyObj, Type, Value); + break; + } + }else{ + if(BodyObj == NONE){ + Obj = Create(GetBodyContainer(CreatureID, Position), Type, Value); + break; + } + } + }catch(RESULT r){ + // TODO(fusion): Is this even possible if we're checking the + // weight before hand? + if(r == TOOHEAVY){ + TooHeavy = true; + break; + } + } + } + + if(Obj != NONE || TooHeavy){ + break; + } + + CheckContainers = !CheckContainers; + } + + if(Obj == NONE){ + Obj = Create(GetMapContainer(Creature->CrObject), Type, Value); + } + + return Obj; +} + +void DeleteAtCreature(uint32 CreatureID, ObjectType Type, int Amount, uint32 Value){ + while(Amount > 0){ + Object Obj = GetInventoryObject(CreatureID, Type, Value); + if(Obj == NONE){ + error("DeleteAtCreature: Kein Objekt gefunden.\n"); + throw ERROR; + } + + if(Type.getFlag(CUMULATIVE)){ + int ObjAmount = (int)Obj.getAttribute(AMOUNT); + if(ObjAmount < Amount){ + Delete(Obj, -1); + Amount -= ObjAmount; + }else{ + Change(Obj, AMOUNT, (ObjAmount - Amount)); + Amount = 0; + } + }else{ + Delete(Obj, -1); + Amount -= 1; + } + } +} + +void ProcessCronSystem(void){ + while(true){ + Object Obj = CronCheck(); + if(Obj == NONE){ + break; + } + + ObjectType ObjType = Obj.getObjectType(); + ObjectType ExpireTarget = ObjType.getAttribute(EXPIRETARGET); + if(ObjType.getFlag(CONTAINER)){ + int Remainder = 0; + if(!ExpireTarget.isMapContainer() && ExpireTarget.getFlag(CONTAINER)){ + Remainder = (int)ExpireTarget.getAttribute(CAPACITY); + } + + Empty(Obj, Remainder); + } + + Change(Obj, ExpireTarget, 0); + } +} + +bool SectorRefreshable(int SectorX, int SectorY, int SectorZ){ + // TODO(fusion): Have the sector size defined as a constant in `map.hh`? + //constexpr int SectorSize = 32; + int SearchRadiusX = 32 - 1; + int SearchRadiusY = 32 - 1; + int SearchCenterX = SectorX * 32 + (32 / 2); + int SearchCenterY = SectorY * 32 + (32 / 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){ + error("SectorRefreshable: Kreatur existiert nicht.\n"); + continue; + } + + // TODO(fusion): Maybe an inlined function to check whether a player + // can see some floor. All floors above ground, when above ground. Or + // two floors up and down, when underground. + if(Player->posz <= 7){ + if(SectorZ <= 7){ + return false; + } + }else{ + if(std::abs(Player->posz - SectorZ) <= 2){ + return false; + } + } + } + + return true; +} + +void RefreshSector(int SectorX, int SectorY, int SectorZ, const uint8 *Data, int Count){ + if(!SectorRefreshable(SectorX, SectorY, SectorZ)){ + return; + } + + TReadBuffer Buffer(Data, Count); + RefreshSector(SectorX, SectorY, SectorZ, &Buffer); + + // TODO(fusion): This function is very similar to `ApplyPatch`. + int SearchRadiusX = 32 / 2; + int SearchRadiusY = 32 / 2; + int SearchCenterX = SectorX * 32 + (32 / 2); + int SearchCenterY = SectorY * 32 + (32 / 2); + TFindCreatures Search(SearchRadiusX, SearchRadiusY, SearchCenterX, SearchCenterY, FIND_ALL); + while(true){ + uint32 CreatureID = Search.getNext(); + if(CreatureID == 0){ + break; + } + + TCreature *Creature = GetCreature(CreatureID); + if(Creature == NULL){ + error("RefreshSector: Kreatur existiert nicht.\n"); + continue; + } + + if(Creature->posz != SectorZ){ + continue; + } + + bool FieldBlocked = false; + int FieldX = Creature->posx; + int FieldY = Creature->posy; + int FieldZ = Creature->posz; + Object Obj = GetFirstObject(FieldX, FieldY, FieldZ); + while(Obj != NONE){ + ObjectType ObjType = Obj.getObjectType(); + if(!ObjType.isCreatureContainer() && ObjType.getFlag(UNPASS)){ + FieldBlocked = true; + break; + } + Obj = Obj.getNextObject(); + } + + if(FieldBlocked){ + try{ + if(!SearchFreeField(&FieldX, &FieldY, &FieldZ, 1, 0, false)){ + if(Creature->Type == NPC){ + print(2, "NPC auf [%d,%d,%d] muß auf seinen Start zurückgesetzt werden.\n", FieldX, FieldY, FieldZ); + FieldX = Creature->startx; + FieldY = Creature->starty; + FieldZ = Creature->startz; + }else if(Creature->Type == MONSTER){ + print(2, "Monster auf [%d,%d,%d] muß gelöscht werden.\n", FieldX, FieldY, FieldZ); + delete Creature; + continue; + }else{ + error("Spieler auf [%d,%d,%d] ist von Refresh betroffen.\n", FieldX, FieldY, FieldZ); + continue; + } + } + + Object MapCon = GetMapContainer(FieldX, FieldY, FieldZ); + Move(0, Creature->CrObject, MapCon, -1, false, NONE); + }catch(RESULT r){ + error("RefreshSector: Exception %d beim Neusetzen des Monsters.\n", r); + } + } + } +} + +void RefreshMap(void){ + TDynamicWriteBuffer HelpBuffer(0x10000); + for(int SectorZ = SectorZMin; SectorZ <= SectorZMax; SectorZ += 1) + for(int SectorY = SectorYMin; SectorY <= SectorYMax; SectorY += 1) + for(int SectorX = SectorXMin; SectorX <= SectorXMax; SectorX += 1){ + if(!SectorRefreshable(SectorX, SectorY, SectorZ)){ + continue; + } + + char FileName[4096]; + snprintf(FileName, sizeof(FileName), "%s/%04u-%04u-%02u.sec", + ORIGMAPPATH, SectorX, SectorY, SectorZ); + if(!FileExists(FileName)){ + continue; + } + + bool Refreshable = false; + int OffsetX = -1; + int OffsetY = -1; + HelpBuffer.reset(); + try{ + TReadScriptFile Script; + Script.open(FileName); + while(true){ + Script.nextToken(); + if(Script.Token == ENDOFFILE){ + Script.close(); + break; + } + + if(Script.Token == SPECIAL && Script.getSpecial() == ','){ + continue; + } + + if(Script.Token == BYTES){ + uint8 *SectorOffset = Script.getBytesequence(); + OffsetX = (int)SectorOffset[0]; + OffsetY = (int)SectorOffset[1]; + Script.readSymbol(':'); + continue; + } + + // TODO(fusion): We don't enforce the token to be an identifier + // here as we do when loading any sector file in `map.cc`. + if(Script.Token == IDENTIFIER){ + if(OffsetX == -1 || OffsetY == -1){ + Script.error("coordinate expected"); + } + + const char *Identifier = Script.getIdentifier(); + if(strcmp(Identifier, "refresh") == 0){ + Refreshable = true; + }else if(strcmp(Identifier, "content") == 0){ + Script.readSymbol('='); + if(Refreshable){ + HelpBuffer.writeByte((uint8)OffsetX); + HelpBuffer.writeByte((uint8)OffsetY); + } + LoadObjects(&Script, &HelpBuffer, !Refreshable); + } + } + } + + if(HelpBuffer.Position > 0){ + TReadBuffer ReadBuffer(HelpBuffer.Data, HelpBuffer.Position); + RefreshSector(SectorX, SectorY, SectorZ, &ReadBuffer); + } + }catch(const char *str){ + error("RefreshMap: Fehler beim Bearbeiten der Datei (%s).\n", str); + } + } +} + +void RefreshCylinders(void){ + // TODO(fusion): `RefreshedCylinders` is the number of cylinders we attempt to + // refresh in this function, which is called every minute or so by `AdvanceGame`. + // We should probably rename it to something more clear. + + // TODO(fusion): This might be on purpose but `RefreshX` and `RefreshY` will be + // way below `SectorXMin` and `SectorYMin` the first N times this function is + // called and depending on the value of `RefreshedCylinders`, could take a few + // hours before we even start refreshing the map. + // If the intent is to delay refreshing cylinders until some time after startup, + // we should change how this function is called by `AdvanceGame` instead. + + static int RefreshX = -1; + static int RefreshY = -1; + for(int i = 0; i < RefreshedCylinders; i += 1){ + if(RefreshX < SectorXMax){ + RefreshX += 1; + }else{ + RefreshX = SectorXMin; + if(RefreshY < SectorYMax){ + RefreshY += 1; + }else{ + RefreshY = SectorYMin; + } + } + + for(int RefreshZ = SectorZMin; + RefreshZ <= SectorZMax; + RefreshZ += 1){ + if(SectorRefreshable(RefreshX, RefreshY, RefreshZ)){ + LoadSectorOrder(RefreshX, RefreshY, RefreshZ); + } + } + } +} + +void ApplyPatch(int SectorX, int SectorY, int SectorZ, + bool FullSector, TReadScriptFile *Script, bool SaveHouses){ + if(SectorX < SectorXMin || SectorX > SectorXMax + || SectorY < SectorYMin || SectorY > SectorYMax + || SectorZ < SectorZMin || SectorZ > SectorZMax){ + return; + } + + PrepareHouseCleanup(); + PatchSector(SectorX, SectorY, SectorZ, FullSector, Script, SaveHouses); + FinishHouseCleanup(); + + // TODO(fusion): Similar to `SectorRefreshable` but with a reduced radius. + int SearchRadiusX = 32 / 2; + int SearchRadiusY = 32 / 2; + int SearchCenterX = SectorX * 32 + (32 / 2); + int SearchCenterY = SectorY * 32 + (32 / 2); + TFindCreatures Search(SearchRadiusX, SearchRadiusY, SearchCenterX, SearchCenterY, FIND_ALL); + while(true){ + uint32 CreatureID = Search.getNext(); + if(CreatureID == 0){ + break; + } + + TCreature *Creature = GetCreature(CreatureID); + if(Creature == NULL){ + error("ApplyPatch: Kreatur existiert nicht.\n"); + continue; + } + + if(Creature->posz != SectorZ){ + continue; + } + + bool FieldBlocked = false; + int FieldX = Creature->posx; + int FieldY = Creature->posy; + int FieldZ = Creature->posz; + Object Obj = GetFirstObject(FieldX, FieldY, FieldZ); + while(Obj != NONE){ + ObjectType ObjType = Obj.getObjectType(); + if(!ObjType.isCreatureContainer() && ObjType.getFlag(UNPASS)){ + FieldBlocked = true; + break; + } + Obj = Obj.getNextObject(); + } + + if(!FieldBlocked){ + // TODO(fusion): Not sure why we're moving the creature to the same + // field, specially with `MoveObject`. Does `PatchSector` leave + // creatures on invalid indices? + Object MapCon = GetMapContainer(FieldX, FieldY, FieldZ); + MoveObject(Creature->CrObject, MapCon); + }else{ + try{ + if(!SearchFreeField(&FieldX, &FieldY, &FieldZ, 1, 0, false)){ + if(Creature->Type == NPC){ + print(2, "NPC auf [%d,%d,%d] muß auf seinen Start zurückgesetzt werden.\n", FieldX, FieldY, FieldZ); + FieldX = Creature->startx; + FieldY = Creature->starty; + FieldZ = Creature->startz; + }else if(Creature->Type == MONSTER){ + print(2, "Monster auf [%d,%d,%d] muß gelöscht werden.\n", FieldX, FieldY, FieldZ); + Delete(Creature->CrObject, -1); + Creature->StartLogout(false, false); + continue; + }else{ + error("Spieler auf [%d,%d,%d] ist von Patch betroffen.\n", FieldX, FieldY, FieldZ); + continue; + } + } + + Object MapCon = GetMapContainer(FieldX, FieldY, FieldZ); + Move(0, Creature->CrObject, MapCon, -1, false, NONE); + }catch(RESULT r){ + error("ApplyPatch: Exception %d beim Neusetzen des Monsters.\n", r); + } + } + } +} + +void ApplyPatches(void){ + // TODO(fusion): We don't handle any script exceptions in here which could + // lead to `PatchDir` being leaked. It may not be a problem overall because + // this function is only called at startup by `InitAll`. + + char FileName[4096]; + bool SaveHouses = false; + snprintf(FileName, sizeof(FileName), "%s/save-houses", SAVEPATH); + if(FileExists(FileName)){ + SaveHouses = true; + print(2, "Häuser werden nicht gepatcht.\n"); + unlink(FileName); + } + + DIR *PatchDir = opendir(SAVEPATH); + if(PatchDir == NULL){ + error("ApplyPatches: Unterverzeichnis %s nicht gefunden.\n", SAVEPATH); + return; + } + + int MaxPatch = -1; + while(dirent *DirEntry = readdir(PatchDir)){ + if(DirEntry->d_type != DT_REG){ + continue; + } + + const char *FileExt = findLast(DirEntry->d_name, '.'); + if(FileExt == NULL){ + continue; + } + + if(strcmp(FileExt, ".pat") == 0){ + int Patch; + if(sscanf(DirEntry->d_name, "%d.pat", &Patch) == 1){ + if(MaxPatch < Patch){ + MaxPatch = Patch; + } + } + }else if(strcmp(FileExt, ".sec") == 0){ + int SectorX, SectorY, SectorZ; + if(sscanf(DirEntry->d_name, "%d-%d-%d.sec", &SectorX, &SectorY, &SectorZ) == 3){ + print(2,"Patche kompletten Sektor %d/%d/%d ...\n", SectorX, SectorY, SectorZ); + snprintf(FileName, sizeof(FileName), "%s/%s", SAVEPATH, DirEntry->d_name); + + TReadScriptFile Script; + Script.open(FileName); + ApplyPatch(SectorX, SectorY, SectorZ, true, &Script, SaveHouses); + Script.close(); + unlink(FileName); + } + } + } + + closedir(PatchDir); + + for(int Patch = 0; Patch <= MaxPatch; Patch += 1){ + snprintf(FileName, sizeof(FileName), "%s/%03d.pat", SAVEPATH, Patch); + if(FileExists(FileName)){ + TReadScriptFile Script; + Script.open(FileName); + if(strcmp(Script.readIdentifier(), "sector") != 0){ + Script.error("Sector expected"); + } + + int SectorX = Script.readNumber(); + Script.readSymbol(','); + int SectorY = Script.readNumber(); + Script.readSymbol(','); + int SectorZ = Script.readNumber(); + + print(2, "Patche Teile von Sektor %d/%d/%d (Patch %d) ...\n", + SectorX, SectorY, SectorZ, Patch); + ApplyPatch(SectorX, SectorY, SectorZ, false, &Script, false); + Script.close(); + unlink(FileName); + } + } +} + +// Communication Logging +// ============================================================================= +uint32 LogCommunication(uint32 CreatureID, int Mode, int Channel, const char *Text){ + static uint32 StatementID = 0; + + if(Text == NULL){ + error("LogCommunication: Text ist NULL.\n"); + return 0; + } + + if(CreatureID == 0){ + error("LogCommunication: CharacterID ist Null.\n"); + return 0; + } + + StatementID += 1; + + TStatement *Statement = Statements.append(); + Statement->StatementID = StatementID; + Statement->TimeStamp = (uint32)time(NULL); + Statement->CharacterID = CreatureID; + Statement->Mode = Mode; + Statement->Channel = Channel; + Statement->Text = AddDynamicString(Text); + Statement->Reported = false; + return StatementID; +} + +uint32 LogListener(uint32 StatementID, TPlayer *Player){ + if(StatementID == 0 || Player == NULL + || !CheckRight(Player->ID, LOG_COMMUNICATION)){ + return 0; + } + + TListener *Listener = Listeners.append(); + Listener->StatementID = StatementID; + Listener->CharacterID = Player->ID; + return StatementID; +} + +void ProcessCommunicationControl(void){ + // NOTE(fusion): Remove statements older than 30 minutes. + int Now = (int)time(NULL); + uint32 Limit = 0; + while(true){ + TStatement *Statement = Statements.next(); + if(Statement == NULL || Now <= (Statement->TimeStamp + 1800)){ + if(Statement != NULL){ + Limit = Statement->StatementID; + } + break; + } + + DeleteDynamicString(Statement->Text); + Statements.remove(); + } + + // TODO(fusion): If there are no statements left we'll end up with `Limit` + // equal to zero which will prevent any listeners entry from being removed. + while(true){ + TListener *Listener = Listeners.next(); + if(Listener == NULL || Listener->StatementID >= Limit){ + break; + } + + Listeners.remove(); + } +} + +int GetCommunicationContext(uint32 CharacterID, uint32 StatementID, + int *NumberOfStatements, vector **ReportedStatements){ + TStatement *Statement = NULL; + int StatementsIter = Statements.iterFirst(); + while(true){ + Statement = Statements.iterNext(&StatementsIter); + if(Statement == NULL || Statement->StatementID == StatementID){ + break; + } + } + + if(Statement == NULL){ + return 1; // STATEMENT_UNKNOWN ? + } + + if(CharacterID == 0){ + bool ChannelMode = Statement->Mode == TALK_CHANNEL_CALL + || Statement->Mode == TALK_GAMEMASTER_CHANNELCALL + || Statement->Mode == TALK_HIGHLIGHT_CHANNELCALL; + + bool ReportableChannel = Statement->Channel == 2 // CHANNEL_TUTOR + || Statement->Channel == 4 // CHANNEL_GAME + || Statement->Channel == 5 // CHANNEL_TRADE + || Statement->Channel == 6 // CHANNEL_RLCHAT + || Statement->Channel == 7; // CHANNEL_HELP + + if(!ChannelMode || !ReportableChannel){ + error("GetCommunicationContext: Äußerung dürfte nicht gemeldet werden.\n"); + return 1; // STATEMENT_UNKNOWN ? + } + } + + if(Statement->Reported){ + return 2; // STATEMENT_ALREADY_REPORTED ? + } + + // TODO(fusion): `FreeSpace` limits the amount of data that is gathered. It + // seems to grow/shrink with the size of the text plus an extra 46 bytes for + // each statement entry. This flat memory overhead of 46 bytes could be some + // compilation/decompilation artifact so I've left it out for now. + // The end point of this data is `ProcessPunishmentOrder` which may record + // statements into the database so it may be related to that. + + bool StatementContained = false; + int FreeSpace = KB(16); + *NumberOfStatements = 0; + *ReportedStatements = new vector(0, 100, 100); + StatementsIter = Statements.iterLast(); + while(true){ + TStatement *Current = Statements.iterPrev(&StatementsIter); + if(Current == NULL){ + break; + } + + if(Current->TimeStamp < (Statement->TimeStamp - 180) + || Current->TimeStamp > (Statement->TimeStamp + 60)){ + continue; + } + + if(FreeSpace <= 0 && Current->TimeStamp > Statement->TimeStamp){ + error("GetCommunicationContext: Kontext wird zu groß. Schneide Ende ab.\n"); + break; + } + + if(CharacterID == 0){ + bool ChannelMode = Current->Mode == TALK_CHANNEL_CALL + || Current->Mode == TALK_GAMEMASTER_CHANNELCALL + || Current->Mode == TALK_HIGHLIGHT_CHANNELCALL; + if(!ChannelMode || Current->Channel != Statement->Channel){ + continue; + } + }else{ + // NOTE(fusion): Check if the character listened to the current statement. + TListener *Listener = NULL; + int ListenersIter = Listeners.iterLast(); + while(true){ + Listener = Listeners.iterPrev(&ListenersIter); + if(Listener == NULL + || Listener->StatementID > Current->StatementID + || (Listener->StatementID == Current->StatementID + && Listener->CharacterID == CharacterID)){ + break; + } + } + + if(Listener == NULL || Listener->StatementID != Current->StatementID){ + continue; + } + } + + TReportedStatement *Entry = (*ReportedStatements)->at(*NumberOfStatements); + Entry->StatementID = Current->StatementID; + Entry->TimeStamp = Current->TimeStamp; + Entry->CharacterID = Current->CharacterID; + Entry->Mode = Current->Mode; + Entry->Channel = Current->Channel; + if(Current->Text != 0){ + // TODO(fusion): OOF SIZE: Large. + strcpy(Entry->Text, GetDynamicString(Current->Text)); + }else{ + Entry->Text[0] = 0; + } + *NumberOfStatements += 1; + FreeSpace -= (int)strlen(Entry->Text); + + if(Current->StatementID == StatementID){ + StatementContained = true; + } + } + + if(FreeSpace < 0){ + error("GetCommunicationContext: Kontext wird um %d Bytes zu groß. Lösche Anfang.\n", -FreeSpace); + + for(int i = 0; i < *NumberOfStatements && FreeSpace < 0; i += 1){ + TReportedStatement *Entry = (*ReportedStatements)->at(i); + if(Entry->StatementID != StatementID){ + Entry->StatementID = 0; + FreeSpace += (int)strlen(Entry->Text); + } + } + + if(FreeSpace < 0){ + error("GetCommunicationContext: FreeSpace ist immer noch negativ (%d).\n", FreeSpace); + } + } + + if(!StatementContained){ + error("GetCommunicationContext: Statement ist nicht im Kontext enthalten.\n"); + delete *ReportedStatements; + *NumberOfStatements = 0; + *ReportedStatements = NULL; + return 1; // STATEMENT_UNKNOWN ? + } + + Statement->Reported = true; + return 0; // STATEMENT_REPORTED ? +} + +// Channel +// ============================================================================= +TChannel::TChannel(void) : Subscriber(0, 10, 10), InvitedPlayer(0, 10, 10) { + this->Moderator = 0; + this->ModeratorName[0] = 0; + this->Subscribers = 0; + this->InvitedPlayers = 0; +} + +TChannel::TChannel(const TChannel &Other) : TChannel() { + this->operator=(Other); +} + +void TChannel::operator=(const TChannel &Other){ + this->Subscribers = Other.Subscribers; + for(int i = 0; i < Other.Subscribers; i += 1){ + *this->Subscriber.at(i) = Other.Subscriber.copyAt(i); + } + + this->InvitedPlayers = Other.InvitedPlayers; + for(int i = 0; i < Other.InvitedPlayers; i += 1){ + *this->InvitedPlayer.at(i) = Other.InvitedPlayer.copyAt(i); + } +} + +bool ChannelActive(int ChannelID){ + if(ChannelID < 0 || ChannelID >= Channels){ + error("ChannelActive: Ungültige Kanalnummer %d.\n", ChannelID); + return false; + } + + return ChannelID <= 7 || Channel.at(ChannelID)->Moderator != 0; +} + +const char *GetChannelName(int ChannelID, uint32 CharacterID){ + static char ChannelName[40]; + + if(!ChannelActive(ChannelID)){ + return "Unknown"; + } + + TPlayer *Player = GetPlayer(CharacterID); + if(Player == NULL){ + error("GetChannelName: Spieler existiert nicht.\n"); + return "Unknown"; + } + + switch(ChannelID){ + case 0: return Player->Guild; + case 1: return "Gamemaster"; + case 2: return "Tutor"; + case 3: return "Rule Violations"; + case 4: return "Game-Chat"; + case 5: return "Trade"; + case 6: return "RL-Chat"; + case 7: return "Help"; + default:{ + if(ChannelID > 7){ + snprintf(ChannelName, sizeof(ChannelName), "%s\'s Channel", + Channel.at(ChannelID)->ModeratorName); + return ChannelName; + }else{ + error("GetChannelName: Unbenutzter Kanal %d.\n", ChannelID); + return "Unknown"; + } + } + } +} + +// Party +// ============================================================================= +TParty::TParty(void) : Member(0, 10, 10), InvitedPlayer(0, 10, 10) { + this->Leader = 0; + this->Members = 0; + this->InvitedPlayers = 0; +} + +TParty::TParty(const TParty &Other) : TParty() { + this->operator=(Other); +} + +void TParty::operator=(const TParty &Other){ + this->Members = Other.Members; + for(int i = 0; i < Other.Members; i += 1){ + *this->Member.at(i) = Other.Member.copyAt(i); + } + + this->InvitedPlayers = Other.InvitedPlayers; + for(int i = 0; i < Other.InvitedPlayers; i += 1){ + *this->InvitedPlayer.at(i) = Other.InvitedPlayer.copyAt(i); + } +} -- cgit v1.2.3