From 42be37f4ad99fc8580415fed89939e305d56193e Mon Sep 17 00:00:00 2001 From: fusion32 Date: Tue, 20 May 2025 16:41:03 -0300 Subject: implement `main.cc`, `shm.cc`, and `time.cc` + overall tweeks --- src/config.cc | 6 + src/connection.hh | 56 ++++ src/containers.hh | 338 +++++++++++++++++++++++ src/containers/matrix.hh | 132 --------- src/containers/priority_queue.hh | 94 ------- src/containers/vector.hh | 123 --------- src/creature.cc | 2 +- src/creature.hh | 61 +++++ src/enums.hh | 198 +++++++++++++- src/main.cc | 569 +++++++++++++++++++++++++++++++++++++-- src/main.hh | 58 ++++ src/map.hh | 26 ++ src/monster.hh | 88 ++++++ src/player.hh | 9 +- src/shm.cc | 385 ++++++++++++++++++++++++++ src/skill.cc | 6 +- src/skill.hh | 14 + src/time.cc | 96 +++++++ src/util.cc | 42 +++ 19 files changed, 1923 insertions(+), 380 deletions(-) create mode 100644 src/config.cc create mode 100644 src/connection.hh create mode 100644 src/containers.hh delete mode 100644 src/containers/matrix.hh delete mode 100644 src/containers/priority_queue.hh delete mode 100644 src/containers/vector.hh create mode 100644 src/map.hh create mode 100644 src/monster.hh create mode 100644 src/shm.cc create mode 100644 src/time.cc create mode 100644 src/util.cc (limited to 'src') diff --git a/src/config.cc b/src/config.cc new file mode 100644 index 0000000..36917cb --- /dev/null +++ b/src/config.cc @@ -0,0 +1,6 @@ +#include "main.hh" + +int Beat = 0; +int DebugLevel = 0; + +// diff --git a/src/connection.hh b/src/connection.hh new file mode 100644 index 0000000..724ac91 --- /dev/null +++ b/src/connection.hh @@ -0,0 +1,56 @@ +#ifndef TIBIA_CONNECTION_HH_ +#define TIBIA_CONNECTION_HH_ 1 + +#include "main.hh" +#include "enums.hh" + +struct TConnection; + +struct TKnownCreature { + KNOWNCREATURESTATE State; + uint32 CreatureID; + TKnownCreature *Next; + TConnection *Connection; +}; + +struct TConnection { + uint8 InData[2048]; + int InDataSize; + bool SigIOPending; + bool WaitingForACK; + uint8 OutData[16384]; + uint8 field5_0x4806; + uint8 field6_0x4807; + int NextToSend; + int NextToCommit; + int NextToWrite; + bool Overflow; + bool WillingToSend; + uint8 field12_0x4816; + uint8 field13_0x4817; + TConnection *NextSendingConnection; + uint32 RandomSeed; + CONNECTIONSTATE State; + pid_t PID; + int Socket; + char IPAddress[16]; + // TXTEASymmetricKey SymmetricKey; // TODO + bool ConnectionIsOk; + bool ClosingIsDelayed; + uint8 field23_0x4852; + uint8 field24_0x4853; + uint32 TimeStamp; + uint32 TimeStampAction; + int TerminalType; + int TerminalVersion; + int TerminalOffsetX; + int TerminalOffsetY; + int TerminalWidth; + int TerminalHeight; + uint32 CharacterID; + char Name[31]; + uint8 field35_0x4897; + TKnownCreature KnownCreatureTable[150]; +}; + +#endif // TIBIA_CONNECTION_HH_ diff --git a/src/containers.hh b/src/containers.hh new file mode 100644 index 0000000..f249c81 --- /dev/null +++ b/src/containers.hh @@ -0,0 +1,338 @@ +#ifndef TIBIA_CONTAINERS_HH_ +#define TIBIA_CONTAINERS_HH_ 1 + +#include "main.hh" + +// NOTE(fusion): What the actual fuck. This is an ever growing dynamic array +// with each access through `operator()` growing it to make sure the requested +// index is valid, even for negative indices. +template +struct vector{ + // REGULAR FUNCTIONS + // ========================================================================= + vector(int min, int max, int block){ + int space = (max - min) + 1; + if(space < 1){ + error("vector: Ungueltige Feldgroesse %d bis %d.\n", min, max); + space = 1; + } + + if(block < 0){ + error("vector: Ungueltige Blockgroesse %d.\n", block); + block = 0; + } + + this->min = min; + this->max = max; + this->start = min; + this->space = space; + this->block = block; + this->initialized = false; + this->entry = new T[this->space]; + } + + vector(int min, int max, int block, T init) : vector(min, max, block) { + this->initialized = true; + this->init = init; + for(int i = 0; i < this->space; i += 1){ + this->entry[i] = init; + } + } + + // TODO(fusion): Probably missing some inlined destructor? + + T *at(int index){ + // TODO(fusion): This is probably not the best way to achieve this. + + while(index < this->start){ + int increment = this->block; + if(increment == 0){ + increment = this->space; + } + + T *entry = new T[this->space + increment]; + for(int i = this->min; i <= this->max; i += 1){ + int old_index = i - this->start; + int new_index = old_index + increment; + + // TODO(fusion): Do we actually need to swap elements here? I'm + // assuming some non-trivial structures that would invoke their + // destructors when `this->entry` gets deleted just below. + std::swap(entry[new_index], this->entry[old_index]); + } + + if(this->entry != NULL){ + delete[] this->entry; + } + this->entry = entry; + this->start -= increment; + this->space += increment; + } + + while(index >= (this->start + this->space)){ + int increment = this->block; + if(increment == 0){ + increment = this->space; + } + + T *entry = new[this->space + increment]; + for(int i = this->min; i <= this->max; i += 1){ + int old_index = i - this->start; + int new_index = old_index; + + // TODO(fusion): Same as above. + std::swap(entry[new_index], this->entry[old_index]); + } + + if(this->entry != NULL){ + delete[] this->entry; + } + this->entry = entry; + this->space += increment; + } + + while(index < this->min){ + this->min -= 1; + if(this->initialized){ + this->entry[this->min - this->start] = this->init; + } + } + + while(index > this->max){ + this->max += 1; + if(this->initialized){ + this->entry[this->max - this->start] = this->init; + } + } + + return &this->entry[index - this->start]; + } + + // DATA + // ========================================================================= + int min; + int max; + int start; + int space; + int block; + bool initialized; + T init; + T *entry; +}; + +template +struct priority_queue_entry{ + K Key; + T Data; +}; + +template +struct priority_queue{ + // REGULAR FUNCTIONS + // ========================================================================= + priority_queue(int capacity, int increment){ + Entry = new vector>(1, capacity, increment); + Entries = 0; + } + + // TODO(fusion): Probably missing some inlined destructor? + + void insert(K Key, T *Data){ + this->Entries += 1; + int CurrentIndex = this->Entries; + *this->Entry->at(CurrentIndex) = {Key, *Data}; + while(CurrentIndex > 1){ + int ParentIndex = CurrentIndex / 2; + priority_queue_entry *Current = this->Entry->at(CurrentIndex); + priority_queue_entry *Parent = this->Entry->at(ParentIndex); + if(Parent->Key <= Current->Key) + break; + std::swap(*Current, *Parent); + CurrentIndex = ParentIndex; + } + } + + void deleteMin(void){ + if(this->Entries < 1){ + error("priority_queue::deleteMin: Warteschlange ist leer.\n"); + return; + } + + if(this->Entries > 1){ + int CurrentIndex = 1; + int LastIndex = this->Entries; + std::swap(*this->Entry->at(CurrentIndex), + *this->Entry->at(LastIndex)); + + // TODO(fusion): This may be an oversight but the decompiled version + // checks in the loop below would INCLUDE `LastIndex`, which I assume + // is a bug? The first and last elements were just swapped and what + // was the first element in the queue is now at the end and should be + // considered "removed". + while(1){ + int SmallestIndex = CurrentIndex * 2; + if(SmallestIndex >= LastIndex){ + break; + } + + priority_queue_entry *Smallest = this->Entry->at(SmallestIndex); + if((SmallestIndex + 1) < LastIndex){ + priority_queue_entry *Other = this->Entry->at(SmallestIndex + 1); + if(Other->Key < Smallest->Key){ + Smallest = Other; + SmallestIndex += 1; + } + } + + priority_queue_entry *Current = this->Entry->at(CurrentIndex); + if(Current->Key <= Smallest->Key){ + break; + } + + std::swap(*Current, *Smallest); + CurrentIndex = SmallestIndex; + } + } + + this->Entries -= 1; + } + + // DATA + // ========================================================================= + vector> *Entry; + int Entries; +}; + +// TODO(fusion): We only use this structure two global queues: +// priority_queue ToDoQueue(5000, 1000); +// priority_queue AttackWaveQueue(100, 100); + +typedef +struct matrix{ + // REGULAR FUNCTIONS + // ========================================================================= + matrix(int xmin, int xmax, int ymin, int ymax){ + int dx = (xmax - xmin) + 1; + int dy = (ymax - ymin) + 1; + + if(dx < 1 || dy < 1){ + error("matrix: Ungueltige Feldgroesse %d..%d, %d..%d.\n", xmin, xmax, ymin, ymax); + + if(dx < 1){ + dx = 1; + } + + if(dy < 1){ + dy = 1; + } + } + + this->xmin = xmin; + this->ymin = ymin; + this->dx = dx; + this->dy = dy; + this->entry = new T[dx * dy]; + } + + matrix(int xmin, int xmax, int ymin, int ymax, T init) : matrix(xmin, xmax, ymin, ymax) { + int count = this->dx * this->dy; + for(int i = 0; i < count; i += 1){ + this->entry[i] = init; + } + } + + // TODO(fusion): Probably missing some inlined destructor? + + T *at(int x, int y){ + int xoffset = x - this->xmin; + int yoffset = y - this->ymin; + if(xoffset < 0 || xoffset >= this->dx || yoffset < 0 || yoffset >= this->dy){ + error("matrix::operator(): Ungueltiger Index %d/%d.\n", x, y); + return &this->entry[0]; + }else{ + // TODO(fusion): Are we really storing this in row major order? + return &this->entry[yoffset * this->dx + xoffset]; + } + } + + // DATA + // ========================================================================= + int xmin; + int ymin; + int dx; + int dy; + T *entry; +}; + +typedef +struct matrix3d{ + // REGULAR FUNCTIONS + // ========================================================================= + matrix3d(int xmin, int xmax, int ymin, int ymax, int zmin, int zmax){ + int dx = (xmax - xmin) + 1; + int dy = (ymax - ymin) + 1; + int dz = (zmax - zmin) + 1; + + if(dx < 1 || dy < 1 || dz < 1){ + error("matrix3d: Ungueltige Feldgroesse %d..%d, %d..%d, %d..%d.\n", + xmin, xmax, ymin, ymax, zmin, zmax); + + if(dx < 1){ + dx = 1; + } + + if(dy < 1){ + dy = 1; + } + + if(dz < 1){ + dz = 1; + } + } + + this->xmin = xmin; + this->ymin = ymin; + this->dx = dx; + this->dy = dy; + this->entry = new T[dx * dy]; + } + + matrix3d(int xmin, int xmax, int ymin, int ymax, int zmin, int zmax, T init) + : matrix(xmin, xmax, ymin, ymax, zmin, zmax) { + int count = this->dx * this->dy * this->dz; + for(int i = 0; i < count; i += 1){ + this->entry[i] = init; + } + } + + // TODO(fusion): Probably missing some inlined destructor? + + T *at(int x, int y, int z){ + int xoffset = x - this->xmin; + int yoffset = y - this->ymin; + int zoffset = z - this->zmin; + if(xoffset < 0 || xoffset >= this->dx + || yoffset < 0 || yoffset >= this->dy + || zoffset < 0 || zoffset >= this->dz){ + error("matrix3d::operator(): Ungueltiger Index %d/%d/%d.\n", x, y, z); + return &this->entry[0]; + }else{ + // TODO(fusion): Same as `matrix::at` on the XY plane. + return &this->entry[zoffset * this->dx * this->dy + + yoffset * this->dx + + xoffset]; + } + } + + // DATA + // ========================================================================= + int xmin; + int ymin; + int zmin; + int dx; + int dy; + int dz; + T *entry; +}; + +#endif //TIBIA_CONTAINERS_HH_ \ No newline at end of file diff --git a/src/containers/matrix.hh b/src/containers/matrix.hh deleted file mode 100644 index bf255ff..0000000 --- a/src/containers/matrix.hh +++ /dev/null @@ -1,132 +0,0 @@ -#ifndef TIBIA_MATRIX_HH_ -#define TIBIA_MATRIX_HH_ 1 - -typedef -struct matrix{ - // REGULAR FUNCTIONS - // ========================================================================= - matrix(int xmin, int xmax, int ymin, int ymax){ - int dx = (xmax - xmin) + 1; - int dy = (ymax - ymin) + 1; - - if(dx < 1 || dy < 1){ - error("matrix: Ungueltige Feldgroesse %d..%d, %d..%d.\n", xmin, xmax, ymin, ymax); - - if(dx < 1){ - dx = 1; - } - - if(dy < 1){ - dy = 1; - } - } - - this->xmin = xmin; - this->ymin = ymin; - this->dx = dx; - this->dy = dy; - this->entry = new T[dx * dy]; - } - - matrix(int xmin, int xmax, int ymin, int ymax, T init) : matrix(xmin, xmax, ymin, ymax) { - int count = this->dx * this->dy; - for(int i = 0; i < count; i += 1){ - this->entry[i] = init; - } - } - - // TODO(fusion): Probably missing some inlined destructor? - - T *at(int x, int y){ - int xoffset = x - this->xmin; - int yoffset = y - this->ymin; - if(xoffset < 0 || xoffset >= this->dx || yoffset < 0 || yoffset >= this->dy){ - error("matrix::operator(): Ungueltiger Index %d/%d.\n", x, y); - return &this->entry[0]; - }else{ - // TODO(fusion): Are we really storing this in row major order? - return &this->entry[yoffset * this->dx + xoffset]; - } - } - - // DATA - // ========================================================================= - int xmin; - int ymin; - int dx; - int dy; - T *entry; -}; - -typedef -struct matrix3d{ - // REGULAR FUNCTIONS - // ========================================================================= - matrix3d(int xmin, int xmax, int ymin, int ymax, int zmin, int zmax){ - int dx = (xmax - xmin) + 1; - int dy = (ymax - ymin) + 1; - int dz = (zmax - zmin) + 1; - - if(dx < 1 || dy < 1 || dz < 1){ - error("matrix3d: Ungueltige Feldgroesse %d..%d, %d..%d, %d..%d.\n", - xmin, xmax, ymin, ymax, zmin, zmax); - - if(dx < 1){ - dx = 1; - } - - if(dy < 1){ - dy = 1; - } - - if(dz < 1){ - dz = 1; - } - } - - this->xmin = xmin; - this->ymin = ymin; - this->dx = dx; - this->dy = dy; - this->entry = new T[dx * dy]; - } - - matrix3d(int xmin, int xmax, int ymin, int ymax, int zmin, int zmax, T init) - : matrix(xmin, xmax, ymin, ymax, zmin, zmax) { - int count = this->dx * this->dy * this->dz; - for(int i = 0; i < count; i += 1){ - this->entry[i] = init; - } - } - - // TODO(fusion): Probably missing some inlined destructor? - - T *at(int x, int y, int z){ - int xoffset = x - this->xmin; - int yoffset = y - this->ymin; - int zoffset = z - this->zmin; - if(xoffset < 0 || xoffset >= this->dx - || yoffset < 0 || yoffset >= this->dy - || zoffset < 0 || zoffset >= this->dz){ - error("matrix3d::operator(): Ungueltiger Index %d/%d/%d.\n", x, y, z); - return &this->entry[0]; - }else{ - // TODO(fusion): Same as `matrix::at` on the XY plane. - return &this->entry[zoffset * this->dx * this->dy - + yoffset * this->dx - + xoffset]; - } - } - - // DATA - // ========================================================================= - int xmin; - int ymin; - int zmin; - int dx; - int dy; - int dz; - T *entry; -}; - -#endif //TIBIA_MATRIX_HH_ diff --git a/src/containers/priority_queue.hh b/src/containers/priority_queue.hh deleted file mode 100644 index b48353e..0000000 --- a/src/containers/priority_queue.hh +++ /dev/null @@ -1,94 +0,0 @@ -#ifndef TIBIA_PRIORITY_QUEUE_HH_ -#define TIBIA_PRIORITY_QUEUE_HH_ 1 - -#include "main.hh" -#include "vector.hh" - -template -struct priority_queue_entry{ - K Key; - T Data; -}; - -template -struct priority_queue{ - // REGULAR FUNCTIONS - // ========================================================================= - priority_queue(int capacity, int increment){ - Entry = new vector>(1, capacity, increment); - Entries = 0; - } - - // TODO(fusion): Probably missing some inlined destructor? - - void insert(K Key, T *Data){ - this->Entries += 1; - int CurrentIndex = this->Entries; - *this->Entry->at(CurrentIndex) = {Key, *Data}; - while(CurrentIndex > 1){ - int ParentIndex = CurrentIndex / 2; - priority_queue_entry *Current = this->Entry->at(CurrentIndex); - priority_queue_entry *Parent = this->Entry->at(ParentIndex); - if(Parent->Key <= Current->Key) - break; - std::swap(*Current, *Parent); - CurrentIndex = ParentIndex; - } - } - - void deleteMin(void){ - if(this->Entries < 1){ - error("priority_queue::deleteMin: Warteschlange ist leer.\n"); - return; - } - - if(this->Entries > 1){ - int CurrentIndex = 1; - int LastIndex = this->Entries; - std::swap(*this->Entry->at(CurrentIndex), - *this->Entry->at(LastIndex)); - - // TODO(fusion): This may be an oversight but the decompiled version - // checks in the loop below would INCLUDE `LastIndex`, which I assume - // is a bug? The first and last elements were just swapped and what - // was the first element in the queue is now at the end and should be - // considered "removed". - while(1){ - int SmallestIndex = CurrentIndex * 2; - if(SmallestIndex >= LastIndex){ - break; - } - - priority_queue_entry *Smallest = this->Entry->at(SmallestIndex); - if((SmallestIndex + 1) < LastIndex){ - priority_queue_entry *Other = this->Entry->at(SmallestIndex + 1); - if(Other->Key < Smallest->Key){ - Smallest = Other; - SmallestIndex += 1; - } - } - - priority_queue_entry *Current = this->Entry->at(CurrentIndex); - if(Current->Key <= Smallest->Key){ - break; - } - - std::swap(*Current, *Smallest); - CurrentIndex = SmallestIndex; - } - } - - this->Entries -= 1; - } - - // DATA - // ========================================================================= - vector> *Entry; - int Entries; -}; - -// TODO(fusion): We only use this structure two global queues: -// priority_queue ToDoQueue(5000, 1000); -// priority_queue AttackWaveQueue(100, 100); - -#endif //TIBIA_PRIORITY_QUEUE_HH_ diff --git a/src/containers/vector.hh b/src/containers/vector.hh deleted file mode 100644 index 044ffe1..0000000 --- a/src/containers/vector.hh +++ /dev/null @@ -1,123 +0,0 @@ -#ifndef TIBIA_VECTOR_HH_ -#define TIBIA_VECTOR_HH_ 1 - -#include "main.hh" - -// NOTE(fusion): What the actual fuck. This is an ever growing dynamic array -// with each access through `operator()` growing it to make sure the requested -// index is valid, even for negative indices. -template -struct vector{ - // REGULAR FUNCTIONS - // ========================================================================= - vector(int min, int max, int block){ - int space = (max - min) + 1; - if(space < 1){ - error("vector: Ungueltige Feldgroesse %d bis %d.\n", min, max); - space = 1; - } - - if(block < 0){ - error("vector: Ungueltige Blockgroesse %d.\n", block); - block = 0; - } - - this->min = min; - this->max = max; - this->start = min; - this->space = space; - this->block = block; - this->initialized = false; - this->entry = new T[this->space]; - } - - vector(int min, int max, int block, T init) : vector(min, max, block) { - this->initialized = true; - this->init = init; - for(int i = 0; i < this->space; i += 1){ - this->entry[i] = init; - } - } - - // TODO(fusion): Probably missing some inlined destructor? - - T *at(int index){ - // TODO(fusion): This is probably not the best way to achieve this. - - while(index < this->start){ - int increment = this->block; - if(increment == 0){ - increment = this->space; - } - - T *entry = new T[this->space + increment]; - for(int i = this->min; i <= this->max; i += 1){ - int old_index = i - this->start; - int new_index = old_index + increment; - - // TODO(fusion): Do we actually need to swap elements here? I'm - // assuming some non-trivial structures that would invoke their - // destructors when `this->entry` gets deleted just below. - std::swap(entry[new_index], this->entry[old_index]); - } - - if(this->entry != NULL){ - delete[] this->entry; - } - this->entry = entry; - this->start -= increment; - this->space += increment; - } - - while(index >= (this->start + this->space)){ - int increment = this->block; - if(increment == 0){ - increment = this->space; - } - - T *entry = new[this->space + increment]; - for(int i = this->min; i <= this->max; i += 1){ - int old_index = i - this->start; - int new_index = old_index; - - // TODO(fusion): Same as above. - std::swap(entry[new_index], this->entry[old_index]); - } - - if(this->entry != NULL){ - delete[] this->entry; - } - this->entry = entry; - this->space += increment; - } - - while(index < this->min){ - this->min -= 1; - if(this->initialized){ - this->entry[this->min - this->start] = this->init; - } - } - - while(index > this->max){ - this->max += 1; - if(this->initialized){ - this->entry[this->max - this->start] = this->init; - } - } - - return &this->entry[index - this->start]; - } - - // DATA - // ========================================================================= - int min; - int max; - int start; - int space; - int block; - bool initialized; - T init; - T *entry; -}; - -#endif //TIBIA_VECTOR_HH_ diff --git a/src/creature.cc b/src/creature.cc index f3acdb8..effac42 100644 --- a/src/creature.cc +++ b/src/creature.cc @@ -46,7 +46,7 @@ void CheckMana(TCreature *Creature, int ManaPoints, int SoulPoints, int Delay){ } // NOTE(fusion): Maintain largest exhaust? - int EarliestSpellTime = Delay + ServerMilliseconds; + uint32 EarliestSpellTime = Delay + ServerMilliseconds; if(Creature->EarliestSpellTime < EarliestSpellTime){ Creature->EarliestSpellTime = EarliestSpellTime; } diff --git a/src/creature.hh b/src/creature.hh index 8f0917a..2b19836 100644 --- a/src/creature.hh +++ b/src/creature.hh @@ -2,6 +2,8 @@ #define TIBIA_CREATURE_HH_ 1 #include "main.hh" +#include "connection.hh" +#include "containers.hh" #include "skill.hh" struct TCreature; @@ -13,6 +15,12 @@ struct TCombatEntry{ }; struct TCombat{ + // REGULAR FUNCTIONS + // ========================================================================= + // TODO + + // DATA + // ========================================================================= TCreature *Master; uint32 EarliestAttackTime; uint32 EarliestDefendTime; @@ -36,6 +44,59 @@ struct TCombat{ int LearningPoints; }; +struct TToDoEntry { + ToDoType Code; + union{ + struct{ + uint32 Time; + } Wait; + + struct{ + int x; + int y; + int z; + } Go; + + struct{ + int Direction; + } Rotate; + + struct{ + uint32 Obj; + int x; + int y; + int z; + int Count; + } Move; + + struct{ + uint32 Obj; + uint32 Partner; + } Trade; + + struct{ + uint32 Obj1; + uint32 Obj2; + int Dummy; + } Use; + + struct{ + uint32 Obj; + } Turn; + + struct{ + uint32 Text; // POINTER? Probably a reference from `AddDynamicString`? + int Mode; + uint32 Addressee; + bool CheckSpamming; + } Talk; + + struct{ + int NewState; + } ChangeState; + }; +}; + struct TOutfit{ int OutfitID; union{ diff --git a/src/enums.hh b/src/enums.hh index 053a2ed..feb9775 100644 --- a/src/enums.hh +++ b/src/enums.hh @@ -12,6 +12,99 @@ enum CreatureType: int { NPC = 2, }; +enum CONNECTIONSTATE: int { + CONNECTION_FREE = 0, + CONNECTION_ASSIGNED = 1, + CONNECTION_CONNECTED = 2, + CONNECTION_LOGIN = 3, + CONNECTION_GAME = 4, + CONNECTION_DEAD = 5, + CONNECTION_LOGOUT = 6, + CONNECTION_DISCONNECTED = 7, +}; + +enum FLAG: int { + BANK = 0, + CLIP = 1, + BOTTOM = 2, + TOP = 3, + CONTAINER = 4, + CHEST = 5, + CUMULATIVE = 6, + USEEVENT = 7, + CHANGEUSE = 8, + FORCEUSE = 9, + MULTIUSE = 10, + DISTUSE = 11, + MOVEMENTEVENT = 12, + COLLISIONEVENT = 13, + SEPARATIONEVENT = 14, + KEY = 15, + KEYDOOR = 16, + NAMEDOOR = 17, + LEVELDOOR = 18, + QUESTDOOR = 19, + BED = 20, + FOOD = 21, + RUNE = 22, + INFORMATION = 23, + TEXT = 24, + WRITE = 25, + WRITEONCE = 26, + LIQUIDCONTAINER = 27, + LIQUIDSOURCE = 28, + LIQUIDPOOL = 29, + TELEPORTABSOLUTE = 30, + TELEPORTRELATIVE = 31, + UNPASS = 32, + UNMOVE = 33, + UNTHROW = 34, + UNLAY = 35, + AVOID = 36, + MAGICFIELD = 37, + RESTRICTLEVEL = 38, + RESTRICTPROFESSION = 39, + TAKE = 40, + HANG = 41, + HOOKSOUTH = 42, + HOOKEAST = 43, + ROTATE = 44, + DESTROY = 45, + CLOTHES = 46, + SKILLBOOST = 47, + PROTECTION = 48, + LIGHT = 49, + ROPESPOT = 50, + CORPSE = 51, + EXPIRE = 52, + EXPIRESTOP = 53, + WEAROUT = 54, + WEAPON = 55, + SHIELD = 56, + BOW = 57, + THROW = 58, + WAND = 59, + AMMO = 60, + ARMOR = 61, + HEIGHT = 62, + DISGUISE = 63, + SHOWDETAIL = 64, + SPECIALOBJECT = 65, +}; + +enum GAMESTATE: int { + GAME_STARTING = 0, + GAME_RUNNING = 1, + GAME_CLOSING = 2, + GAME_ENDING = 3, +}; + +enum KNOWNCREATURESTATE: int { + KNOWNCREATURE_FREE = 0, + KNOWNCREATURE_UPTODATE = 1, + KNOWNCREATURE_OUTDATED = 2 +}; + // NOTE(fusion): Not in debug symbols. enum PROFESSION: uint8 { PROFESSION_NONE = 0, @@ -202,7 +295,26 @@ enum Skill: int { SKILL_SOUL = 22, }; -enum TalkMode: int { +enum SpellShapeType: int { + SHAPE_ACTOR = 0, + SHAPE_VICTIM = 1, + SHAPE_ORIGIN = 2, + SHAPE_DESTINATION = 3, + SHAPE_ANGLE = 4, +}; + +enum SpellImpactType: int { + IMPACT_DAMAGE = 0, + IMPACT_FIELD = 1, + IMPACT_HEALING = 2, + IMPACT_SPEED = 3, + IMPACT_DRUNKEN = 4, + IMPACT_STRENGTH = 5, + IMPACT_OUTFIT = 6, + IMPACT_SUMMON = 7, +}; + +enum TALK_MODE: int { TALK_SAY = 1, TALK_WHISPER = 2, TALK_YELL = 3, @@ -228,4 +340,88 @@ enum TalkMode: int { TALK_FAILURE_MESSAGE = 23, }; +enum ToDoType: int { + TDWait = 0, + TDGo = 1, + TDRotate = 2, + TDMove = 3, + TDTrade = 4, + TDUse = 5, + TDTurn = 6, + TDAttack = 7, + TDTalk = 8, + TDChangeState = 9, +}; + +enum TYPEATTRIBUTE: int { + WAYPOINTS = 0, + CAPACITY = 1, + CHANGETARGET = 2, + KEYDOORTARGET = 3, + NAMEDOORTARGET = 4, + LEVELDOORTARGET = 5, + QUESTDOORTARGET = 6, + NUTRITION = 7, + INFORMATIONTYPE = 8, + FONTSIZE = 9, + MAXLENGTH = 10, + MAXLENGTHONCE = 11, + SOURCELIQUIDTYPE = 12, + ABSTELEPORTEFFECT = 13, + RELTELEPORTDISPLACEMENT = 14, + RELTELEPORTEFFECT = 15, + AVOIDDAMAGETYPES = 16, + MINIMUMLEVEL = 17, + PROFESSIONS = 18, + WEIGHT = 19, + ROTATETARGET = 20, + DESTROYTARGET = 21, + BODYPOSITION = 22, + SKILLNUMBER = 23, + SKILLMODIFICATION = 24, + PROTECTIONDAMAGETYPES = 25, + DAMAGEREDUCTION = 26, + BRIGHTNESS = 27, + LIGHTCOLOR = 28, + CORPSETYPE = 29, + TOTALEXPIRETIME = 30, + EXPIRETARGET = 31, + TOTALUSES = 32, + WEAROUTTARGET = 33, + WEAPONTYPE = 34, + WEAPONATTACKVALUE = 35, + WEAPONDEFENDVALUE = 36, + SHIELDDEFENDVALUE = 37, + BOWRANGE = 38, + BOWAMMOTYPE = 39, + THROWRANGE = 40, + THROWATTACKVALUE = 41, + THROWDEFENDVALUE = 42, + THROWMISSILE = 43, + THROWSPECIALEFFECT = 44, + THROWEFFECTSTRENGTH = 45, + THROWFRAGILITY = 46, + WANDRANGE = 47, + WANDMANACONSUMPTION = 48, + WANDATTACKSTRENGTH = 49, + WANDATTACKVARIATION = 50, + WANDDAMAGETYPE = 51, + WANDMISSILE = 52, + AMMOTYPE = 53, + AMMOATTACKVALUE = 54, + AMMOMISSILE = 55, + AMMOSPECIALEFFECT = 56, + AMMOEFFECTSTRENGTH = 57, + ARMORVALUE = 58, + ELEVATION = 59, + DISGUISETARGET = 60, + MEANING = 61, +}; + +enum TWorldType: int { + NORMAL = 0, + NON_PVP = 1, + PVP_ENFORCED = 2, +}; + #endif //TIBIA_ENUMS_HH_ diff --git a/src/main.cc b/src/main.cc index e616ce3..e7a5721 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,38 +1,563 @@ #include "main.hh" +#include +#include + +static bool BeADaemon = false; +static bool Reboot = false; +static bool SaveMapOn = false; + +static int SigAlarmCounter = 0; +static int SigUsr1Counter = 0; + +static sighandler_t handler(int signr, sighandler_t sighandler){ + struct sigaction act; + struct sigaction oldact; + + act.sa_handler = sighandler; + + // TODO(fusion): I feel we should probably use `sigfillset` specially for + // signals that share the same handler. + sigemptyset(&act.sa_mask); + + // TODO(fusion): We had this weird logic in the decompiled version of this + // function but it looks like some GLIBC internal thing, since these values + // would mask signals 1020 and 1021 which don't really make sense. + // + //if(signr == SIGALRM){ + // act.sa_mask.__val[0x1f] = 0x20000000; + //}else{ + // act.sa_mask.__val[0x1f] = 0x10000000; + //} + // + + if(sigaction(signr, &act, &oldact) == 0){ + return oldact.sa_handler; + }else{ + return SIG_ERR; + } +} + +static void SigHupHandler(int signr){ + // no-op (?) +} + +static void SigAbortHandler(int signr){ + print(1, "SigAbortHandler: schalte Writer-Thread ab.\n"); + AbortWriter(); +} + +static void DefaultHandler(int signr){ + print(1, "DefaultHandler: Beende Game-Server (SigNr. %d: %s).\n", + signr, sigdescr_np(signr)); + + handler(SIGINT, SIG_IGN); + handler(SIGQUIT, SIG_IGN); + handler(SIGTERM, SIG_IGN); + handler(SIGXCPU, SIG_IGN); + handler(SIGXFSZ, SIG_IGN); + handler(SIGPWR, SIG_IGN); + + SaveMapOn = (signr == SIGQUIT) || (signr == SIGTERM) || (signr == SIGPWR); + if(signr == SIGTERM){ + GetRealTime(&Hour, &Minute); + RebootTime = (Hour * 60 + Minute + 6) % 1440; + CloseGame(); + }else{ + EndGame(); + } + + Reboot = false; +} + +// TODO(fusion): This function was exported in the binary but not referenced anywhere. +static void ErrorHandler(int signr){ + error("ErrorHandler: SigNr. %d: %s\n", signr, sigdescr_np(signr)); + EndGame(); + LogoutAllPlayers(); + exit(1); +} + +static void InitSignalHandler(void){ + int count = 0; + count += (handler(SIGHUP, SigHupHandler) != SIG_ERR); + count += (handler(SIGINT, DefaultHandler) != SIG_ERR); + count += (handler(SIGQUIT, DefaultHandler) != SIG_ERR); + count += (handler(SIGABRT, SigAbortHandler) != SIG_ERR); + count += (handler(SIGUSR1, SIG_IGN) != SIG_ERR); + count += (handler(SIGUSR2, SIG_IGN) != SIG_ERR); + count += (handler(SIGPIPE, SIG_IGN) != SIG_ERR); + count += (handler(SIGALRM, SIG_IGN) != SIG_ERR); + count += (handler(SIGTERM, DefaultHandler) != SIG_ERR); + count += (handler(SIGSTKFLT, SIG_IGN) != SIG_ERR); + count += (handler(SIGCHLD, SIG_IGN) != SIG_ERR); + count += (handler(SIGTSTP, SIG_IGN) != SIG_ERR); + count += (handler(SIGTTIN, SIG_IGN) != SIG_ERR); + count += (handler(SIGTTOU, SIG_IGN) != SIG_ERR); + count += (handler(SIGURG, SIG_IGN) != SIG_ERR); + count += (handler(SIGXCPU, DefaultHandler) != SIG_ERR); + count += (handler(SIGXFSZ, DefaultHandler) != SIG_ERR); + count += (handler(SIGVTALRM, SIG_IGN) != SIG_ERR); + count += (handler(SIGWINCH, SIG_IGN) != SIG_ERR); + count += (handler(SIGPOLL, SIG_IGN) != SIG_ERR); + count += (handler(SIGPWR, DefaultHandler) != SIG_ERR); + print(1, "InitSignalHandler: %d Signalhandler eingerichtet (Soll=%d)\n", count, 0x1c); +} + +static void ExitSignalHandler(void){ + // no-op +} + +static void SigAlarmHandler(int signr){ + SigAlarmCounter += 1; + + struct itimerval new_timer = {}; + strict itimerval old_timer = {}; + new_timer.it_value.tv_usec = Beat * 1000; + setitimer(ITIMER_REAL, &new_timer, &old_timer); +} + +static void InitTime(void){ + SigAlarmCounter = 0; + handler(SIGALRM, SigAlarmHandler); + SigAlarmHandler(SIGALARM); +} + +static void ExitTime(void){ + struct itimerval new_timer = {}; + strict itimerval old_timer = {}; + setitimer(ITIMER_REAL, &new_timer, &old_timer); + handler(SIGALRM, SIG_IGN); +} + +static void UnlockGame(void){ + // TODO(fusion): Probably use snprintf to format file name? + char FileName[4096]; + strcpy(FileName, SAVEPATH); + strcat(FileName, "/game.pid"); + + std::ifstream InputFile(Filename, std::ios_base::in); + if(!InputFile.fail()){ + int Pid; + InputFile >> Pid; + + if(Pid == getpid()){ + unlink(FileName); + } + } +} + +static void LockGame(void){ + // TODO(fusion): Probably use snprintf to format file name? + char FileName[4096]; + strcpy(FileName, SAVEPATH); + strcat(FileName, "/game.pid"); + + { + std::ifstream InputFile(Filename, std::ios_base::in); + if(!InputFile.fail()){ + int Pid; + InputFile >> Pid; + if(Pid != 0){ + throw "Game-Server is already running, PID file exists."; + } + } + } + + { + std::ofstream OutputFile(FileName, std::ios_base::out | std::ios_base::trunc); + OutputFile << (int)getpid(); + } + + atexit(UnlockGame); +} + +void LoadWorldConfig(void){ #if 0 -// TODO(fusion): All globals should probably be packed into a `GlobalState` -// structure to minimize collisions and bugs. -struct GlobalState{ - // world state - // creatures - // items - // etc... -}; - -GlobalState G = {}; + // TODO(fusion): Whenever we implement query/database stuff. + TQueryManagerConnection Connection(0x4000); + if(Connection.WriteBuffer.Position < 0){ + error("LoadWorldConfig: Kann nicht zum Query-Manager verbinden.\n"); + throw "cannot connect to querymanager"; + } + + int HelpGameAddress[4]; + int Ret = Connection.loadWorldConfig(&WorldType, &RebootTime, HelpGameAddress, + &Port, &MaxPlayers, &PremiumPlayerBuffer, &MaxNewbies, &PremiumNewbieBuffer); + if(Ret != 0){ // TODO(fusion): Maybe `Ret != QUERY_OK` or something? + error("LoadWorldConfig: Kann Konfigurationsdaten nicht holen.\n"); + throw "cannot load world config"; + } + + // NOTE(fusion): Ugh... + snprintf(GameAddress, sizeof(GameAddress), "%d.%d.%d.%d", + HelpGameAddress[0], HelpGameAddress[1], + HelpGameAddress[2], HelpGameAddress[3]); #endif -// TODO(fusion): Probably current server time. Used for delays, timers, etc. -int ServerMilliseconds = 0; + WorldType = NORMAL; + RebootTime = 6 * 60; // minutes + strcpy(GameAddress, "127.0.0.1"); // I KNOW + Port = 7171; + MaxPlayers = 1000; + PremiumPlayerBuffer = 100; + MaxNewbies = 200; + PremiumNewbieBuffer = 50; +} -void (*ErrorFunction)(const char*) = NULL; +static void InitAll(void){ + try{ + //ReadConfig(); + //SetQueryManagerLoginData(1, WorldName); + LoadWorldConfig(); + InitSHM(!BeADaemon); + LockGame(); + //InitLog("game"); + srand(time(NULL)); + InitSignalHandler(); + //InitConnections(); + //InitCommunication(); + //InitStrings(); + //InitWriter(); + //InitReader(); + //InitObjects(); + //InitMap(); + //InitInfo(); + //InitMoveUse(); + //InitMagic(); + //InitCr(); + //InitHouses(); + InitTime(); + //ApplyPatches(); + }catch(const char *str){ + error("Initialisierungsfehler: %s\n", str); + exit(EXIT_FAILURE); + } +} -void error(char *Text, ...){ - char s[1024]; +static void ExitAll(void){ + // TODO(fusion): Probably missing some inlined empty `Exit*` functions here. - va_list ap; - va_start(ap, Text); - vsnprintf(s, sizeof(s) - 1, Text, ap); - va_end(ap); + EndGame(); + ExitTime(); + //ExitCr(); + //ExitMagic(); + //ExitMoveUse(); + //ExitInfo(); + //ExitHouses(); + //ExitMap(SaveMapOn); + //ExitObjects(); + //ExitReader(); + //ExitWriter(); + //ExitStrings(); + //ExitCommunication(); + //ExitConnections(); + ExitSignalHandler(); + ExitSHM(); +} - if(ErrorFunction){ - ErrorFunction(s); +static void ProcessCommand(void){ + int Command = GetCommand(); + if(Command != 0){ + char *Buffer = GetCommandBuffer(); + if(Command == 1){ + if(Buffer != NULL){ + BroadcastMessage(TALK_ADMIN_MESSAGE, "%s", Buffer); + }else{ + error("ProcessCommand: Text für Broadcast ist NULL.\n"); + } + }else{ + error("ProcessCommand: Unbekanntes Kommando %d.\n", Command); + } + + SetCommand(0, NULL); + } +} + +static void AdvanceGame(int Delay){ + static int CreatureTimeCounter = 0; + static int CronTimeCounter = 0; + static int SkillTimeCounter = 0; + static int OtherTimeCounter = 0; + static int OldAmbiente = -1; + static uint32 NextMinute = 30; + static bool Lag = false; + + CreatureTimeCounter += Delay; + CronTimeCounter += Delay; + SkillTimeCounter += Delay; + OtherTimeCounter += Delay; + + if(CreatureTimeCounter >= 1750){ + CreatureTimeCounter -= 1000; + ProcessCreature(); + } + + if(CronTimeCounter >= 1500){ + CronTimeCounter -= 1000; + ProcessCronSystem(); + } + + if(SkillTimeCounter >= 1250){ + SkillTimeCounter -= 1000; + ProcessSkills(); + } + + if(OtherTimeCounter >= 1000){ + OtherTimeCounter -= 1000; + + RoundNr += 1; + SetRoundNr(RoundNr); + + ProcessConnections(); + ProcessMonsterhomes(); + ProcessMonsterRaids(); + ProcessCommunicationControl(); + ProcessReaderThreadReplies(RefreshSector,SendMails); + ProcessWriterThreadReplies(); + ProcessCommand(); + + // TODO(fusion): Shouldn't we be checking both brightness and color? + int Brightness, Color; + GetAmbiente(&Brightness, &Color); + if(OldAmbiente != Brightness){ + OldAmbiente = Brightness; + TConnection *Connection = GetFirstConnection(); + while(Connection != NULL){ + // TODO(fusion): This is probably an inlined function that checks + // whether the connection is still going. The exact decompiled condition + // was `Connection->State - CONDITION_LOGIN < 4` but I think that's + // just a compiler optimization that wouldn't work properly on the + // decompiled version. That comparison in the disassembly is unsigned + // (`JBE`) but the enum is signed which would probably generate an + // invalid signed comparison (`JLE`). + // + // MOV EAX, dword ptr [Connection + Connection->State] + // SUB EAX, 0x3 + // CMP EAX, 0x3 + // JBE ... -> SendAmbiente(Connection) + // + if(Connection->State == CONNECTION_LOGIN + || Connection->State == CONNECTION_GAME + || Connection->State == CONNECTION_DEAD + || Connection->State == CONNECTION_LOGOUT){ + SendAmbiente(Connection); + } + Connection = GetNextConnection(); + } + } + + if(RoundNr % 10 == 0){ + NetLoadCheck(); + } + + if(RoundNr >= NextMinute){ + int Hour, Minute; + GetRealTime(&Hour, &Minute); + + RefreshCylinders(); + if(Minute % 5 == 0){ + CreatePlayerList(true); + } + if(Minute % 15 == 0){ + SavePlayerDataOrder(); + } + if(Minute == 0){ + NetLoadSummary(); + } + if(Minute == 55){ + WriteKillStatistics(); + } + + int RealTime = Minute + Hour * 60; + if((RealTime + 5) % 1440 == RebootTime){ + if(Reboot){ + BroadcastMessage(TALK_ADMIN_MESSAGE, + "Server is saving game in 5 minutes.\nPlease come back in 10 minutes."); + }else{ + BroadcastMessage(TALK_ADMIN_MESSAGE, + "Server is going down in 5 minutes.\nPlease log out."); + } + CloseGame(); + }else if((RealTime + 3) % 1440 == RebootTime){ + if(Reboot){ + BroadcastMessage(TALK_ADMIN_MESSAGE, + "Server is saving game in 3 minutes.\nPlease come back in 10 minutes."); + }else{ + BroadcastMessage(TALK_ADMIN_MESSAGE, + "Server is going down in 3 minutes.\nPlease log out."); + } + }else if((RealTime + 1) % 1440 == RebootTime){ + if(Reboot){ + BroadcastMessage(TALK_ADMIN_MESSAGE, + "Server is saving game in one minute.\nPlease log out."); + }else{ + BroadcastMessage(TALK_ADMIN_MESSAGE, + "Server is going down in one minute.\nPlease log out."); + } + }else if(RealTime == RebootTime){ + CloseGame(); + LogoutAllPlayers(); + SendAll(); + if(Reboot){ + RefreshMap(); + } + SaveMap(); + SaveMapOn = false; + EndGame(); + } + + NextMinute = GetRoundForNextMinute(); + } + CleanupDynamicStrings(); + } + + if(Delay > Beat){ + Log("lag", "Verzögerung %d msec.\n", Delay); + } + + // TODO(fusion): Why would we delay creature movement yet another beat? + if(Delay < 1000){ + MoveCreatures(Delay); + Lag = false; }else{ - printf("%s", s); + if(!Lag && RoundNr > 10){ + error("AdvanceGame: Keine Kreaturbewegung wegen Lag (Verzögerung: %d msec).\n", Delay); + } + Lag = true; } + + SendAll(); +} + +static void SigUsr1Handler(int signr){ + SigUsr1Counter += 1; +} + +static void LaunchGame(void){ + SigUsr1Counter = 0; + handler(SIGUSR1, SigUsr1Handler); + StartGame(); + + print(1, "LaunchGame: Game-Server ist bereit (Pid=%d).\n", getpid()); + + // IMPORTANT(fusion): The whole design of the server is to run across a few + // different processes and communicate via shared memory and signals. Each + // of these processes are single threaded, meaning that signal handlers are + // executed concurrently on the SAME thread. You'd still require to synchronize + // access to large structures to avoid race conditions BUT accessing a few + // independent integers and booleans like we're doing with `SigAlarmCounter`, + // `SigUsr1Counter`, and `SaveMapOn` is perfectly safe. + + SaveMapOn = true; + SigAlarmCounter = 0; + while(GameRunning()){ + // TODO(fusion): `sigblock` and `sigpause` are deprecated in favour of + // `sigprocmask` and `sigsuspend`. + sigblock(sigmask(SIGUSR1)); + while(SigUsr1Counter == 0 && SigAlarmCounter == 0){ + sigpause(0); + } + + if(SigUsr1Counter > 0){ + SigUsr1Counter = 0; + ReceiveData(); + } + + int NumBeats = SigAlarmCounter; + if(NumBeats > 0){ + SigAlarmCounter = 0; + AdvanceGame(NumBeats * Beat); + } + } + + LogoutAllPlayers(); +} + +static bool DaemonInit(bool NoFork){ + if(!NoFork){ + pid_t Pid = fork(); + if(Pid < 0){ + return true; + } + + if(Pid != 0){ + exit(EXIT_SUCCESS); + } + + setsid(); + } + + umask(0177); + chdir(SAVEPATH); + + int OpenMax = sysconf(_SC_OPEN_MAX); + if(OpenMax < 0){ + OpenMax = 1024; + } + + for(int fd = 0; fd < OpenMax; fd += 1){ + close(fd); + } + + return false; } int main(int argc, char **argv){ + bool NoFork = false; + BeADaemon = false; + Reboot = true; + + for(int i = 1; i < argc; i += 1){ + if(strcmp(argv[i], "daemon") == 0){ + BeADaemon = true; + }else if(strcmp(argv[i], "nofork") == 0){ + NoFork = true; + } + } + + // TODO(fusion): It doesn't make sense for `DaemonInit` to even return here. + // It either exits the parent or child process, or let it run. + if(BeADaemon && DaemonInit(NoFork)){ + return 2; + } + + puts("Tibia Game-Server\n(c) by CIP Productions, 2003.\n"); + + InitAll(); + atexit(ExitAll); + + // TODO(fusion): The original binary does use exceptions but identifying + // try..catch blocks are not as straightforward as throw statements. I'll + // leave this one at the top level but we should come back to this problem + // once we identify all throw statements and how to roughly handle them. + try{ + LaunchGame(); + }catch(RESULT result){ + error("main: Nicht abgefangene Exception %d.\n", result); + }catch(const char *str){ + error("main: Nicht abgefangene Exception \"%s\".\n", str); + }catch(const std::exception &e){ + error("main: Nicht abgefangene Exception %s.\n", e.what()); + }catch(...){ + error("main: Nicht abgefangene Exception unbekannten Typs.\n"); + } + + if(!Reboot){ + print(1, "Beende Game-Server...\n"); + }else{ + UnlockGame(); + + char FileName[4096]; + snprintf(FileName, sizeof(FileName), "%s/reboot-daily", BINPATH); + if(FileExists(FileName)){ + ExitAll(); + print(1, "Starte Game-Server neu...\n"); + execv(FileName, argv); + }else{ + print(1, "Reboot-Skript existiert nicht.\n"); + } + } + return 0; } diff --git a/src/main.hh b/src/main.hh index 0b0993c..287b59c 100644 --- a/src/main.hh +++ b/src/main.hh @@ -75,6 +75,64 @@ STATIC_ASSERT(sizeof(int) == 4); # define ASSERT(expr) ((void)(expr)) #endif +// TODO(fusion): We should re-implement the multi-process structure used by the +// original code but only for reference. Making it compile on Windows shouldn't +// be too difficult either. Overall this design is outdated and should be reviewed. +// Nevertheless, we should focus on getting it working as intended, on the target +// platform (which is Linux 32-bits) before attempting to refine it. +STATIC_ASSERT(OS_LINUX); +#include +// + +// config.cc +extern int Beat; + +// shm.cc +void StartGame(void); +void CloseGame(void); +void EndGame(void); +bool LoginAllowed(void); +bool GameRunning(void); +bool GameStarting(void); +bool GameEnding(void); +pid_t GetGameThreadPID(void); +int GetPrintlogPosition(void); +char *GetPrintlogLine(int Line); +void IncrementObjectCounter(void); +void DecrementObjectCounter(void); +uint32 GetObjectCounter(void); +void IncrementPlayersOnline(void); +void DecrementPlayersOnline(void); +int GetPlayersOnline(void); +void IncrementNewbiesOnline(void); +void DecrementNewbiesOnline(void); +int GetNewbiesOnline(void); +void SetRoundNr(uint32 RoundNr); +uint32 GetRoundNr(void); +void SetCommand(int Command, char *Text); +int GetCommand(void); +char *GetCommandBuffer(void); +void InitSHM(bool Verbose); +void ExitSHM(void); +void InitSHMExtern(bool Verbose); +void ExitSHMExtern(void); + +// time.cc +extern uint32 RoundNr; +extern uint32 ServerMilliseconds; +void GetRealTime(int *Hour, int *Minute); +void GetTime(int *Hour, int *Minute); +void GetDate(int *Year, int *Cycle, int *Day); +void GetAmbiente(int *Brightness, int *Color); +uint32 GetRoundAtTime(int Hour, int Minute); +uint32 GetRoundForNextMinute(void); + +// util.cc +typedef void TErrorFunction(const char *Text); +typedef void TPrintFunction(int Level, const char *Text); +void SetErrorFunction(TErrorFunction *Function); +void SetPrintFunction(TPrintFunction *Function); void error(char *Text, ...) ATTR_PRINTF(1, 2); +void print(int Level, char *Text, ...) ATTR_PRINTF(1, 2); #endif //TIBIA_MAIN_HH_ diff --git a/src/map.hh b/src/map.hh new file mode 100644 index 0000000..7bef02b --- /dev/null +++ b/src/map.hh @@ -0,0 +1,26 @@ +#ifndef TIBIA_MAP_HH_ +#define TIBIA_MAP_HH_ 1 + +#include "main.hh" + +// TODO(fusion): I'm not sure whether to put these. + +struct Object { + uint32 ObjectID; +}; + +constexpr Object NONE = {}; + +struct ObjectType { + int TypeID; +}; + +struct TObjectType { + char *Name; + char *Description; + uint8 Flags[9]; + uint32 Attributes[62]; + int AttributeOffsets[18]; +}; + +#endif //TIBIA_MAP_HH_ \ No newline at end of file diff --git a/src/monster.hh b/src/monster.hh new file mode 100644 index 0000000..7e18f48 --- /dev/null +++ b/src/monster.hh @@ -0,0 +1,88 @@ +#ifndef TIBIA_MONSTER_HH_ +#define TIBIA_MONSTER_HH_ 1 + +#include "main.hh" +#include "creature.hh" +#include "containers.hh" +#include "enums.hh" + +struct TSkillData { + int Nr; + int Actual; + int Minimum; + int Maximum; + int NextLevel; + int FactorPercent; + int AddLevel; +}; + +struct TItemData { + ObjectType Type; + int Maximum; + int Probability; +}; + +struct TSpellData { + SpellShapeType Shape; + int ShapeParam1; + int ShapeParam2; + int ShapeParam3; + int ShapeParam4; + SpellImpactType Impact; + int ImpactParam1; + int ImpactParam2; + int ImpactParam3; + int ImpactParam4; + int Delay; +}; + +struct TRaceData { + char Name[30]; + char Article[3]; + TOutfit Outfit; + ObjectType MaleCorpse; + ObjectType FemaleCorpse; + BloodType Blood; + int ExperiencePoints; + int FleeThreshold; + int Attack; + int Defend; + int Armor; + int Poison; + int SummonCost; + int LoseTarget; + int Strategy[4]; + bool KickBoxes; + bool KickCreatures; + bool SeeInvisible; + bool Unpushable; + bool DistanceFighting; + bool NoSummon; + bool NoIllusion; + bool NoConvince; + bool NoBurning; + bool NoPoison; + bool NoEnergy; + bool NoHit; + bool NoLifeDrain; + bool NoParalyze; + int Skills; + vector Skill; + int Talks; + vector Talk; // POINTER? Probably a reference from `AddDynamicString`? + int Items; + vector Item; + int Spells; + vector Spell; +}; + +#if 0 +// TODO(fusion): +struct TMonster: TNonplayer { + int Home; + uint32 Master; + uint32 Target; +}; +#endif + +#endif //TIBIA_MONSTER_HH_ diff --git a/src/player.hh b/src/player.hh index b6009a1..0a3f583 100644 --- a/src/player.hh +++ b/src/player.hh @@ -3,6 +3,7 @@ #include "main.hh" #include "creature.hh" +#include "containers.hh" struct TPlayerData { uint32 CharacterID; @@ -43,7 +44,7 @@ struct TPlayerData { int InventorySize; uint8 *Depot[9]; int DepotSize[9]; - ulong AccountID; + uint32 AccountID; int Sex; char Name[30]; uint8 Rights[12]; @@ -76,7 +77,7 @@ struct TPlayer: TCreature { // DATA // ========================================================================= //TCreature super_TCreature; // IMPLICIT - ulong AccountID; + uint32 AccountID; char Guild[31]; char Rank[31]; char Title[31]; @@ -98,10 +99,10 @@ struct TPlayer: TCreature { uint8 SpellList[256]; int QuestValues[500]; Object OpenContainer[16]; - vector AttackedPlayers; + vector AttackedPlayers; int NumberOfAttackedPlayers; bool Aggressor; - vector FormerAttackedPlayers; + vector FormerAttackedPlayers; int NumberOfFormerAttackedPlayers; bool FormerAggressor; uint32 FormerLogoutRound; diff --git a/src/shm.cc b/src/shm.cc new file mode 100644 index 0000000..5cd4a52 --- /dev/null +++ b/src/shm.cc @@ -0,0 +1,385 @@ +#include "main.hh" + +// NOTE(fusion): This looks like an interface to external tools. Looking at the +// `bin` directory this program was in, there are other programs that probably +// use this interface to control certain aspects of the server. My previous +// assumption that each connection was dispatched into its own process may not +// be correct after all. + +struct TSharedMemory { + int Command; + char CommandBuffer[256]; + uint32 RoundNr; + uint32 ObjectCounter; + uint32 Errors; + int PlayersOnline; + int NewbiesOnline; + int PrintBufferPosition; + char PrintBuffer[200][128]; + GAMESTATE GameState; + pid_t GameThreadPID; +}; + +static TSharedMemory *SHM = NULL; +static bool IsGameServer = false; +static bool VerboseOutput = false; + +void StartGame(void){ + if(SHM != NULL){ + if(SHM->GameState == GAME_STARTING){ + SHM->GameState = GAME_RUNNING; + } + }else{ + error("StartGame: SharedMemory existiert nicht.\n"); + } +} + +void CloseGame(void){ + if(SHM != NULL){ + if(SHM->GameState == GAME_RUNNING){ + SHM->GameState = GAME_CLOSING; + } + }else{ + error("CloseGame: SharedMemory existiert nicht.\n"); + } +} + +void EndGame(void){ + if(SHM != NULL){ + SHM->GameState = GAME_ENDING; + }else{ + error("EndGame: SharedMemory existiert nicht.\n"); + } +} + +bool LoginAllowed(void){ + bool Result = false; + if(SHM != NULL){ + Result = SHM->GameState == GAME_RUNNING; + }else{ + error("IsLoginAllowed: SharedMemory existiert nicht.\n"); + } + return Result; +} + +bool GameRunning(void){ + bool Result = false; + if(SHM != NULL){ + Result = SHM->GameState == GAME_STARTING + || SHM->GameState == GAME_RUNNING + || SHM->GameState == GAME_CLOSING; + }else{ + error("GameRunning: SharedMemory existiert nicht.\n"); + } + return Result; +} + +bool GameStarting(void){ + bool Result = false; + if(SHM != NULL){ + Result = SHM->GameState == GAME_STARTING; + }else{ + error("GameStarting: SharedMemory existiert nicht.\n"); + } + return Result; +} + +bool GameEnding(void){ + bool Result = false; + if(SHM != NULL){ + Result = SHM->GameState == GAME_CLOSING + || SHM->GameState == GAME_ENDING; + }else{ + error("GameEnding: SharedMemory existiert nicht.\n"); + } + return Result; +} + +pid_t GetGameThreadPID(void){ + pid_t Pid = 0; + if(SHM != NULL){ + Pid = SHM->GameThreadPID; + } + return Pid; +} + +static void ErrorHandler(char *Text){ + if(VerboseOutput){ + printf("%s", Text); + } + + if(SHM != NULL){ + SHM->Errors += 1; + if(SHM->Errors <= 0x8000){ + Log("error", Text); + if(SHM->Errors == 0x8000){ + Log("error", "Zu viele Fehler. Keine weitere Protokollierung.\n"); + } + } + } +} + +static void PrintHandler(int Level, char *Text){ + static Semaphore LogfileMutex(1); + + if(Level > DebugLevel){ + return; + } + + if(VerboseOutput){ + printf("%s", Text); + } + + if(SHM != NULL){ + // TODO(fusion): Does it even make sense to have this Semaphore here? + // It controls writes to the print buffer but reads outside this scope + // may be partial. But then, it doesn't seem like it is read anywhere + // else. Perhaps some external monitor tool, in which case do we even + // print from multiple threads? + LogfileMutex.down(); + int Line = SHM->PrintBufferPosition; + char *Buffer = SHM->PrintBuffer[Line]; + // TODO(fusion): Ughh... + strncpy(Buffer, Text, sizeof(SHM->PrintBuffer[0]) - 1); + Buffer[sizeof(SHM->PrintBuffer[0]) - 2] = 0; + if(Buffer[0] != 0){ + usize TextLen = strlen(Buffer); + if(Buffer[TextLen - 1] != '\n'){ + strcat(Buffer, "\n"); + } + } + SHM->PrintBufferPosition = (Line + 1) % NARRAY(SHM->PrintBuffer); + LogfileMutex.up(); + } +} + +int GetPrintlogPosition(void){ + int Result = 0; + if(SHM != NULL){ + Result = SHM->PrintBufferPosition; + } + return Result; +} + +char *GetPrintlogLine(int Line){ + char *Result = NULL; + if(SHM != NULL && Line >= 0 && Line < NARRAY(SHM->PrintBuffer)){ + Result = SHM->PrintBuffer[Line]; + } + return Result; +} + +void IncrementObjectCounter(void){ + if(SHM != NULL){ + SHM->ObjectCounter += 1; + } +} + +void DecrementObjectCounter(void){ + if(SHM != NULL){ + SHM->ObjectCounter -= 1; + } +} + +uint32 GetObjectCounter(void){ + uint32 ObjectCounter = 0; + if(SHM != NULL){ + ObjectCounter = SHM->ObjectCounter; + } + return ObjectCounter; +} + +void IncrementPlayersOnline(void){ + if(SHM != NULL){ + SHM->PlayersOnline += 1; + } +} + +void DecrementPlayersOnline(void){ + if(SHM != NULL){ + SHM->PlayersOnline -= 1; + } +} + +int GetPlayersOnline(void){ + int PlayersOnline = 0; + if(SHM != NULL){ + PlayersOnline = SHM->PlayersOnline; + } + return PlayersOnline; +} + +void IncrementNewbiesOnline(void){ + if(SHM != NULL){ + SHM->NewbiesOnline += 1; + } +} + +void DecrementNewbiesOnline(void){ + if(SHM != NULL){ + SHM->NewbiesOnline -= 1; + } +} + +int GetNewbiesOnline(void){ + int NewbiesOnline = 0; + if(SHM != NULL){ + NewbiesOnline = SHM->NewbiesOnline; + } + return NewbiesOnline; +} + +void SetRoundNr(uint32 RoundNr){ + if(SHM != NULL){ + SHM->RoundNr = RoundNr; + } +} + +uint32 GetRoundNr(void){ + uint32 RoundNr = 0; + if(SHM != NULL){ + RoundNr = SHM->RoundNr; + } + return RoundNr; +} + +void SetCommand(int Command, char *Text){ + if(SHM != NULL){ + SHM->Command = Command; + if(Text == NULL){ + SHM->CommandBuffer[0] = 0; + }else{ + strncpy(SHM->CommandBuffer, sizeof(SHM->CommandBuffer), Text); + SHM->CommandBuffer[sizeof(SHM->CommandBuffer) - 1] = 0; + } + } +} + +int GetCommand(void){ + int Command = 0; + if(SHM != NULL){ + Command = SHM->Command; + } + return Command; +} + +char *GetCommandBuffer(void){ + char *Buffer = NULL; + if(SHM != NULL && SHM->Command != 0){ + Buffer = SHM->CommandBuffer; + } + return Buffer; +} + +static bool DeleteSHM(void){ + int SHMID = shmget(SHMKey, 0, 0); + if(SHMID == -1){ + if(VerboseOutput){ + puts("DeleteSHM: SharedMemory existiert nicht."); + } + return true; + } + + if(shmctl(SHMID, IPC_RMID, NULL) == -1){ + if(VerboseOutput){ + // TODO(fusion): Include `errno` in the error message? + puts("DeleteSHM: Kann SharedMemory nicht löschen."); + } + return false; + } + + return true; +} + +static void CreateSHM(void){ + bool Deleted = false; + while(true){ + int SHMID = shmget(SHMKey, sizeof(TSharedMemory), IPC_CREAT | IPC_EXCL | 0777); + if(SHMID != -1){ + return; + } + + if(errno != EEXIST || Deleted){ + if(VerboseOutput){ + printf("CreateSHM: Kann SharedMemory nicht anlegen (Fehler %d).\n", errno); + } + throw "Cannot create SharedMemory"; + } + + if(!DeleteSHM()){ + throw "Cannot delete SharedMemory"; + } + + Deleted = true; + } +} + +static void AttachSHM(void){ + int SHMID = shmget(SHMKey, sizeof(TSharedMemory), 0); + if(SHMID == -1){ + if(VerboseOutput){ + // TODO(fusion): Include `errno` in the error message? + puts("AttachSHM: Kann SharedMemory nicht fassen."); + } + SHM = NULL; + throw "Cannot get SharedMemory"; + } + + SHM = (TSharedMemory*)shmat(SHMID, NULL, 0); + if(SHM == (void*)-1){ + if(VerboseOutput){ + puts("AttachSHM: Kann SharedMemory nicht anbinden."); + } + + SHM = NULL; + throw "Cannot attach SharedMemory"; + } +} + +static void DetachSHM(void){ + if(SHM != NULL){ + if(shmdt(SHM) == -1 && VerboseOutput){ + puts("DetachSHM: Kann SharedMemory nicht löschen."); + } + SHM = NULL; + } +} + +void InitSHM(bool Verbose){ + IsGameServer = true; + VerboseOutput = Verbose; + + CreateSHM(); + AttachSHM(); + SetErrorFunction(ErrorHandler); + SetPrintFunction(PrintHandler); + + // NOTE(fusion): `CreateSHM` ensures a new shared memory segment is created + // and it should be always zero initialized as per the manual. Nevertheless + // the decompiled version was also clearing SHM, probably just in case. + memset(SHM, 0, sizeof(TSharedMemory)); + + strncpy(SHM->PrintBuffer[0], sizeof(SHM->PrintBuffer[0]), + "SHM initialized. System printing is working!\n"); + SHM->PrintBuffer[0][sizeof(SHM->PrintBuffer[0]) - 1] = 0; + + SHM->PrintBufferPosition = 1; + SHM->GameState = GAME_STARTING; + SHM->GameThreadPID = getpid(); +} + +void ExitSHM(void){ + SetErrorFunction(NULL); + SetPrintFunction(NULL); + DetachSHM(); + DeleteSHM(); +} + +void InitSHMExtern(bool Verbose){ + VerboseOutput = Verbose; + AttachSHM(); +} + +void ExitSHMExtern(void){ + DetachSHM(); +} diff --git a/src/skill.cc b/src/skill.cc index 629d935..6ca04ef 100644 --- a/src/skill.cc +++ b/src/skill.cc @@ -796,7 +796,7 @@ void TSkillSoulpoints::Event(int Range){ // TODO(fusion): Shouldn't this be the same as `Master->Skills[SKILL_SOUL]`? // Not sure what's going on here. if(!Master->IsDead){ - Master->Skills[SKILL_SOUL]->Set(Other->Act + 1); + Master->Skills[SKILL_SOUL]->Change(1); } } @@ -1157,7 +1157,7 @@ bool TSkillBase::NewSkill(uint16 SkillNo, TCreature *Creature){ } bool TSkillBase::SetSkills(int Race){ - if(Race < 0 || Race >= 512){ + if(Race < 0 || Race >= NARRAY(RaceData)){ error("TSkillBase::SetSkills: Ungültige Rassennummer %d.\n", Race); return false; } @@ -1237,7 +1237,7 @@ void TSkillBase::DelTimer(uint16 SkNr){ Skill->DelTimer(); int End = this->FirstFreeTimer; - for(int Index = 0; Index < End; i += 1){ + for(int Index = 0; Index < End; Index += 1){ if(Skill == this->TimerList[Index]){ // NOTE(fusion): A little swap and pop action. End -= 1; diff --git a/src/skill.hh b/src/skill.hh index 9b54649..c3beff6 100644 --- a/src/skill.hh +++ b/src/skill.hh @@ -64,6 +64,7 @@ struct TSkill{ }; struct TSkillLevel: TSkill { + TSkillLevel(int SkNr, TCreature *Master) : TSkill(SkNr, Master) {} void Increase(int Amount) override; void Decrease(int Amount) override; int GetExpForLevel(int Level) override; @@ -71,6 +72,7 @@ struct TSkillLevel: TSkill { }; struct TSkillProbe: TSkill { + TSkillProbe(int SkNr, TCreature *Master) : TSkill(SkNr, Master) {} void Increase(int Amount) override; void Decrease(int Amount) override; int GetExpForLevel(int Level) override; @@ -83,47 +85,57 @@ struct TSkillProbe: TSkill { }; struct TSkillAdd: TSkill { + TSkillAdd(int SkNr, TCreature *Master) : TSkill(SkNr, Master) {} void Advance(int Range) override; }; struct TSkillHitpoints: TSkillAdd { + TSkillHitpoints(int SkNr, TCreature *Master) : TSkillAdd(SkNr, Master) {} void Set(int Value) override; }; struct TSkillMana: TSkillAdd { + TSkillMana(int SkNr, TCreature *Master) : TSkillAdd(SkNr, Master) {} void Set(int Value) override; }; struct TSkillGoStrength: TSkillAdd { + TSkillGoStrength(int SkNr, TCreature *Master) : TSkillAdd(SkNr, Master) {} bool SetTimer(int Cycle, int Count, int MaxCount, int AdditionalValue) override; void Event(int Range) override; }; struct TSkillCarryStrength: TSkillAdd { + TSkillCarryStrength(int SkNr, TCreature *Master) : TSkillAdd(SkNr, Master) {} void Set(int Value) override; }; struct TSkillSoulpoints: TSkillAdd { + TSkillSoulpoints(int SkNr, TCreature *Master) : TSkillAdd(SkNr, Master) {} void Set(int Value) override; int TimerValue(void) override; void Event(int Range) override; }; struct TSkillFed: TSkill { + TSkillFed(int SkNr, TCreature *Master) : TSkill(SkNr, Master) {} void Event(int Range) override; }; struct TSkillLight: TSkill { + TSkillLight(int SkNr, TCreature *Master) : TSkill(SkNr, Master) {} bool SetTimer(int Cycle, int Count, int MaxCount, int AdditionalValue) override; void Event(int Range) override; }; struct TSkillIllusion: TSkill { + TSkillIllusion(int SkNr, TCreature *Master) : TSkill(SkNr, Master) {} bool SetTimer(int Cycle, int Count, int MaxCount, int AdditionalValue) override; void Event(int Range) override; }; struct TSkillPoison: TSkill { + TSkillPoison(int SkNr, TCreature *Master) : TSkill(SkNr, Master) {} bool Process(void) override; bool SetTimer(int Cycle, int Count, int MaxCount, int AdditionalValue) override; void Event(int Range) override; @@ -131,10 +143,12 @@ struct TSkillPoison: TSkill { }; struct TSkillBurning: TSkill { + TSkillBurning(int SkNr, TCreature *Master) : TSkill(SkNr, Master) {} void Event(int Range) override; }; struct TSkillEnergy: TSkill { + TSkillEnergy(int SkNr, TCreature *Master) : TSkill(SkNr, Master) {} void Event(int Range) override; }; diff --git a/src/time.cc b/src/time.cc new file mode 100644 index 0000000..89325ba --- /dev/null +++ b/src/time.cc @@ -0,0 +1,96 @@ +#include "main.hh" + +// IMPORTANT(fusion): `RoundNr` is just the number of seconds since startup which +// is why `GetRoundAtTime` and `GetRoundForNextMinute` straight up uses it as +// seconds. It is incremented every 1 second inside `AdvanceGame`. +uint32 RoundNr = 0; + +uint32 ServerMilliseconds = 0; + +// NOTE(fusion): This isn't strictly required because each server process is +// single threaded and don't share static memory, which is what the result of +// the regular `localtime` function uses. +static struct tm GetLocalTimeTM(time_t timer){ + struct tm result; +#if COMPILER_MSVC + localtime_s(&result, &timer); +#else + localtime_r(&timer, &result); +#endif + return result; +} + +void GetRealTime(int *Hour, int *Minute){ + struct tm LocalTime = GetLocalTimeTM(time(NULL)); + *Hour = LocalTime.tm_hour; + *Minute = LocalTime.tm_min; +} + +void GetTime(int *Hour, int *Minute){ + // NOTE(fusion): This maps each real time hour to a game time day. + struct tm LocalTime = GetLocalTimeTM(time(NULL)); + int Time = LocalTime.tm_sec + LocalTime.tm_min * 60; + *Hour = (Time / 150); + *Minute = (Time % 150) * 2 / 5; +} + +void GetDate(int *Year, int *Cycle, int *Day){ + // NOTE(fusion): This maps each real time week to a game time year. + time_t RealTime = time(NULL); + struct tm LocalTime = GetLocalTimeTM(RealTime); + *Year = ((T / 86400) + 4) / 7 + *Cycle = LocalTime.tm_wday; + *Day = LocalTime.tm_hour; +} + +void GetAmbiente(int *Brightness, int *Color){ + int Hour, Minute; + GetTime(&Hour, &Minute); + + int Time = Minute + Hour * 60; + if(Time < 60){ + *Brightness = 0x33; + *Color = 0xD7; + }else if(Time < 120){ + *Brightness = 0x66; + *Color = 0xD7; + }else if(Time < 180){ + *Brightness = 0x99; + *Color = 0xAD; + }else if(Time < 240){ + *Brightness = 0xCC; + *Color = 0xAD; + }else if(Time <= 1200){ + *Brightness = 0xFF; + *Color = 0xD7; + }else if(Time <= 1260){ + *Brightness = 0xCC; + *Color = 0xD0; + }else if(Time <= 1320){ + *Brightness = 0x99; + *Color = 0xD0; + }else if(Time <= 1380){ + *Brightness = 0x66; + *Color = 0xD7; + }else{ + *Brightness = 0x33; + *Color = 0xD7; + } +} + +uint32 GetRoundAtTime(int Hour, int Minute){ + struct tm LocalTime = GetLocalTimeTM(RealTime); + int SecondsToTime = (Hour - LocalTime.tm_hour) * 3600 + + (Minute - LocalTime.tm_min) * 60 + + (0 - LocalTime.tm_sec); + if(SecondsToTime < 0){ + SecondsToTime += 86400; + } + return SecondsToTime + RoundNr; +} + +uint32 GetRoundForNextMinute(void){ + struct tm LocalTime = GetLocalTimeTM(RealTime); + int SecondsToNextMinute = 60 - LocalTime.tm_sec; + return SecondsToNextMinute + RoundNr + 30; +} diff --git a/src/util.cc b/src/util.cc new file mode 100644 index 0000000..f2ebc48 --- /dev/null +++ b/src/util.cc @@ -0,0 +1,42 @@ +#include "util.hh" + +static void (*ErrorFunction)(const char *Text) = NULL; +static void (*PrintFunction)(int Level, const char *Text) = NULL; + +void SetErrorFunction(TErrorFunction *Function){ + ErrorFunction = Function; +} + +void SetPrintFunction(TPrintFunction *Function){ + PrintFunction = Function; +} + +void error(char *Text, ...){ + char s[1024]; + + va_list ap; + va_start(ap, Text); + vsnprintf(s, sizeof(s), Text, ap); + va_end(ap); + + if(ErrorFunction){ + ErrorFunction(s); + }else{ + printf("%s", s); + } +} + +void print(int Level, char *Text, ...){ + char s[1024]; + + va_list ap; + va_start(ap, Text); + vsnprintf(s, sizeof(s), Text, ap); + va_end(ap); + + if(PrintFunction){ + PrintFunction(Level, s); + }else{ + printf("%s", s); + } +} -- cgit v1.2.3