diff options
| author | fusion32 <marcopuzziello@gmail.com> | 2025-05-24 20:57:24 -0300 |
|---|---|---|
| committer | fusion32 <marcopuzziello@gmail.com> | 2025-05-24 20:57:24 -0300 |
| commit | 3f557369c7bbd08fdf2e6dbd9271873b20a9a144 (patch) | |
| tree | 28bb9b4ad9c0324036d753eea772855a180c37f3 /src/map.cc | |
| parent | b7432b4e74284138f1740f4577b9cb32e1f120d0 (diff) | |
| download | game-3f557369c7bbd08fdf2e6dbd9271873b20a9a144.tar.gz game-3f557369c7bbd08fdf2e6dbd9271873b20a9a144.zip | |
halfway of `map.cc` and start of `objects.cc`
Diffstat (limited to 'src/map.cc')
| -rw-r--r-- | src/map.cc | 1418 |
1 files changed, 1418 insertions, 0 deletions
diff --git a/src/map.cc b/src/map.cc new file mode 100644 index 0000000..604fd6b --- /dev/null +++ b/src/map.cc @@ -0,0 +1,1418 @@ +#include "map.hh" +#include "containers.hh" + +// NOTE(fusion): This is used by hash table entries and sectors to tell whether +// they're currently loaded or swapped out to disk. +enum : uint8 { + STATUS_FREE = 0, + STATUS_LOADED = 1, + STATUS_SWAPPED = 2, + + // TODO(fusion): It seems this is only used with the `NONE` entry in the + // hash table. I haven't seen it used **yet** but It may have a purpose + // aside from preventing swap outs. + STATUS_PERMANENT = 255, +}; + +// NOTE(fusion): This is used to determine precedence order of different objects +// in a tile. +enum : int { + PRIORITY_BANK = 0, + PRIORITY_CLIP = 1, + PRIORITY_BOTTOM = 2, + PRIORITY_TOP = 3, + PRIORITY_CREATURE = 4, + PRIORITY_OTHER = 5, +}; + +static int OBCount; +static int SectorXMin; +static int SectorXMax; +static int SectorYMin; +static int SectorYMax; +static int SectorZMin; +static int SectorZMax; +static int RefreshedCylinders; +static int NewbieStartPositionX; +static int NewbieStartPositionY; +static int NewbieStartPositionZ; +static int VeteranStartPositionX; +static int VeteranStartPositionY; +static int VeteranStartPositionZ; + +static matrix3d<TSector*> *Sector; +static TObjectBlock **ObjectBlock; +static TObject *FirstFreeObject; +static TObject **HashTableData; +static uint8 *HashTableType; +static uint32 HashTableSize; +static uint32 HashTableMask; +static uint32 HashTableFree; +static uint32 ObjectCounter; + +static vector<TCronEntry> CronEntry(0, 256, 256); +static int CronHashTable[2047]; +static int CronEntries; + +static vector<TDepotInfo> DepotInfo(0, 4, 5); +static int Depots; + +static vector<TMask> Mark(0, 4, 5); +static int Marks; + +static TDynamicWriteBuffer HelpBuffer(0x10000); + +// Map Init/Exit and Helpers +// ============================================================================= +static void SwapObject(TWriteBinaryFile *File, Object Obj, uint32 FileNumber){ + ASSERT(Obj != NONE); + + // NOTE(fusion): Does it make sense to swap an object that isn't loaded? We + // were originally calling `Object::exists` that would swap in the object's + // sector if it was swapped out. We should probably have an assertion here. + if(HashTableType[Obj.ObjectID & HashTableMask] != STATUS_LOADED){ + error("SwapObject: Object doesn't exists or is not currently loaded.\n"); + return; + } + + TObject *Entry = HashTableData[Obj.ObjectID & HashTableMask]; + if(Entry->ObjectID != Obj.ObjectID){ + error("SwapObject: Übergebenes Objekt existiert nicht.\n"); + return; + } + + File->writeBytes((const uint8*)Entry, sizeof(TObject)); + if(Entry->Type.getFlag(CONTAINER) || Entry->Type.getFlag(CHEST)){ + Object Current = Obj.getAttribute(CONTENT); + while(Current != NONE){ + Object Next = Current.getNextObject(); + SwapObject(File, Current, FileNumber); + Current = Next; + } + } + + PutFreeObjectSlot(Entry); + HashTableType[EntryIndex] = STATUS_SWAPPED; + HashTableData[EntryIndex] = (TObject*)FileNumber; +} + +static void SwapSector(void){ + static uint32 FileNumber = 0; + + TSector *Oldest = NULL; + int OldestSectorX = 0; + int OldestSectorY = 0; + int OldestSectorZ = 0; + uint32 OldestTimeStamp = RoundNr + 1; + + ASSERT(Sector != NULL); + for(int SectorZ = SectorZMin; SectorZ <= SectorZMax; SectorZ += 1) + for(int SectorY = SectorYMin; SectorY <= SectorYMax; SectorY += 1) + for(int SectorX = SectorXMin; SectorX <= SectorXMax; SectorX += 1){ + TSector *CurrentSector = *Sector->at(SectorX, SectorY, SectorZ); + if(CurrentSector != NULL + && CurrentSector->Status == STATUS_LOADED + && CurrentSector->TimeStamp < OldestTimeStamp){ + Oldest = CurrentSector; + OldestSectorX = SectorX; + OldestSectorY = SectorY; + OldestSectorZ = SectorZ; + OldestTimeStamp = CurrentSector->TimeStamp; + } + } + + if(Oldest == NULL){ + error("FATAL ERROR in SwapSector: Es kann kein Sektor ausgelagert werden.\n"); + abort(); + } + + char FileName[4096]; + do{ + FileNumber += 1; // NOTE(fusion): Let it wrap naturally. + snprintf(FileName, sizeof(FileName), "%s/%010u.swp", SAVEPATH, FileNumber); + }while(FileExists(FileName)); + + TWriteBinaryFile File; + try{ + File.open(FileName); + print(2, "Lagere Sektor %d/%d/%d aus...\n", OldestSectorX, OldestSectorY, OldestSectorZ); + File.writeQuad((uint32)OldestSectorX); + File.writeQuad((uint32)OldestSectorY); + File.writeQuad((uint32)OldestSectorZ); + // TODO(fusion): I think tiles are stored in column major order but it doesn't + // really matter as long as optimize for sequential access. + for(int X = 0; X < 32; X += 1){ + for(int Y = 0; Y < 32; Y += 1){ + SwapObject(&File, Oldest->MapCon[X][Y], FileNumber); + } + } + Oldest->Status = STATUS_SWAPPED; + File.close(); + }catch(const char *str){ + error("FATAL ERROR in SwapSector: Kann Datei \"%s\" nicht anlegen.\n", FileName); + error("# Fehler: %s\n", str); + abort(); + } +} + +static void UnswapSector(uint32 FileNumber){ + char FileName[4096]; + snprintf(FileName, sizeof(FileName), "%s/%010u.swp", SAVEPATH, FileNumber); + + TReadBinaryFile File; + try{ + File.open(FileName); + int SectorX = (int)File.readQuad(); + int SectorY = (int)File.readQuad(); + int SectorZ = (int)File.readQuad(); + print(2, "Lagere Sector %d/%d/%d ein...\n", SectorX, SectorY, SectorZ); + + ASSERT(Sector != NULL); + TSector *LoadingSector = *Sector->at(SectorX, SectorY, SectorZ); + if(LoadingSector == NULL){ + error("UnswapSector: Sektor %d/%d/%d existiert nicht.\n", SectorX, SectorY, SectorZ); + File.close(); + return; + } + + if(LoadingSector->Status != STATUS_SWAPPED){ + error("UnswapSector: Sektor %d/%d/%d ist nicht ausgelagert.\n", SectorX, SectorY, SectorZ); + File.close(); + return; + } + + int Size = File.getSize(); + while(File.getPosition() < Size){ + TObject Entry; + File.readBytes((uint8*)&Entry, sizeof(TObject)); + + uint32 EntryIndex = Entry.ObjectID & HashTableMask; + if(HashTableType[EntryIndex] == STATUS_SWAPPED){ + // NOTE(fusion): Make sure we only allocate the object if we confirm + // its status. The original code would call `readBytes` on the result + // from `GetFreeObjectSlot()` directly and would then leak it if the + // entry status was not `STATUS_SWAPPED`. + TObject *EntryPointer = GetFreeObjectSlot(); + *EntryPointer = Entry; + HashTableData[EntryIndex] = EntryPointer; + HashTableType[EntryIndex] = STATUS_LOADED; + }else{ + error("UnswapSector: Objekt %u existiert schon.\n", Entry.ObjectID); + } + } + LoadingSector->Status = STATUS_LOADED; + File.close(); + unlink(FileName); + }catch(const char *str){ + error("FATAL ERROR in UnswapSector: Kann Datei \"%s\" nicht lesen.\n", FileName); + error("# Fehler: %s\n", str); + abort(); + } +} + +static void DeleteSwappedSectors(void){ + DIR *SwapDir = opendir(SAVEPATH); + if(SwapDir == NULL){ + error("DeleteSwappedSectors: Unterverzeichnis %s nicht gefunden\n", SAVEPATH); + return; + } + + char FilePath[4096]; + while(dirent *DirEntry = readdir(SwapDir)){ + if(DirEntry->d_type == DT_REG){ + // NOTE(fusion): `DirEntry->d_name` will only contain the filename + // so we need to assemble the actual file path (relative in this + // case) to properly unlink it. Windows has the same behavior with + // its `FindFirstFile`/`FindNextFile` API. + const char *FileExt = findLast(DirEntry->d_name, '.'); + if(FileExt != NULL && strcmp(FileExt, ".swp") == 0){ + snprintf(FilePath, sizeof(FilePath), "%s/%s", SAVEPATH, DirEntry->d_name); + unlink(FilePath); + } + } + } + + closedir(SwapDir); +} + +static void LoadObjects(TReadScriptFile *Script, TWriteStream *Stream, bool Skip){ + int Depth = 1; + bool ParseAttribute = false; + Script->readSymbol('{'); + Script->nextToken(); + while(true){ + if(!ParseAttribute){ + if(Script->Token != SPECIAL){ + int TypeID = Script->getNumber(); + if(!ObjectTypeExists(TypeID)){ + Script->error("unknown object type"); + } + + if(!Skip){ + Stream->writeWord((uint16)TypeID); + } + + ParseAttribute = true; + }else{ + char Special = Script->getSpecial(); + if(Special == '}'){ + if(!Skip){ + Stream->writeWord(0xFFFF); + } + + Depth -= 1; + if(Depth <= 0){ + break; + } + }else if(Special != ','){ + Script->error("expected comma"); + } + } + Script->nextToken(); + }else{ + if(Script->Token != SPECIAL){ + int Attribute = GetInstanceAttributeByName(Script->getIdentifier()); + if(Attribute == -1){ + Script->error("unknown attribute"); + } + + Script->readSymbol('='); + if(!Skip){ + Stream->writeByte((uint8)Attribute); + } + + if(Attribute == CONTENT){ + Script->readSymbol('{'); + Depth += 1; + ParseAttribute = false; + }else if(Attribute == TEXTSTRING || Attribute == EDITOR){ + const char *String = Script->readString(); + if(!Skip){ + Stream->writeString(String); + } + }else{ + int Number = Script->readNumber(); + if(!Skip){ + Stream->writeQuad((uint32)Number); + } + } + + Script->nextToken(); + }else{ + // NOTE(fusion): Attributes are key-value pairs separated by whitespace. + // If we find a special token (probably ',' or '}'), then we're done + // parsing attributes for the current object. + if(!Skip){ + Stream->writeByte(0xFF); + } + ParseAttribute = false; + } + } + } +} + +static void LoadObjects(TReadStream *Stream, Object Con){ + int Depth = 1; + Object Obj = NONE; + while(true){ + if(Obj == NONE){ + int TypeID = (int)Stream->readWord(); + if(TypeID != 0xFFFF){ + ObjectCounter += 1; + ObjectType ObjType(TypeID); + + if(ObjType.isBodyContainer()){ + Obj = GetContainerObject(Con, (TypeID - 1)); + }else{ + Obj = AppendObject(Con, ObjType); + } + }else{ + Obj = Con; + Con = Con.getContainer(); + Depth -= 1; + if(Depth <= 0){ + break; + } + } + }else{ + int Attribute = (int)Stream->readByte(); + if(Attribute != 0xFF){ + if(Attribute == CONTENT){ + Con = Obj; + Obj = NONE; + Depth += 1; + }else if(Attribute == TEXTSTRING || Attribute == EDITOR){ + uint32 Value = AddDynamicString(Stream->readString()); + Obj.setAttribute(Attribute, Value); + }else if(Attribute != REMAININGEXPIRETIME){ + uint32 Value = Stream->readQuad(); + Obj.setAttribute(Attribute, Value); + }else{ + uint32 Value = Stream->readQuad(); + if(Value != 0){ + CronChange(Obj, (int)Value); + } + } + }else{ + Obj = NONE; + } + } + } +} + +static void InitSector(int SectorX, int SectorY, int SectorZ){ + ASSERT(Sector); + if(*Sector->at(SectorX, SectorY, SectorZ) != NULL){ + error("InitSector: Sektor %d/%d/%d existiert schon.\n", SectorX, SectorY, SectorZ); + return; + } + + TSector *NewSector = (TSector*)malloc(sizeof(TSector)); + for(int X = 0; X < 32; X += 1){ + for(int Y = 0; Y < 32; Y += 1){ + Object MapCon = CreateObject(); + // NOTE(fusion): `Attributes[0]` is probably the object id of the + // first object in the container. + Access(MapCon)->Attributes[1] = SectorX * 32 + X; + Access(MapCon)->Attributes[2] = SectorY * 32 + Y; + Access(MapCon)->Attributes[3] = SectorZ; + NewSector->MapCon[X][Y] = MapCon; + } + } + NewSector->TimeStamp = RoundNr; + NewSector->Status = STATUS_LOADED; + NewSector->MapFlags = 0; + + *Sector->at(SectorX, SectorY, SectorZ) = NewSector; +} + +static void LoadSector(const char *FileName, int SectorX, int SectorY, int SectorZ){ + if(SectorX < SectorXMin || SectorXMax < SectorX + || SectorY < SectorYMin || SectorYMax < SectorY + || SectorZ < SectorZMin || SectorZMax < SectorZ){ + return; + } + + InitSector(SectorX, SectorY, SectorZ); + + ASSERT(Sector != NULL); + TSector *LoadingSector = *Sector->at(SectorX, SectorY, SectorZ); + ASSERT(LoadingSector != NULL); + + TReadScriptFile Script; + try{ + Script.open(FileName); + print(1, "Lade Sektor %d/%d/%d ...\n", SectorX, SectorY, SectorZ); + + int OffsetX = -1; + int OffsetY = -1; + while(true){ + Script.nextToken(); + if(Script.Token == ENDOFFILE){ + Script.close(); + return; + } + + 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; + } + + if(Script.Token != IDENTIFIER){ + Script.error("next map point expected"); + } + + if(OffsetX == -1 || OffsetY == -1){ + Script.error("coordinate expected"); + } + + const char *Identifier = Script.getIdentifier(); + if(strcmp(Identifier, "refresh") == 0){ + LoadingSector->MapFlags |= 1; + AccessObject(LoadingSector->MapCon[OffsetX][OffsetY])->Attributes[3] |= 0x100; + }else if(strcmp(Identifier, "nologout") == 0){ + LoadingSector->MapFlags |= 2; + AccessObject(LoadingSector->MapCon[OffsetX][OffsetY])->Attributes[3] |= 0x200; + }else if(strcmp(Identifier, "protectionzone") == 0){ + LoadingSector->MapFlags |= 4; + AccessObject(LoadingSector->MapCon[OffsetX][OffsetY])->Attributes[3] |= 0x400; + }else if(strcmp(Identifier, "content") == 0){ + Script.readSymbol('='); + HelpBuffer.reset(); + LoadObjects(&Script, &HelpBuffer, false); + TReadBuffer ReadBuffer(HelpBuffer); + LoadObjects(&ReadBuffer, LoadingSector->MapCon[OffsetX][OffsetY]); + }else{ + Script.error("unknown map flag"); + } + } + }catch(const char *str){ + error("LoadSector: Kann Datei \"%s\" nicht lesen.\n", FileName); + error("# Fehler: %s\n", str); + throw "Cannot load sector"; + } +} + +static void LoadMap(void){ + DIR *MapDir = opendir(MAPPATH); + if(MapDir == NULL){ + error("LoadMap: Unterverzeichnis %s nicht gefunden\n", MAPPATH); + throw "Cannot load map"; + } + + print(1, "Lade Karte ...\n"); + ObjectCounter = 0; + + char FilePath[4096]; + while(dirent *DirEntry = readdir(MapDir)){ + if(DirEntry->d_type == DT_REG){ + // NOTE(fusion): See note in `DeleteSwappedSectors`. + const char *FileExt = findLast(DirEntry->d_name, '.'); + if(FileExt != NULL && strcmp(FileExt, ".sec") == 0){ + int SectorX, SectorY, SectorZ; + if(sscanf("%d-%d-%d.sec", &SectorX, &SectorY, &SectorZ) == 3){ + snprintf(FilePath, sizeof(FilePath), "%s/%s", SAVEPATH, DirEntry->d_name); + LoadSector(FilePath, SectorX, SectorY, SectorZ); + SectorCounter += 1; + } + } + } + } + + closedir(MapDir); + print(1, "%d Sektoren geladen.\n", SectorCounter); + print(1, "%d Objekte geladen.\n", ObjectCounter); +} + +//SaveObjects +//SaveObjects + +void SaveSector(char *FileName, int SectorX, int SectorY, int SectorZ){ + ASSERT(Sector); + TSector *SavingSector = *Sector->at(SectorX, SectorY, SectorZ); + if(!SavingSector){ + return; + } + + bool Empty = true; + TWriteScriptFile Script; + try{ + Script.open(FileName); + print(1, "Speichere Sektor %d/%d/%d ...\n", SectorX, SectorY, SectorZ); + + Script.writeText("# Tibia - graphical Multi-User-Dungeon"); + Script.writeLn(); + Script.writeText("# Data for sector "); + Script.writeNumber(SectorX); + Script.writeText("/"); + Script.writeNumber(SectorY); + Script.writeText("/"); + Script.writeNumber(SectorZ); + Script.writeLn(); + Script.writeLn(); + + for(int X = 0; X < 32; X += 1){ + for(int Y = 0; Y < 32; Y += 1){ + Object First = Object(SavingSector->MapCon[X][Y].getAttribute(CONTENT)); + uint8 Flags = GetMapContainerFlags(SavingSector->MapCon[X][Y]); + if(First != NONE || Flags != 0){ + Script.writeNumber(X); + Script.writeText("-"); + Script.writeNumber(Y); + Script.writeText(": "); + + int AttrCount = 0; + + if(Flags & 1){ + if(AttrCount > 0){ + Script.writeText(", "); + } + Script.writeText("Refresh"); + AttrCount += 1; + } + + if(Flags & 2){ + if(AttrCount > 0){ + Script.writeText(", "); + } + Script.writeText("NoLogout"); + AttrCount += 1; + } + + if(Flags & 4){ + if(AttrCount > 0){ + Script.writeText(", "); + } + Script.writeText("ProtectionZone"); + AttrCount += 1; + } + + if(First != NONE){ + if(AttrCount > 0){ + Script.writeText(", "); + } + Script.writeText("Content="); + HelpBuffer.reset(); + SaveObjects(First, &HelpBuffer, false); + TReadBuffer ReadBuffer(HelpBuffer); + SaveObjects(&ReadBuffer, &Script); + AttrCount += 1; + } + + Script.writeLn(); + Empty = false; + } + } + } + + Script.close(); + if(Empty){ + error("SaveSector: Sektor %d/%d/%d ist leer.\n", SectorX, SectorY, SectorZ); + unlink(FileName); + } + }catch(const char *str){ + error("SaveSector: Kann Datei %s nicht schreiben.\n", FileName); + error("# Fehler: %s\n", str); + } +} + +static void SaveMap(void){ + // NOTE(fusion): I guess this could happen if we're already saving the map + // and a signal causes `exit` to execute cleanup functions registered with + // `atexit`, among which is `ExitAll` which may call `SaveMap` throught + // `ExitMap`. + static bool SavingMap = false; + if(SavingMap){ + error("SaveMap: Karte wird schon gespeichert.\n"); + return; + } + + SavingMap = true; + print(1, "Speichere Karte ...\n"); + ObjectCounter = 0; + + char FileName[4096]; + for(int SectorZ = SectorZMin; SectorZ <= SectorZMax; SectorZ += 1) + for(int SectorY = SectorYMin; SectorY <= SectorYMax; SectorY += 1) + for(int SectorX = SectorXMin; SectorX <= SectorXMax; SectorX += 1){ + snprintf(FileName, sizeof(FileName), "%s/%04d-%04d-%02d.sec", + MAPPATH, SectorX, SectorY, SectorZ); + SaveSector(FileName, SectorX, SectorY, SectorZ); + } + + print(1, "%d Objekte gespeichert.\n", ObjectCounter); + SavingMap = false; +} + +static void ResizeHashTable(void){ + uint32 OldSize = HashTableSize; + uint32 NewSize = OldSize * 2; + ASSERT(ISPOW2(OldSize)); + ASSERT(NewSize > OldSize); + + // TODO(fusion): See note below. + error("FATAL ERROR in ResizeHashTable: Resizing the object hash table is" + " currently disabled. You may increase `Objects` in the map config" + " from %d to %d, to achieve the same effect.", OldSize, NewSize); + abort(); + + error("INFO: HashTabelle zu klein. Größe wird verdoppelt auf %d.\n", NewSize); + + uint32 NewMask = NewSize - 1; + TObject **NewData = (TObject**)malloc(NewSize * sizeof(TObject*)); + uint8 *NewType = (uint8*)malloc(NewSize * sizeof(uint8)); + memset(NewType, 0, NewSize * sizeof(uint8)); + + // TODO(fusion): This rehash loop doesn't make a lot of sense. It wants to + // access all existing objects but doing so would cause all sectors to be + // swapped in at some time or another. This may be bad for performance but + // the real problem is that `UnswapSector` may swap out some other sector + // whose objects were already put into `NewType` and `NewData`, causing + // multiple object ids to reference the same `TObject`. + // Looking at some of the logs included with this executable, it doesn't + // look like this function was ever called which explains why it wasn't fixed + // earlier. + // It would be possible to do this rehashing without swapping anything out, + // if we stored ObjectID somewhere (maybe `HashTableData`). Doubling the size + // of the table means there are two possible indices for each previous entry, + // and we can only determine which one to actually move it with the ObjectID. + + NewType[0] = STATUS_PERMANENT; + NewData[0] = HashTableData[0]; + for(uint32 i = 1; i < NewSize; i += 1){ + if(HashTableType[i] != STATUS_FREE){ + if(HashTableType[i] == STATUS_SWAPPED){ + UnswapSector((uintptr)HashTableData[i]); + } + + if(HashTableType[i] == STATUS_LOADED){ + TObject *Entry = HashTableData[i]; + NewType[Entry->ObjectID & NewMask] = STATUS_LOADED; + NewData[Entry->ObjectID & NewMask] = Entry; + }else{ + error("ResizeHashTable: Fehler beim Reorganisieren der HashTabelle.\n"); + } + } + } + + free(HashTableData); + free(HashTableType); + + HashTableData = NewData; + HashTableType = NewType; + HashTableSize = NewSize; + HashTableMask = NewMask; + HashTableFree += (NewSize - OldSize); +} + +static TObject *GetFreeObjectSlot(void){ + if(FirstFreeObject == NULL){ + SwapSector(); + } + + TObject *Entry = FirstFreeObject; + if(Entry == NULL){ + error("GetFreeObjectSlot: Kein freier Platz mehr.\n"); + return NULL; + } + + // NOTE(fusion): The next object pointer was originally stored in `Entry->NextObject.ObjectID` + // which is a problem when compiling in 64 bits mode. For this reason, I've changed it to be + // stored at the beggining of `TObject`. + FirstFreeObject = *((TObject**)Entry); + + memset(Entry, 0, sizeof(TObject)); + + return Entry; +} + +static void PutFreeObjectSlot(TObject *Entry){ + if(Entry == NULL){ + error("PutFreeObjectSlot: Entry ist NULL.\n"); + return; + } + + // NOTE(fusion): See note in `GetFreeObjectSlot`, just above. + *((TObject**)Entry) = FirstFreeObject; + FirstFreeObject = Entry; +} + +static void ReadMapConfig(void){ + OBCount = 0xA0000; + SectorXMin = 1000; + SectorXMax = 1015; + SectorYMin = 1000; + SectorYMax = 1015; + SectorZMin = 0; + SectorZMax = 15; + RefreshedCylinders = 1; + NewbieStartPositionX = 0; + NewbieStartPositionY = 0; + NewbieStartPositionZ = 0; + VeteranStartPositionX = 0; + VeteranStartPositionY = 0; + VeteranStartPositionZ = 0; + HashTableSize = 0x100000; + Marks = 0; + Depots = 0; + + char FileName[4096]; + snprintf(FileName, sizeof(FileName), "%s/map.dat", DATAPATH); + + TReadScriptFile Script; + Script.open(FileName); + while(true){ + Script.nextToken(); + if(Script.Token == ENDOFFILE){ + Script.close(): + break; + } + + const char *Identifier = Script.readIdentifier(); + Script.readSymbol('='); + + if(strcmp(Identifier, "sectorxmin") == 0){ + SectorXMin = Script.readNumber(); + }else if(strcmp(Identifier, "sectorxmax") == 0){ + SectorXMax = Script.readNumber(); + }else if(strcmp(Identifier, "sectorymin") == 0){ + SectorYMin = Script.readNumber(); + }else if(strcmp(Identifier, "sectorymax") == 0){ + SectorYMax = Script.readNumber(); + }else if(strcmp(Identifier, "sectorzmin") == 0){ + SectorZMin = Script.readNumber(); + }else if(strcmp(Identifier, "sectorzmax") == 0){ + SectorZMax = Script.readNumber(); + }else if(strcmp(Identifier, "refreshedcylinders") == 0){ + RefreshedCylinders = Script.readNumber(); + }else if(strcmp(Identifier, "objects") == 0){ + HashTableSize = (uint32)Script.readNumber(); + }else if(strcmp(Identifier, "cachesize") == 0){ + OBCount = Script.readNumber(); + }else if(strcmp(Identifier, "depot") == 0){ + int DepotIndex = 0; + TDepotInfo TempInfo = {}; + Script.readSymbol('('); + DepotIndex = Script.readNumber(); + Script.readSymbol(','); + const char *Town = Script.readString(); + if(strlen(Town) >= NARRAY(TempInfo.Town)){ + Script.error("town name too long"); + } + strcpy(TempInfo.Town, Town); + Script.readSymbol(','); + TempInfo.Size = Script.readNumber(); + Script.readSymbol(')'); + *DepotInfo.at(DepotIndex) = TempInfo; + }else if(strcmp(Identifier, "mark") == 0){ + TMark TempMark = {}; + Script.readSymbol('('); + const char *Name = Script.readString(); + if(strlen(Name) >= NARRAY(TempMark.Name)){ + Script.error("mark name too long"); + } + strcpy(TempMark.Name, Name); + Script.readSymbol(','); + Script.readCoordinate(&TempMark.x, &TempMark.y, &TempMark.z); + Script.readSymbol(')'); + *Mark.at(Marks) = TempMark; + Marks += 1; + }else if(strcmp(Identifier, "newbiestart") == 0){ + Script.readCoordinate( + &NewbieStartPositionX, + &NewbieStartPositionY, + &NewbieStartPositionZ); + }else if(strcmp(Identifier, "veteranstart") == 0){ + Script.readCoordinate( + &VeteranStartPositionX, + &VeteranStartPositionY, + &VeteranStartPositionZ); + }else{ + // TODO(fusion): + //error("Unknown map configuration key \"%s\"", Identifier); + } + } + + // NOTE(fusion): If each sector is 32 x 32 tiles and the whole world is + // 65535 x 65535 tiles, then the maximum number of sectors is 65535 / 32 + // which is the 2047 used below. We should probably define these contants. + // Notice that sectors at the edge of the XY-plane are also considered + // invalid, probably to add some buffer to avoid wrapping or other types + // of problems. + + if(SectorXMin <= 0){ + throw "illegal value for SectorXMin"; + } + + if(SectorXMax >= 2047){ + throw "illegal value for SectorXMax"; + } + + if(SectorYMin <= 0){ + throw "illegal value for SectorYMin"; + } + + if(SectorYMax >= 2047){ + throw "illegal value for SectorYMax"; + } + + if(SectorZMin < 0){ + throw "illegal value for SectorZMin"; + } + + if(SectorZMax > 15){ + throw "illegal value for SectorZMax"; + } + + if(SectorXMin > SectorXMax){ + throw "SectorXMin is greater than SectorXMax"; + } + + if(SectorYMin > SectorYMax){ + throw "SectorYMin is greater than SectorYMax"; + } + + if(SectorZMin > SectorZMax){ + throw "SectorZMin is greater than SectorZMax"; + } + + // TODO(fusion): Just align up from whatever value we got? And use `ObjectsPerBlock`? + if(OBCount % 32768 != 0){ + throw "CacheSize must be a multiple of 32768"; + } + + if(OBCount <= 0){ + throw "illegal value for CacheSize"; + } + + OBCount /= 32768; + + if(HashTableSize <= 0){ + throw "illegal value for Objects"; + } + + if(!ISPOW2(HashTableSize)){ + throw "Objects must be a power of 2"; + } + + if(NewbieStartPositionX == 0){ + throw "no start position for newbies specified"; + } + + if(VeteranStartPositionX == 0){ + throw "no start position for veterans specified"; + } +} + +void InitMap(void){ + ReadMapConfig(); + + Sector = new matrix3d<TSector*>(SectorXMin, SectorXMax, + SectorYMin, SectorYMax, SectorZMin, SectorZMax, NULL); + + DeleteSwappedSectors(); + + // NOTE(fusion): Object storage is FIXED and determined at startup. + ObjectBlock = (TObjectBlock**)malloc(OBCount * sizeof(TObjectBlock*)); + for(int i = 0; i < OBCount; i += 1){ + ObjectBlock[i] = (TObjectBlock*)malloc(sizeof(TObjectBlock)); + } + + // NOTE(fusion): Setup free object list. See note in `GetFreeObjectSlot`. + constexpr int ObjectsPerBlock = NARRAY(TObjectBlock::Object); + for(int i = 0; i < OBCount; i += 1){ + for(int j = 0; j < ObjectsPerBlock; j += 1){ + *((TObject**)&ObjectBlock[i]->Object[j]) = &ObjectBlock[i]->Object[j + 1]; + } + + if(i < (OBCount - 1)){ + // NOTE(fusion): Link last entry of this block, with the first entry of the next one. + *((TObject**)&ObjectBlock[i]->Object[ObjectsPerBlock - 1]) = &ObjectBlock[i + 1]->Object[0]; + }else{ + // NOTE(fusion): End of free object list. + *((TObject**)&ObjectBlock[i]->Object[ObjectsPerBlock - 1]) = NULL; + } + } + FirstFreeObject = &ObjectBlock[0]->Object[0]; + + // NOTE(fusion): Initialize object hash table. + ASSERT(ISPOW2(HashTableSize)); + HashTableMask = HashTableSize - 1; + HashTableData = (TObject**)malloc(HashTableSize * sizeof(TObject*)); + HashTableType = (uint8*)malloc(HashTableSize * sizeof(uint8)); + memset(HashTableType, 0, HashTableSize * sizeof(uint8)); + HashTableFree = HashTableSize - 1; + // NOTE(fusion): This is probably reserved for `NONE`. + HashTableType[0] = STATUS_PERMANENT; + HashTableData[0] = GetFreeObjectSlot(); + + // NOTE(fusion): Initialize cron hash table (whatever that is). + for(int i = 0; i < NARRAY(CronHashTable); i += 1){ + CronHashTable[i] = -1; + } + CronEntries = 0; + + LoadMap(); +} + +void ExitMap(bool Save){ + if(Save){ + SaveMap(); + } + + free(HashTableData); + free(HashTableType); + + for(int i = 0; i < OBCount; i += 1){ + free(ObjectBlock[i]); + } + free(ObjectBlock); + + if(Sector != NULL){ + for(int SectorZ = SectorZMin; SectorZ <= SectorZMax; SectorZ += 1) + for(int SectorY = SectorYMin; SectorY <= SectorYMax; SectorY += 1) + for(int SectorX = SectorXMin; SectorX <= SectorXMax; SectorX += 1){ + TSector *CurrentSector = *Sector->at(SectorX, SectorY, SectorZ); + if(CurrentSector != NULL){ + free(CurrentSector); + } + } + delete Sector; + } + + DeleteSwappedSectors(); +} + +// TODO(fusion): For some reason this is not a member of `Object`? +static TObject *AccessObject(Object Obj){ + if(Obj == NONE){ + error("AccessObject: Ungültige Objektnummer Null.\n"); + return HashTableData[0]; + } + + uint32 Index = Obj.ObjectID + if(HashTableType[Index] == STATUS_SWAPPED){ + UnswapSector((uintptr)HashTableData[Index]); + } + + if(HashTableType[Index] == STATUS_LOADED && HashTableData[Index]->ObjectID == Obj.ObjectID){ + return HashTableData[Index]; + }else{ + return HashTableData[0]; + } +} + +static Object CreateObject(void){ + static uint32 NextObjectID = 1; + + // NOTE(fusion): Load factor of 1/16. + if(HashTableFree < (HashTableSize / 16)){ + ResizeHashTable(); + } + + // NOTE(fusion): If we properly manage the load factor and the number of free + // entries, we should have no trouble finding an empty table entry here. Use + // a bounded loop nevertheless, just to be safe. + for(uint32 i = 0; i < HashTableSize; i += 1){ + if(HashTableType[NextObjectID & HashTableMask] == 0) + break; + NextObjectID += 1; + } + ASSERT(HashTableType[NextObjectID & HashTableMask] == 0); + + TObject *Entry = GetFreeObjectSlot(); + if(Entry == NULL){ + error("CreateObject: Kann Objekt nicht anlegen.\n"); + return NONE; + } + + Entry->ObjectID = NextObjectID; + HashTableData[NextObjectID & HashTableMask] = Entry; + HashTableType[NextObjectID & HashTableMask] = STATUS_LOADED; + HashTableFree -= 1; + IncrementObjectCounter(); + + return Object(NextObjectID); +} + +// DestroyObject +// DeleteObject + +// Object +// ============================================================================= +bool Object::exists(void){ + if(*this == NONE){ + return false; + } + + uint32 Index = this->ObjectID & HashTableMask; + if(HashTableType[Index] == STATUS_SWAPPED){ + UnswapSector(HashTableData[Index]); + } + + return HashTableType[Index] == STATUS_LOADED + && HashTableData[Index]->ObjectID == this->ObjectID; +} + +ObjectType Object::getObjectType(void){ + return AccessObject(*this)->Type; +} + +void Object::setObjectType(ObjectType Type){ + AccessObject(*this)->Type = Type; +} + +Object Object::getNextObject(void){ + return AccessObject(*this)->NextObject; +} + +void Object::setNextObject(Object NextObject){ + AccessObject(*this)->NextObject = NextObject; +} + +Object Object::getContainer(void){ + return AccessObject(*this)->Container; +} + +void Object::setContainer(Object Container){ + AccessObject(*this)->Container = Container; +} + +uint32 Object::getCreatureID(void){ + // TODO(fusion): We call `AccessObject` once in `getObjectType` then again + // after checking the TypeID, when we could call it once to check both type + // and access `Attributes[1]`. + + if(!this->getObjectType().isCreatureContainer()){ + error("Object::getCreatureID: Objekt ist keine Kreatur.\n"); + return 0; + } + + return AccessObject(*this)->Attributes[1]; +} + +uint32 Object::getAttribute(INSTANCEATTRIBUTE Attribute){ + ObjectType ObjType = this->getObjectType(); + int AttributeOffset = ObjType.getAttributeOffset(Attribute); + if(AttributeOffset == -1){ + error("Object::getAttribute: Flag für Attribut %d bei Objekttyp %d nicht gesetzt.\n", + Attribute, ObjType.TypeID); + return 0; + } + + if(AttributeOffset < 0 || AttributeOffset >= NARRAY(TObject::Attributes)){ + error("Object::getAttribute: Ungültiger Offset %d für Attribut %d bei Objekttyp %d.\n", + AttributeOffset, Attribute, ObjType.TypeID); + return 0; + } + + return AccessObject(*this)->Attributes[AttributeOffset]; +} + +void Object::setAttribute(INSTANCEATTRIBUTE Attribute, uint32 Value){ + ObjectType ObjType = this->getObjectType(); + int AttributeOffset = ObjType.getAttributeOffset(Attribute); + if(AttributeOffset == -1){ + error("Object::setAttribute: Flag für Attribut %d bei Objekttyp %d nicht gesetzt.\n", + Attribute, ObjType.TypeID); + return; + } + + if(AttributeOffset < 0 || AttributeOffset >= NARRAY(TObject::Attributes)){ + error("Object::setAttribute: Ungültiger Offset %d für Attribut %d bei Objekttyp %d.\n", + AttributeOffset, Attribute, ObjType.TypeID); + return; + } + + if(Value == 0){ + if(Attribute == AMOUNT || Attribute == POOLLIQUIDTYPE || Attribute == CHARGES){ + Value = 1; + }else if(Attribute == REMAININGUSES){ + Value = ObjType.getAttribute(TOTALUSES); + } + } + + AccessObject(*this)->Attributes[AttributeOffset] = Value; +} + +// Object Functions +// ============================================================================= +void ChangeObject(Object Obj, ObjectType NewType){ + if(!Obj.exists()){ + error("ChangeObject: Übergebenes Objekt existiert nicht.\n"); + return; + } + + uint32 Amount = 0; + uint32 SavedExpireTime = 0; + int Delay = -1; + + ObjectType OldType = Obj.getObjectType(); + if(!OldType.isMapContainer()){ + if(OldType.getFlag(CUMULATIVE)){ + Amount = Obj.getAttribute(AMOUNT); + } + + if(OldType.getFlag(EXPIRE)){ + SavedExpireTime = CronStop(Obj); + if(NewType.getFlag(CUMULATIVE)){ + Amount = 1; + } + } + + if(OldType.getFlag(TEXT) && !NewType.getFlag(TEXT)){ + DeleteDynamicString(Obj.getAttribute(TEXTSTRING)); + Obj.setAttribute(TEXTSTRING, 0); + + DeleteDynamicString(Obj.getAttribute(EDITOR)); + Obj.setAttribute(EDITOR, 0); + } + + if(OldType.getFlag(EXPIRESTOP)){ + uint32 Value = Obj.getAttribute(SAVEDEXPIRETIME); + if(Value != 0){ + Delay = (int)Value; + } + } + + if(OldType.getFlag(MAGICFIELD)){ + Obj.setAttribute(RESPONSIBLE, 0); + } + } + + Obj.setObjectType(NewType); + + if(NewType.getFlag(CUMULATIVE)){ + if(Amount == 0){ + Amount = 1; + } + Obj.setAttribute(AMOUNT, Amount); + } + + if(NewType.getFlag(RUNE)){ + if(Obj.getAttribute(CHARGES) == 0){ + Obj.setAttribute(CHARGES, 1); + } + } + + if(NewType.getFlag(LIQUIDPOOL)){ + if(Obj.getAttribute(POOLLIQUIDTYPE) == 0){ + Obj.setAttribute(POOLLIQUIDTYPE, 1); // LIQUID_TYPE_NONE (?) + } + } + + if(NewType.getFlag(WEAROUT)){ + if(Obj.getAttribute(REMAININGUSES) == 0){ + Obj.setAttribute(REMAININGUSES, NewType.getAttribute(TOTALUSES)); + } + } + + if(NewType.getFlag(EXPIRESTOP)){ + Obj.setAttribute(SAVEDEXPIRETIME, SavedExpireTime); + } + + CronExpire(Obj, Delay); +} + +int GetObjectPriority(Object Obj){ + if(!Obj.exists()){ + error("GetObjectPriority: Übergebenes Objekt existiert nicht.\n"); + return -1; + } + + int ObjPriority; + ObjectType ObjType = Obj.getObjectType(); + if(ObjType.getFlag(BANK)){ + ObjPriority = PRIORITY_BANK; + }else if(ObjType.getFlag(CLIP)){ + ObjPriority = PRIORITY_CLIP; + }else if(ObjType.getFlag(BOTTOM)){ + ObjPriority = PRIORITY_BOTTOM; + }else if(ObjType.getFlag(TOP)){ + ObjPriority = PRIORITY_TOP; + }else if(ObjType.isCreatureContainer()){ + ObjPriority = PRIORITY_CREATURE; + }else{ + ObjPriority = PRIORITY_OTHER; + } + return ObjPriority; +} + +void PlaceObject(Object Obj, Object Con, bool Append){ + if(!Obj.exists()){ + error("PlaceObject: Übergebenes Objekt existiert nicht.\n"); + return; + } + + if(!Con.exists()){ + error("PlaceObject: Übergebener Container existiert nicht.\n"); + return; + } + + ObjectType ConType = Con.getObjectType(); + if(!ConType.getFlag(CONTAINER) || !ConType.getFlag(CHEST)){ + error("PlaceObject: Con (%d) ist kein Container.\n", ConType.TypeID); + return; + } + + Object Prev = NONE; + Object Cur(Con.getAttribute(CONTENT)); + if(ConType.isMapContainer()){ + // TODO(fusion): Review. The loop below was a bit rough but it seems that + // append is forced for PRIORITY_CREATURE and PRIORITY_OTHER. We should + // confirm this whenever the server is up and running. It'll show. + int ObjPriority = GetObjectPriority(Obj); + Append = Append + || ObjPriority == PRIORITY_CREATURE + || ObjPriority == PRIORITY_OTHER; + + while(Cur != NONE){ + int CurPriority = GetObjectPriority(Cur); + if(CurPriority == ObjPriority + && (ObjPriority == PRIORITY_BANK + || ObjPriority == PRIORITY_BOTTOM + || ObjPriority == PRIORITY_TOP)){ + // TODO(fusion): Replace item? I think the client might assert + // if there are two of these objects on a single tile. + const char *PriorityString = ""; + if(ObjPriority == PRIORITY_BACK){ + PriorityString == "BANK"; + }else if(ObjPriority == PRIORITY_BOTTOM){ + PriorityString == "BOTTOM"; + }else if(ObjPriority == PRIORITY_TOP){ + PriorityString == "TOP"; + } + + int CoordX, CoordY, CoordZ; + GetObjectCoordinates(Con, CoordX, CoordY, CoordZ); + + ObjectType ObjType = Obj.getObjectType(); + ObjectType CurType = Cur.getObjectType(); + error("PlaceObject: Zwei %s-Objekte (%d und %d) auf Feld [%d,%d,%d].\n", + PriorityString, ObjType.TypeID, CurType.TypeID, CoordX, CoordY, CoordZ); + } + + if(Append && CurPriority > ObjPriority) break; + if(!Append && CurPriority >= ObjPriority) break; + + Prev = Cur; + Cur = Cur.getNextObject(); + } + }else{ + if(Append){ + while(Cur != NONE){ + Prev = Cur; + Cur = Cur.getNextObject(); + } + } + } + + if(Prev != NONE){ + Prev.setNextObject(Obj); + }else{ + Con.setAttribute(CONTENT, Obj.ObjectID); + } + Obj.setNextObject(Cur); + Obj.setContainer(Con); +} + +Object AppendObject(Object Con, ObjectType Type){ + if(!Con.exists()){ + error("AppendObject: Übergebener Container existiert nicht.\n"); + return NONE; + } + + Object Obj = CreateObject(); + ChangeObject(Obj, Type); + PlaceObject(Obj, Con, true); + return Obj; +} + +Object GetFirstContainerObject(Object Con){ + if(Con == NONE){ + error("GetFirstContainerObject: Übergebener Container existiert nicht.\n"); + return NONE; + } + + ObjectType ConType = Con.getObjectType(Con); + if(!ConType.getFlag(CONTAINER) && !ConType.getFlag(CHEST)){ + error("GetFirstContainerObject: Con (%d) ist kein Container.\n", ConType.TypeID); + return NONE; + } + + return Object(Con.getAttribute(CONTENT)); +} + +Object GetContainerObject(Object Con, int Index){ + if(Index < 0){ + error("GetContainerObject: Ungültige laufende Nummer %d.\n", Index); + return NONE; + } + + if(!Con.exists()){ + error("GetContainerObject: Übergebener Container existiert nicht.\n"); + return NONE; + } + + Object Current = GetFirstContainerObject(Con); + while(Current != NONE && Index > 0){ + Current = CurrentObject.getNextObject(); + } + + return Current; +} + +void GetObjectCoordinates(Object Obj, int *x, int *y, int *z){ + if(!Obj.exists()){ + error("GetObjectCoordinates: Übergebenes Objekt existiert nicht.\n"); + *x = 0; + *y = 0; + *z = 0; + return; + } + + // TODO(fusion): I'm not sure I like this approach with calling `AccessObject` + // multiple times for the same object. Furthermore, using `Object::getObjectType` + // (at least until now) is very weird when we could just get the same information + // from the `TObject` which we'll access ANYWAYS. + + while(true){ + if(Obj.getObjectType().isMapContainer()) + break; + Obj = Obj.getContainer(); + } + + *x = AccessObject(Obj)->Attributes[1]; + *y = AccessObject(Obj)->Attributes[2]; + + // NOTE(fusion): The first 8 bits of `Attributes[3]` holds the Z coordinate + // of a map container. The next 8 bits holds the flags of a map container. + *z = AccessObject(Obj)->Attributes[3] & 0xFF; + + return; +} + +uint8 GetMapContainerFlags(Object Obj){ + if(!Obj.exists() || !Obj.getObjectType().isMapContainer()){ + error("GetMapContainerFlags: Objekt ist kein MapContainer.\n"); + return 0; + } + + // NOTE(fusion): See note in `GetObjectCoordinates` just above. + return (uint8)(AccessObject(Obj)->Attributes[3] >> 8); +} + +Object GetMapContainer(int x, int y, int z){ + int SectorX = x / 32; + int SectorY = y / 32; + int SectorZ = z; + + if(SectorX < SectorXMin || SectorXMax < SectorX + || SectorY < SectorYMin || SectorYMax < SectorY + || SectorZ < SectorZMin || SectorZMax < SectorZ){ + return NONE; + } + + ASSERT(Sector != NULL); + TSector *ConSector = *Sector->at(SectorX, SectorY, SectorZ); + if(ConSector == NULL){ + return NONE; + } + + + int OffsetX = x % 32; + int OffsetY = y % 32; + ConSector->TimeStamp = RoundNr; + return ConSector->MapCon[OffsetX][OffsetY]; +} + +Object GetMapContainer(Object Obj){ + if(!Obj.exists()){ + error("GetMapContainer: Übergebenes Objekt existiert nicht\n") + return NONE; + } + + while(true){ + if(Obj.getObjectType().isMapContainer()) + break; + Obj = Obj.getContainer(); + } + + return Obj; +} + +Object GetFirstObject(int x, int y, int z){ + Object MapCon = GetMapContainer(x, y, z); + if(MapCon != NONE){ + return Object(MapCon.getAttribute(CONTENT)); + }else{ + return NONE; + } +} |
