Compare commits
10 Commits
16edba1729
...
386fa9b807
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
386fa9b807 | ||
|
|
a8d26b6ce3 | ||
|
|
e2ba7cd296 | ||
|
|
d1c4dce3be | ||
|
|
54c2169ec9 | ||
|
|
7beaa081de | ||
|
|
a9f70ed1f1 | ||
|
|
f9beda0832 | ||
|
|
995f50a984 | ||
|
|
370842756a |
2
Makefile
2
Makefile
@ -4,7 +4,7 @@ OUTPUTEXE = game
|
||||
|
||||
CC = g++
|
||||
CFLAGS = -m64 -fno-strict-aliasing -pedantic -Wall -Wextra -Wno-deprecated-declarations -Wno-unused-parameter -Wno-format-truncation -std=c++11 -pthread -DOS_LINUX=1 -DARCH_X64=1
|
||||
LFLAGS = -Wl,-t -lcrypto
|
||||
LFLAGS = -Wl,-t -lrt -lcrypto
|
||||
|
||||
DEBUG ?= 0
|
||||
ifneq ($(DEBUG), 0)
|
||||
|
||||
16
README.md
16
README.md
@ -18,10 +18,22 @@ The server uses a few Linux specific features so it will **ONLY** compile on Lin
|
||||
make # build in release mode
|
||||
make DEBUG=1 # build in debug mode
|
||||
make clean # remove `build` directory
|
||||
make clean && make # full rebuild in release mode (recommended)
|
||||
make clean && make DEBUG=1 # full rebuild in debug mode (recommended)
|
||||
make -B # full rebuild in release mode (recommended)
|
||||
make -B DEBUG=1 # full rebuild in debug mode (recommended)
|
||||
```
|
||||
|
||||
### Extra Features (Advanced)
|
||||
There are also some extra features that can be enabled by appending the following definitions to the `CFLAGS` variable in your `Makefile`. They're completely optional but can be useful in specific scenarios.
|
||||
|
||||
- `-DTIBIA772=1`
|
||||
- Modify the protocol to match Tibia 7.72. It's not a 1:1 relation so 7.7 clients won't be able to connect.
|
||||
|
||||
- `-DALLOW_LOCAL_PROXY=1`
|
||||
- Allow **local** connections to send a proxy header. Ideally we'd have a list of trusted proxies to prevent clients from spoofing their addresses. In reality, it is almost always simpler to have a **local** relay. This is also the only way to get a multi-route proxy without having to drastically change the way connections are handled.
|
||||
|
||||
- `-DBIND_ACCEPTOR_TO_GAME_ADDRESS=1`
|
||||
- Make the acceptor socket bind to the interface specified by the game address, rather than all available network interfaces. This is the same address stored in the database and may cause startup issues if no such interface exists, or connection issues if the address is not publicly reachable.
|
||||
|
||||
## Running
|
||||
This repository contains only the source code for the game server. After the first decompilation pass, it was clear the server would need a few supporting services. They're fairly simple but each one will have a separate *README* file with a short description on how to compile and run them.
|
||||
- [Query Manager](https://github.com/fusion32/tibia-querymanager)
|
||||
|
||||
@ -182,6 +182,8 @@ void error(const char *Text, ...) ATTR_PRINTF(1, 2);
|
||||
void print(int Level, const char *Text, ...) ATTR_PRINTF(2, 3);
|
||||
int random(int Min, int Max);
|
||||
bool FileExists(const char *FileName);
|
||||
const char *GetSignalDescription(int SigNr);
|
||||
const char *GetErrorDescription(int SigNr);
|
||||
|
||||
bool isSpace(int c);
|
||||
bool isAlpha(int c);
|
||||
|
||||
@ -652,7 +652,7 @@ bool CallGameThread(TConnection *Connection){
|
||||
Connection->WaitingForACK = true;
|
||||
if(tgkill(GetGameProcessID(), GetGameThreadID(), SIGUSR1) == -1){
|
||||
error("CallGameThread: Can't send SIGUSR1 to thread %d/%d: (%d) %s\n",
|
||||
GetGameProcessID(), GetGameThreadID(), errno, strerrordesc_np(errno));
|
||||
GetGameProcessID(), GetGameThreadID(), errno, GetErrorDescription(errno));
|
||||
SendLoginMessage(Connection, LOGIN_MESSAGE_ERROR,
|
||||
"The server is not online.\nPlease try again later.", -1);
|
||||
return false;
|
||||
@ -1130,7 +1130,12 @@ bool ReceiveCommand(TConnection *Connection){
|
||||
}
|
||||
|
||||
while(!Connection->WaitingForACK){
|
||||
#if ALLOW_LOCAL_PROXY
|
||||
uint8 Help[32];
|
||||
#else
|
||||
uint8 Help[2];
|
||||
#endif
|
||||
|
||||
int BytesRead = ReadFromSocket(Connection, Help, 2);
|
||||
if(BytesRead == 0){
|
||||
// NOTE(fusion): Peer has closed the connection and there was no
|
||||
@ -1153,6 +1158,62 @@ bool ReceiveCommand(TConnection *Connection){
|
||||
return false;
|
||||
}
|
||||
|
||||
#if ALLOW_LOCAL_PROXY
|
||||
if(Connection->State == CONNECTION_CONNECTED){
|
||||
uint8_t *SourceAddr = NULL;
|
||||
if(Help[0] == 0x0D && Help[1] == 0x0A){
|
||||
// NOTE(fusion): HAProxy-V2 header.
|
||||
static const uint8_t HAProxyV2Signature[] = {
|
||||
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51,
|
||||
0x55, 0x49, 0x54, 0x0A,
|
||||
};
|
||||
|
||||
if(ReadFromSocket(Connection, Help + 2, 26) != 26){
|
||||
error("Unable to read proxy header from %s (Socket=%d)\n",
|
||||
Connection->IPAddress, Connection->GetSocket());
|
||||
return false;
|
||||
}
|
||||
|
||||
if(memcmp(Help, HAProxyV2Signature, 12) != 0){
|
||||
error("Invalid proxy header from %s (Socket=%d)\n",
|
||||
Connection->IPAddress, Connection->GetSocket());
|
||||
return false;
|
||||
}
|
||||
|
||||
SourceAddr = Help + 16;
|
||||
}else if(Help[0] == 0xD9 && Help[1] == 0xFF){
|
||||
// NOTE(fusion): Our custom proxy header.
|
||||
if(ReadFromSocket(Connection, Help + 2, 4) != 4){
|
||||
error("Unable to read proxy header from %s (Socket=%d)\n",
|
||||
Connection->IPAddress, Connection->GetSocket());
|
||||
return false;
|
||||
}
|
||||
|
||||
SourceAddr = Help + 2;
|
||||
}
|
||||
|
||||
if(SourceAddr != NULL){
|
||||
// NOTE(fusion): Ideally we'd have a list of trusted proxies to
|
||||
// prevent clients from spoofing their addresses. In reality, it
|
||||
// is almost always simpler to have a localhost "receiver". This
|
||||
// is also the only way to get a multi-route proxy without having
|
||||
// to drastically change the way connections are handled.
|
||||
if(strcmp(Connection->IPAddress, "127.0.0.1") != 0){
|
||||
error("Received proxy header (%02X, %02X) from unknown host %s\n",
|
||||
Help[0], Help[1], Connection->IPAddress);
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOTE(fusion): Update connection IP-Address and resume reading.
|
||||
snprintf(Connection->IPAddress, sizeof(Connection->IPAddress),
|
||||
"%d.%d.%d.%d",
|
||||
(int)SourceAddr[0], (int)SourceAddr[1],
|
||||
(int)SourceAddr[2], (int)SourceAddr[3]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// TODO(fusion): Size is encoded as a little endian uint16. We should
|
||||
// have a few helper functions to assist with buffer reading.
|
||||
int Size = ((uint16)Help[0] | ((uint16)Help[1] << 8));
|
||||
@ -1428,21 +1489,21 @@ bool OpenSocket(void){
|
||||
Linger.l_linger = 0;
|
||||
if(setsockopt(TCPSocket, SOL_SOCKET, SO_LINGER, &Linger, sizeof(Linger)) == -1){
|
||||
error("LaunchServer: Failed to set SO_LINGER=(0, 0): (%d) %s.\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int ReuseAddr = 1;
|
||||
if(setsockopt(TCPSocket, SOL_SOCKET, SO_REUSEADDR, &ReuseAddr, sizeof(ReuseAddr)) == -1){
|
||||
error("LaunchServer: Failed to set SO_REUSEADDR=1: (%d) %s.\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int NoDelay = 1;
|
||||
if(setsockopt(TCPSocket, IPPROTO_TCP, TCP_NODELAY, &NoDelay, sizeof(NoDelay)) == -1){
|
||||
error("LaunchServer: Failed to set TCP_NODELAY=1: (%d) %s.\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1456,7 +1517,7 @@ bool OpenSocket(void){
|
||||
#endif
|
||||
if(bind(TCPSocket, (struct sockaddr*)&ServerAddress, sizeof(ServerAddress)) == -1){
|
||||
error("LaunchServer: Failed to bind to acceptor to %s:%d: (%d) %s.\n",
|
||||
inet_ntoa(ServerAddress.sin_addr), GamePort, errno, strerrordesc_np(errno));
|
||||
inet_ntoa(ServerAddress.sin_addr), GamePort, errno, GetErrorDescription(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1471,9 +1532,13 @@ bool OpenSocket(void){
|
||||
int AcceptorThreadLoop(void *Unused){
|
||||
AcceptorThreadID = gettid();
|
||||
print(1, "Warte auf Clients...\n");
|
||||
while(GameRunning()){
|
||||
while(true){
|
||||
int Socket = accept(TCPSocket, NULL, NULL);
|
||||
if(Socket == -1){
|
||||
if(!GameRunning()){
|
||||
break;
|
||||
}
|
||||
|
||||
error("AcceptorThreadLoop: Fehler %d beim Accept.\n", errno);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ bool TConnection::SetLoginTimer(int Timeout){
|
||||
SigEvent.sigev_notify_thread_id = this->ThreadID;
|
||||
if(timer_create(CLOCK_MONOTONIC, &SigEvent, &this->LoginTimer) == -1){
|
||||
error("TConnection::SetLoginTimer: Failed to create timer: (%d) %s\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -113,7 +113,7 @@ bool TConnection::SetLoginTimer(int Timeout){
|
||||
TimerSpec.it_value.tv_sec = Timeout;
|
||||
if(timer_settime(this->LoginTimer, 0, &TimerSpec, NULL) == -1){
|
||||
error("TConnection::SetLoginTimer: Failed to start timer: (%d) %s\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -133,7 +133,7 @@ void TConnection::StopLoginTimer(void){
|
||||
|
||||
if(timer_delete(this->LoginTimer) == -1){
|
||||
error("TConnection::StopLoginTimer: Failed to delete timer: (%d) %s\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
}
|
||||
|
||||
this->LoginTimer = 0;
|
||||
|
||||
@ -857,6 +857,7 @@ struct TPlayer: TCreature {
|
||||
void SetQuestValue(int QuestNr, int Value);
|
||||
void CheckOutfit(void);
|
||||
void CheckState(void);
|
||||
void SyncState(void);
|
||||
void AddBuddy(const char *Name);
|
||||
void RemoveBuddy(uint32 CharacterID);
|
||||
void SendBuddies(void);
|
||||
|
||||
@ -1016,7 +1016,7 @@ void TCreature::ToDoGo(int DestX, int DestY, int DestZ, bool MustReach, int MaxS
|
||||
|
||||
// NOTE(fusion): The number of steps between two points is the same as the
|
||||
// their manhattan distance, if we exclude diagonal movement. We can skip
|
||||
// the path finder if we know we're step away from the destination.
|
||||
// the path finder if we know we're one step away from the destination.
|
||||
if(DistanceX + DistanceY == 1){
|
||||
TToDoEntry TD = {};
|
||||
TD.Code = TDGo;
|
||||
|
||||
@ -2082,7 +2082,7 @@ void ProcessMonsterRaids(void){
|
||||
|| ItemType.getFlag(WEAROUT)
|
||||
|| ItemType.getFlag(EXPIRE)
|
||||
|| ItemType.getFlag(EXPIRESTOP)){
|
||||
Item = Create(Bag, ItemType, 0);
|
||||
Item = Create(Bag, ItemType, Amount);
|
||||
}else{
|
||||
Item = CreateAtCreature(Creature->ID, ItemType, Amount);
|
||||
}
|
||||
|
||||
@ -1760,10 +1760,18 @@ void TNPC::IdleStimulus(void){
|
||||
case 3: DestY += 1; break;
|
||||
}
|
||||
|
||||
// NOTE(fusion): TNPC::MovePossible in particular doesn't throw
|
||||
// exceptions so we don't need a try-catch block here like in the
|
||||
// case of TMonster::IdleStimulus, nor does the original function
|
||||
// has it. That said, it's still better to be safe.
|
||||
try{
|
||||
if(this->MovePossible(DestX, DestY, DestZ, true, false)){
|
||||
DestFound = true;
|
||||
break;
|
||||
}
|
||||
}catch(RESULT r){
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
if(DestFound){
|
||||
@ -1976,8 +1984,8 @@ TMonster::TMonster(int Race, int x, int y, int z, int Home, uint32 MasterID) :
|
||||
this->starty = y;
|
||||
this->startz = z;
|
||||
this->posx = x;
|
||||
this->posx = y;
|
||||
this->posx = z;
|
||||
this->posy = y;
|
||||
this->posz = z;
|
||||
this->State = IDLE;
|
||||
this->Home = Home;
|
||||
this->Master = MasterID;
|
||||
@ -2050,7 +2058,7 @@ TMonster::TMonster(int Race, int x, int y, int z, int Home, uint32 MasterID) :
|
||||
|| ItemType.getFlag(WEAROUT)
|
||||
|| ItemType.getFlag(EXPIRE)
|
||||
|| ItemType.getFlag(EXPIRESTOP)){
|
||||
Item = Create(Bag, ItemType, 0);
|
||||
Item = Create(Bag, ItemType, Amount);
|
||||
}else{
|
||||
Item = CreateAtCreature(this->ID, ItemType, Amount);
|
||||
}
|
||||
@ -2384,13 +2392,14 @@ void TMonster::IdleStimulus(void){
|
||||
int TalkNr = random(1, RaceData[this->Race].Talks);
|
||||
const char *Text = GetDynamicString(*RaceData[this->Race].Talk.at(TalkNr));
|
||||
if(Text != 0 && Text[0] != 0){
|
||||
// TODO(fusion): We were only checking for a `#` but it could be
|
||||
// problematic for any two character nul terminated string, since
|
||||
// adding 3 would make it point out of bounds. Poor string management
|
||||
// is a recurring theme.
|
||||
// NOTE(fusion): The original would only check for a leading '#' before
|
||||
// advancing 3 characters. This would be a problem for any 2 character
|
||||
// string since we'd end up pointing beyond the null terminator.
|
||||
int Mode = TALK_ANIMAL_LOW;
|
||||
if(Text[0] == '#' && (Text[1] == 'y' || Text[1] == 'Y') && Text[2] == ' '){
|
||||
if(Text[0] == '#' && Text[1] != 0 && Text[2] == ' '){
|
||||
if(Text[1] == 'y' || Text[1] == 'Y'){
|
||||
Mode = TALK_ANIMAL_LOUD;
|
||||
}
|
||||
Text += 3;
|
||||
}
|
||||
|
||||
@ -2450,7 +2459,7 @@ void TMonster::IdleStimulus(void){
|
||||
int DistanceX = std::abs(Target->posx - this->posx);
|
||||
int DistanceY = std::abs(Target->posy - this->posy);
|
||||
if((Target->posz != this->posz || DistanceX > 10 || DistanceY > 10)
|
||||
||(Target->Type == PLAYER && CheckRight(Target->ID, IGNORED_BY_MONSTERS))
|
||||
|| (Target->Type == PLAYER && CheckRight(Target->ID, IGNORED_BY_MONSTERS))
|
||||
|| (Target->IsInvisible() && !RaceData[this->Race].SeeInvisible)
|
||||
|| IsProtectionZone(Target->posx, Target->posy, Target->posz)
|
||||
|| IsHouse(Target->posx, Target->posy, Target->posz)
|
||||
@ -2475,7 +2484,7 @@ void TMonster::IdleStimulus(void){
|
||||
// NOTE(fusion): We're looking for the creature that maximizes
|
||||
// the strategy parameter.
|
||||
int TieBreaker = random(0, 99);
|
||||
if(StrategyParam >= BestStrategyParam
|
||||
if(StrategyParam > BestStrategyParam
|
||||
||(StrategyParam == BestStrategyParam && TieBreaker > BestTieBreaker)){
|
||||
this->Target = TargetID;
|
||||
BestTieBreaker = TieBreaker;
|
||||
@ -2702,7 +2711,7 @@ void TMonster::IdleStimulus(void){
|
||||
// TODO(fusion): We're doing something similar to `TCombat::CanToDoAttack`
|
||||
// with added random steps around the attacking position. This function is
|
||||
// too convoluted and we should definitely split it into smaller ones. I
|
||||
// don't have a problem with large functions but this it too much.
|
||||
// don't have a problem with large functions but this is too much.
|
||||
|
||||
// NOTE(fusion): Max distance.
|
||||
int Distance = std::max<int>(
|
||||
@ -2777,6 +2786,8 @@ void TMonster::IdleStimulus(void){
|
||||
|
||||
if(this->State == ATTACKING || this->State == PANIC){
|
||||
this->Rotate(Target);
|
||||
// TODO(fusion): This condition should be always true because we're
|
||||
// already running on the ELSE branch of `this->Master == this->Target`.
|
||||
if(this->Master != this->Target){
|
||||
this->ToDoAttack();
|
||||
}else{
|
||||
@ -2832,6 +2843,9 @@ void TMonster::IdleStimulus(void){
|
||||
this->ToDoWait(1000);
|
||||
this->ToDoStart();
|
||||
}catch(RESULT r){
|
||||
// TODO(fusion): This shouldn't be possible? Otherwise the creature
|
||||
// might become stuck since we're not calling ToDoStart or ToDoYield.
|
||||
// Probably just fallback to ToDoWait(1000)?
|
||||
error("TMonster::IdleStimulus: Exception %d.\n", r);
|
||||
}
|
||||
}else{
|
||||
@ -2936,7 +2950,7 @@ bool TMonster::KickCreature(TCreature *Creature){
|
||||
|
||||
print(3, "%s verschiebt %s.\n", this->Name, Creature->Name);
|
||||
|
||||
// TODO(fusion): Declare these here so they can be used within the catch
|
||||
// NOTE(fusion): Declare these here so they can be used within the catch
|
||||
// block to print out what's on the last position we tried to move the
|
||||
// creature to.
|
||||
int DestX = Creature->posx;
|
||||
|
||||
@ -205,7 +205,7 @@ TPlayer::TPlayer(TConnection *Connection, uint32 CharacterID):
|
||||
SendAmbiente(Connection);
|
||||
AnnounceChangedCreature(CharacterID, CREATURE_LIGHT_CHANGED);
|
||||
SendPlayerSkills(Connection);
|
||||
this->CheckState();
|
||||
this->SyncState();
|
||||
this->SendBuddies();
|
||||
|
||||
if(PlayerData->LastLoginTime != 0){
|
||||
@ -750,7 +750,6 @@ void TPlayer::TakeOver(TConnection *Connection){
|
||||
strcpy(this->Guild, PlayerData->Guild);
|
||||
strcpy(this->Rank, PlayerData->Rank);
|
||||
strcpy(this->Title, PlayerData->Title);
|
||||
this->OldState = 0;
|
||||
|
||||
this->CheckOutfit();
|
||||
this->ClearRequest();
|
||||
@ -770,7 +769,7 @@ void TPlayer::TakeOver(TConnection *Connection){
|
||||
SendAmbiente(Connection);
|
||||
SendPlayerData(Connection);
|
||||
SendPlayerSkills(Connection);
|
||||
this->CheckState();
|
||||
this->SyncState();
|
||||
this->SendBuddies();
|
||||
}
|
||||
|
||||
@ -1254,6 +1253,16 @@ void TPlayer::CheckState(void){
|
||||
}
|
||||
}
|
||||
|
||||
void TPlayer::SyncState(void){
|
||||
// NOTE(fusion): This is only called after a new connection is established,
|
||||
// when we know that the current client state is ZERO but `OldState` may not.
|
||||
// It'll typically be the case in `TPlayer::TakeOver` but can also happen
|
||||
// in the player constructor if `TPlayer::CheckState` is called from within
|
||||
// any helper function before `TConnection::EnterGame` (see `BeginSendData`).
|
||||
this->OldState = 0;
|
||||
this->CheckState();
|
||||
}
|
||||
|
||||
void TPlayer::AddBuddy(const char *Name){
|
||||
if(Name == NULL){
|
||||
error("TPlayer::AddBuddy: Name ist NULL.\n");
|
||||
@ -2845,7 +2854,7 @@ void AttachPlayerPoolSlot(TPlayerData *Slot, bool DontWait){
|
||||
}
|
||||
|
||||
PlayerDataPoolMutex.up();
|
||||
DelayThread(0,100);
|
||||
DelayThread(0, 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -107,9 +107,6 @@ void TSkill::Load(int Act, int Max, int Min, int DAct, int MDAct,
|
||||
|
||||
TCreature *Master = this->Master;
|
||||
if(Master && Cycle != 0){
|
||||
// NOTE(fusion): It seems we had `TSkillBase::SetTimer` inlined here.
|
||||
// For whatever reason I hadn't noticed the error message referencing
|
||||
// it, LOL.
|
||||
Master->SetTimer(this->SkNr, Cycle, Count, MaxCount, FactorPercent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -798,8 +798,8 @@ static Object CreateTempDepot(void){
|
||||
return TempDepot;
|
||||
}
|
||||
|
||||
// NOTE(fusion): This is used inside house processing functions to empty the depot
|
||||
// container after processing a player's depot.
|
||||
// NOTE(fusion): This is used by house processing functions to clear the temporary
|
||||
// depot container after processing player depots.
|
||||
static void DeleteContainerObjects(Object Con){
|
||||
Object Obj = GetFirstContainerObject(Con);
|
||||
while(Obj != NONE){
|
||||
|
||||
24
src/info.cc
24
src/info.cc
@ -768,19 +768,14 @@ bool SearchFreeField(int *x, int *y, int *z, int Distance, uint16 HouseID, bool
|
||||
int FieldY = *y + OffsetY;
|
||||
int FieldZ = *z;
|
||||
|
||||
// TODO(fusion): This is probably some form of the `TCreature::MovePossible`
|
||||
// function inlined.
|
||||
bool MovePossible;
|
||||
bool MovePossible = false;
|
||||
if(Jump){
|
||||
MovePossible = JumpPossible(FieldX, FieldY, FieldZ, true);
|
||||
}else{
|
||||
MovePossible = CoordinateFlag(FieldX, FieldY, FieldZ, BANK)
|
||||
&& !CoordinateFlag(FieldX, FieldY, FieldZ, UNPASS);
|
||||
|
||||
// TODO(fusion): This one I'm not so sure.
|
||||
if(MovePossible && CoordinateFlag(FieldX, FieldY, FieldZ, AVOID)){
|
||||
MovePossible = CoordinateFlag(FieldX, FieldY, FieldZ, BED);
|
||||
}
|
||||
&& !CoordinateFlag(FieldX, FieldY, FieldZ, UNPASS)
|
||||
&& (!CoordinateFlag(FieldX, FieldY, FieldZ, AVOID)
|
||||
|| CoordinateFlag(FieldX, FieldY, FieldZ, BED));
|
||||
}
|
||||
|
||||
if(MovePossible){
|
||||
@ -792,11 +787,9 @@ bool SearchFreeField(int *x, int *y, int *z, int Distance, uint16 HouseID, bool
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// NOTE(fusion): We're spiraling out from the initial coordinate.
|
||||
// TODO(fusion): This function used directions different from the ones
|
||||
// used by creatures and defined in `enums.hh` so I made it use them
|
||||
// instead, LOL.
|
||||
// NOTE(fusion): We're spiraling out from the initial coordinate. The
|
||||
// original function used direction values different from the ones used
|
||||
// by creatures and defined in `enums.hh` so I changed it for consistency.
|
||||
if(CurrentDirection == DIRECTION_NORTH){
|
||||
OffsetY -= 1;
|
||||
if(OffsetY <= -CurrentDistance){
|
||||
@ -979,9 +972,8 @@ bool SearchSpawnField(int *x, int *y, int *z, int Distance, bool Player){
|
||||
if(ObjType.getFlag(UNMOVE)){
|
||||
ExpansionPossible = false;
|
||||
LoginPossible = LoginPossible && !Player;
|
||||
}else{
|
||||
LoginBad = true;
|
||||
}
|
||||
LoginBad = true;
|
||||
}
|
||||
|
||||
Obj = Obj.getNextObject();
|
||||
|
||||
@ -4119,7 +4119,7 @@ void UseMagicItem(uint32 CreatureID, Object Obj, Object Dest){
|
||||
throw NOCREATURE;
|
||||
}
|
||||
|
||||
int Amount = ComputeDamage(Actor, SpellNr, 250, 30);
|
||||
int Amount = ComputeDamage(Actor, SpellNr, 250, 0);
|
||||
Heal(Target, -1, 0, Amount); // -1 ?
|
||||
break;
|
||||
}
|
||||
|
||||
34
src/main.cc
34
src/main.cc
@ -53,7 +53,8 @@ static void SigBlock(int SigNr){
|
||||
sigaddset(&Set, SigNr);
|
||||
if(sigprocmask(SIG_BLOCK, &Set, NULL) == -1){
|
||||
error("SigBlock: Failed to block signal %d (%s): (%d) %s\n",
|
||||
SigNr, sigdescr_np(SigNr), errno, strerrordesc_np(errno));
|
||||
SigNr, GetSignalDescription(SigNr),
|
||||
errno, GetErrorDescription(errno));
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,18 +64,18 @@ static void SigWaitAny(void){
|
||||
sigsuspend(&Set);
|
||||
}
|
||||
|
||||
static void SigHupHandler(int signr){
|
||||
static void SigHupHandler(int SigNr){
|
||||
// no-op (?)
|
||||
}
|
||||
|
||||
static void SigAbortHandler(int signr){
|
||||
static void SigAbortHandler(int SigNr){
|
||||
print(1, "SigAbortHandler: schalte Writer-Thread ab.\n");
|
||||
AbortWriter();
|
||||
}
|
||||
|
||||
static void DefaultHandler(int signr){
|
||||
static void DefaultHandler(int SigNr){
|
||||
print(1, "DefaultHandler: Beende Game-Server (SigNr. %d: %s).\n",
|
||||
signr, sigdescr_np(signr));
|
||||
SigNr, GetSignalDescription(SigNr));
|
||||
|
||||
SigHandler(SIGINT, SIG_IGN);
|
||||
SigHandler(SIGQUIT, SIG_IGN);
|
||||
@ -83,8 +84,8 @@ static void DefaultHandler(int signr){
|
||||
SigHandler(SIGXFSZ, SIG_IGN);
|
||||
SigHandler(SIGPWR, SIG_IGN);
|
||||
|
||||
SaveMapOn = (signr == SIGQUIT) || (signr == SIGTERM) || (signr == SIGPWR);
|
||||
if(signr == SIGTERM){
|
||||
SaveMapOn = (SigNr == SIGQUIT) || (SigNr == SIGTERM) || (SigNr == SIGPWR);
|
||||
if(SigNr == SIGTERM){
|
||||
int Hour, Minute;
|
||||
GetRealTime(&Hour, &Minute);
|
||||
RebootTime = (Hour * 60 + Minute + 6) % 1440;
|
||||
@ -98,8 +99,8 @@ static void DefaultHandler(int signr){
|
||||
|
||||
#if 0
|
||||
// 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));
|
||||
static void ErrorHandler(int SigNr){
|
||||
error("ErrorHandler: SigNr. %d: %s\n", SigNr, GetSignalDescription(SigNr));
|
||||
EndGame();
|
||||
LogoutAllPlayers();
|
||||
exit(EXIT_FAILURE);
|
||||
@ -151,7 +152,7 @@ static void InitTime(void){
|
||||
SigEvent.sigev_notify_thread_id = gettid();
|
||||
if(timer_create(CLOCK_MONOTONIC, &SigEvent, &BeatTimer) == -1){
|
||||
error("InitTime: Failed to create beat timer: (%d) %s\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
throw "cannot create beat timer";
|
||||
}
|
||||
|
||||
@ -161,7 +162,7 @@ static void InitTime(void){
|
||||
TimerSpec.it_value = TimerSpec.it_interval;
|
||||
if(timer_settime(BeatTimer, 0, &TimerSpec, NULL) == -1){
|
||||
error("InitTime: Failed to start beat timer: (%d) %s\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
throw "cannot start beat timer";
|
||||
}
|
||||
}
|
||||
@ -169,7 +170,7 @@ static void InitTime(void){
|
||||
static void ExitTime(void){
|
||||
if(timer_delete(BeatTimer) == -1){
|
||||
error("ExitTime: Failed to delete beat timer: (%d) %s\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
}
|
||||
|
||||
SigHandler(SIGALRM, SIG_IGN);
|
||||
@ -448,7 +449,7 @@ static void AdvanceGame(int Delay){
|
||||
SendAll();
|
||||
}
|
||||
|
||||
static void SigUsr1Handler(int signr){
|
||||
static void SigUsr1Handler(int SigNr){
|
||||
SigUsr1Counter += 1;
|
||||
}
|
||||
|
||||
@ -508,7 +509,12 @@ static bool DaemonInit(bool NoFork){
|
||||
}
|
||||
|
||||
umask(0177);
|
||||
chdir(SAVEPATH);
|
||||
|
||||
// NOTE(fusion): The original binary would change directories to `SAVEPATH`
|
||||
// here, but because this function is called very early at startup, no config
|
||||
// values would have been loaded, leaving `SAVEPATH` empty and causing this
|
||||
// next call to `chdir` to always fail with ENOENT.
|
||||
//chdir(SAVEPATH);
|
||||
|
||||
int OpenMax = sysconf(_SC_OPEN_MAX);
|
||||
if(OpenMax < 0){
|
||||
|
||||
10
src/map.cc
10
src/map.cc
@ -1569,6 +1569,10 @@ void PatchSector(int SectorX, int SectorY, int SectorZ, bool FullSector,
|
||||
}
|
||||
|
||||
if(IN.Token == BYTES){
|
||||
if(OffsetX != -1 && OffsetY != -1 && !FieldPatched[OffsetX][OffsetY]){
|
||||
OUT.writeLn();
|
||||
}
|
||||
|
||||
uint8 *SectorOffset = IN.getBytesequence();
|
||||
OffsetX = (int)SectorOffset[0];
|
||||
OffsetY = (int)SectorOffset[1];
|
||||
@ -1634,6 +1638,10 @@ void PatchSector(int SectorX, int SectorY, int SectorZ, bool FullSector,
|
||||
IN.error("unknown map flag");
|
||||
}
|
||||
}
|
||||
|
||||
if(OffsetX != -1 && OffsetY != -1 && !FieldPatched[OffsetX][OffsetY]){
|
||||
OUT.writeLn();
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(fusion): Step 4.
|
||||
@ -1705,7 +1713,7 @@ void PatchSector(int SectorX, int SectorY, int SectorZ, bool FullSector,
|
||||
if(rename(FileNameBak, FileName) != 0){
|
||||
int ErrCode = errno;
|
||||
error("PatchSector: Fehler %d beim Umbenennen von %s.\n", ErrCode, FileNameBak);
|
||||
error("# Fehler %d: %s.\n", ErrCode, strerror(ErrCode));
|
||||
error("# Fehler %d: %s.\n", ErrCode, GetErrorDescription(ErrCode));
|
||||
throw "cannot patch ORIGMAP";
|
||||
}
|
||||
}
|
||||
|
||||
@ -2367,7 +2367,7 @@ void Talk(uint32 CreatureID, int Mode, const char *Addressee, const char *Text,
|
||||
continue;
|
||||
}
|
||||
|
||||
if(DistanceX > 1 && DistanceY > 1){
|
||||
if(DistanceX > 1 || DistanceY > 1){
|
||||
SendTalk(Spectator->Connection, 0, Creature->Name, Mode,
|
||||
Creature->posx, Creature->posy, Creature->posz,
|
||||
"pspsps");
|
||||
@ -2406,7 +2406,7 @@ void Talk(uint32 CreatureID, int Mode, const char *Addressee, const char *Text,
|
||||
TConnection *Connection = GetFirstConnection();
|
||||
while(Connection != NULL){
|
||||
if(Connection->Live()){
|
||||
SendMessage(Connection, TALK_ADMIN_MESSAGE, Text);
|
||||
SendMessage(Connection, TALK_ADMIN_MESSAGE, "%s", Text);
|
||||
}
|
||||
|
||||
Connection = GetNextConnection();
|
||||
|
||||
@ -95,7 +95,7 @@ void TQueryManagerConnection::connect(void){
|
||||
int SocketFlags = fcntl(this->Socket, F_GETFL);
|
||||
if(SocketFlags == -1 || fcntl(this->Socket, F_SETFL, (SocketFlags | O_NONBLOCK)) == -1){
|
||||
error("TQueryManagerConnection::connect: Failed to set socket as non-blocking: (%d) %s\n",
|
||||
errno, strerrordesc_np(errno));
|
||||
errno, GetErrorDescription(errno));
|
||||
this->disconnect();
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@ TReadScriptFile::~TReadScriptFile(void){
|
||||
|
||||
void TReadScriptFile::open(const char *FileName){
|
||||
int Depth = this->RecursionDepth + 1;
|
||||
if((Depth + 1) >= NARRAY(this->File)){
|
||||
if(Depth >= NARRAY(this->File)){
|
||||
::error("TReadScriptFile::open: Rekursionstiefe zu groß.\n");
|
||||
throw "Recursion depth too high";
|
||||
}
|
||||
@ -115,7 +115,7 @@ void TReadScriptFile::open(const char *FileName){
|
||||
if(this->File[Depth] == NULL){
|
||||
int ErrCode = errno;
|
||||
::error("TReadScriptFile::open: Kann Datei %s nicht öffnen.\n", this->Filename[Depth]);
|
||||
::error("Fehler %d: %s.\n", ErrCode, strerror(ErrCode));
|
||||
::error("Fehler %d: %s.\n", ErrCode, GetErrorDescription(ErrCode));
|
||||
throw "Cannot open script-file";
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@ void TReadScriptFile::close(void){
|
||||
|
||||
void TReadScriptFile::error(const char *Text){
|
||||
int Depth = this->RecursionDepth;
|
||||
ASSERT(Depth >= 0 && Depth <= NARRAY(this->File));
|
||||
ASSERT(Depth >= 0 && Depth < NARRAY(this->File));
|
||||
|
||||
const char *Filename = this->Filename[Depth];
|
||||
if(const char *Slash = findLast(this->Filename[Depth], '/')){
|
||||
@ -582,7 +582,7 @@ void TWriteScriptFile::open(const char *FileName){
|
||||
if(this->File == NULL){
|
||||
int ErrCode = errno;
|
||||
::error("TWriteScriptFile: Kann Datei %s nicht anlegen.\n", FileName);
|
||||
::error("Fehler %d: %s.\n", ErrCode, strerror(ErrCode));
|
||||
::error("Fehler %d: %s.\n", ErrCode, GetErrorDescription(ErrCode));
|
||||
throw "Cannot create script-file";
|
||||
}
|
||||
|
||||
@ -751,7 +751,7 @@ void TReadBinaryFile::close(void){
|
||||
int ErrCode = errno;
|
||||
::error("TReadBinaryFile::close: Fehler beim Schließen der Datei.\n");
|
||||
::error("# Datei: %s, Fehlercode: %d (%s)\n",
|
||||
this->Filename, ErrCode, strerror(ErrCode));
|
||||
this->Filename, ErrCode, GetErrorDescription(ErrCode));
|
||||
}
|
||||
this->File = NULL;
|
||||
}
|
||||
@ -811,7 +811,7 @@ uint8 TReadBinaryFile::readByte(void){
|
||||
int Position = this->getPosition();
|
||||
::error("TReadBinaryFile::readByte: Fehler beim Lesen eines Bytes\n");
|
||||
::error("# Datei: %s, Position: %d, Rückgabewert: %d, Fehlercode: %d (%s)\n",
|
||||
this->Filename, Position, Result, ErrCode, strerror(ErrCode));
|
||||
this->Filename, Position, Result, ErrCode, GetErrorDescription(ErrCode));
|
||||
|
||||
// NOTE(fusion): Close file and make a backup, possibly for further inspection.
|
||||
if(fclose(this->File) != 0){
|
||||
@ -832,7 +832,7 @@ void TReadBinaryFile::readBytes(uint8 *Buffer, int Count){
|
||||
int Position = this->getPosition();
|
||||
::error("TReadBinaryFile::readBytes: Fehler beim Lesen von %d Bytes\n", Count);
|
||||
::error("# Datei: %s, Position %d, Rückgabewert: %d, Fehlercode: %d (%s)\n",
|
||||
this->Filename, Position, Result, ErrCode, strerror(ErrCode));
|
||||
this->Filename, Position, Result, ErrCode, GetErrorDescription(ErrCode));
|
||||
|
||||
// NOTE(fusion): Close file and make a backup, possibly for further inspection.
|
||||
if(fclose(this->File) != 0){
|
||||
@ -885,7 +885,7 @@ void TWriteBinaryFile::open(const char *FileName){
|
||||
if(this->File == NULL){
|
||||
int ErrCode = errno;
|
||||
::error("TWriteBinaryFile::open: Kann Datei %s nicht anlegen.\n", FileName);
|
||||
::error("Fehler %d: %s.\n", ErrCode, strerror(ErrCode));
|
||||
::error("Fehler %d: %s.\n", ErrCode, GetErrorDescription(ErrCode));
|
||||
|
||||
snprintf(ErrorString, sizeof(ErrorString),
|
||||
"Cannot create file %s.", FileName);
|
||||
@ -902,7 +902,7 @@ void TWriteBinaryFile::close(void){
|
||||
int ErrCode = errno;
|
||||
::error("TWriteBinaryFile::close: Fehler beim Schließen der Datei.\n");
|
||||
::error("# Datei: %s, Fehlercode: %d (%s)\n",
|
||||
this->Filename, ErrCode, strerror(ErrCode));
|
||||
this->Filename, ErrCode, GetErrorDescription(ErrCode));
|
||||
}
|
||||
this->File = NULL;
|
||||
}
|
||||
@ -928,7 +928,7 @@ void TWriteBinaryFile::writeByte(uint8 Byte){
|
||||
int ErrCode = errno;
|
||||
::error("TWriteBinaryFile::writeByte: Fehler beim Schreiben eines Bytes\n");
|
||||
::error("# Datei: %s, Rückgabewert: %d, Fehlercode: %d (%s)\n",
|
||||
this->Filename, Result, ErrCode, strerror(ErrCode));
|
||||
this->Filename, Result, ErrCode, GetErrorDescription(ErrCode));
|
||||
|
||||
// NOTE(fusion): Close file and make a backup, possibly for further inspection.
|
||||
if(fclose(this->File) != 0){
|
||||
@ -947,7 +947,7 @@ void TWriteBinaryFile::writeBytes(const uint8 *Buffer, int Count){
|
||||
int ErrCode = errno;
|
||||
::error("TWriteBinaryFile::writeBytes: Fehler beim Schreiben von %d Bytes\n", Count);
|
||||
::error("# Datei: %s, Rückgabewert: %d, Fehlercode: %d (%s)\n",
|
||||
this->Filename, Result, ErrCode, strerror(ErrCode));
|
||||
this->Filename, Result, ErrCode, GetErrorDescription(ErrCode));
|
||||
|
||||
// NOTE(fusion): Close file and make a backup, possibly for further inspection.
|
||||
if(fclose(this->File) != 0){
|
||||
|
||||
@ -349,7 +349,7 @@ void SendResult(TConnection *Connection, RESULT r){
|
||||
}
|
||||
|
||||
if(Message != NULL){
|
||||
SendMessage(Connection, TALK_FAILURE_MESSAGE, Message);
|
||||
SendMessage(Connection, TALK_FAILURE_MESSAGE, "%s", Message);
|
||||
if(r == ENTERPROTECTIONZONE || r == NOTINVITED || r == MOVENOTPOSSIBLE){
|
||||
SendSnapback(Connection);
|
||||
}
|
||||
@ -1721,7 +1721,7 @@ void BroadcastMessage(int Mode, const char *Text, ...){
|
||||
TConnection *Connection = GetFirstConnection();
|
||||
while(Connection != NULL){
|
||||
if(Connection->Live()){
|
||||
SendMessage(Connection, Mode, Message);
|
||||
SendMessage(Connection, Mode, "%s", Message);
|
||||
}
|
||||
Connection = GetNextConnection();
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ static void ErrorHandler(const char *Text){
|
||||
if(SHM != NULL){
|
||||
SHM->Errors += 1;
|
||||
if(SHM->Errors <= 0x8000){
|
||||
Log("error", Text);
|
||||
Log("error", "%s", Text);
|
||||
if(SHM->Errors == 0x8000){
|
||||
Log("error", "Zu viele Fehler. Keine weitere Protokollierung.\n");
|
||||
}
|
||||
|
||||
@ -146,10 +146,10 @@ Semaphore::~Semaphore(void){
|
||||
int ErrorCode;
|
||||
if((ErrorCode = pthread_mutex_destroy(&this->mutex)) != 0){
|
||||
error("Semaphore::~Semaphore: Kann Mutex nicht freigeben: (%d) %s.\n",
|
||||
ErrorCode, strerrordesc_np(ErrorCode));
|
||||
ErrorCode, GetErrorDescription(ErrorCode));
|
||||
}else if((ErrorCode = pthread_cond_destroy(&this->condition)) != 0){
|
||||
error("Semaphore::~Semaphore: Kann Wartebedingung nicht freigeben: (%d) %s.\n",
|
||||
ErrorCode, strerrordesc_np(ErrorCode));
|
||||
ErrorCode, GetErrorDescription(ErrorCode));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
41
src/utils.cc
41
src/utils.cc
@ -1,5 +1,6 @@
|
||||
#include "common.hh"
|
||||
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static TErrorFunction *ErrorFunction;
|
||||
@ -95,6 +96,46 @@ bool FileExists(const char *FileName){
|
||||
return Result;
|
||||
}
|
||||
|
||||
#if !defined(_GNU_SOURCE) || (__GLIBC__ < 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ < 32))
|
||||
# if defined(_BSD_SOURCE) || defined(_DEFAULT_SOURCE)
|
||||
static const char *sigdescr_np(int SigNr){
|
||||
const char *Description = NULL;
|
||||
if(SigNr >= 0 && SigNr < NSIG){
|
||||
Description = sys_siglist[SigNr];
|
||||
}
|
||||
return Description;
|
||||
}
|
||||
|
||||
static const char *strerrordesc_np(int ErrCode){
|
||||
const char *Description = NULL;
|
||||
if(ErrCode >= 0 && ErrCode < sys_nerr){
|
||||
Description = sys_errlist[ErrCode];
|
||||
}
|
||||
return Description;
|
||||
}
|
||||
# else // defined(_BSD_SOURCE) || defined(_DEFAULT_SOURCE)
|
||||
#warning "Current LIBC/GLIBC version doesn't expose error/signal description tables."
|
||||
static const char *sigdescr_np(int SigNr){ return "No signal description"; }
|
||||
static const char *strerrordesc_np(int ErrCode){ return "No error description"; }
|
||||
# endif // defined(_BSD_SOURCE) || defined(_DEFAULT_SOURCE)
|
||||
#endif //!defined(_GNU_SOURCE) || (__GLIBC__ < 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ < 32))
|
||||
|
||||
const char *GetSignalDescription(int SigNr){
|
||||
const char *Description = sigdescr_np(SigNr);
|
||||
if(Description == NULL){
|
||||
Description = "Invalid signal";
|
||||
}
|
||||
return Description;
|
||||
}
|
||||
|
||||
const char *GetErrorDescription(int ErrCode){
|
||||
const char *Description = strerrordesc_np(ErrCode);
|
||||
if(Description == NULL){
|
||||
Description = "Invalid error";
|
||||
}
|
||||
return Description;
|
||||
}
|
||||
|
||||
// String Utility
|
||||
// =============================================================================
|
||||
bool isSpace(int c){
|
||||
|
||||
@ -921,7 +921,7 @@ void ProcessBroadcastReply(TBroadcastReplyData *Data){
|
||||
return;
|
||||
}
|
||||
|
||||
BroadcastMessage(TALK_STATUS_MESSAGE, Data->Message);
|
||||
BroadcastMessage(TALK_STATUS_MESSAGE, "%s", Data->Message);
|
||||
|
||||
delete Data;
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ RestartSec=10
|
||||
# After a SIGTERM, the server will issue shutdown warnings for 5 minutes and
|
||||
# take another minute or two to save everything and exit. Not waiting for this
|
||||
# process to complete would cause data to not be properly saved.
|
||||
TimeoutStopSec=600
|
||||
TimeoutStopSec=900
|
||||
LimitCORE=infinity
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
@ -30,6 +30,7 @@ var (
|
||||
releaseOptions = []string{"-O2"}
|
||||
linkerOptions = []string{
|
||||
"-Wl,-t",
|
||||
"-lrt",
|
||||
"-lcrypto",
|
||||
}
|
||||
)
|
||||
@ -80,9 +81,9 @@ func main() {
|
||||
// DEBUG SWITCH
|
||||
fmt.Fprint(&output, "DEBUG ?= 0\n")
|
||||
fmt.Fprint(&output, "ifneq ($(DEBUG), 0)\n")
|
||||
fmt.Fprintf(&output, "\tCFLAGS += %v\n", strings.Join(debugOptions, " "))
|
||||
fmt.Fprintf(&output, " CFLAGS += %v\n", strings.Join(debugOptions, " "))
|
||||
fmt.Fprint(&output, "else\n")
|
||||
fmt.Fprintf(&output, "\tCFLAGS += %v\n", strings.Join(releaseOptions, " "))
|
||||
fmt.Fprintf(&output, " CFLAGS += %v\n", strings.Join(releaseOptions, " "))
|
||||
fmt.Fprint(&output, "endif\n\n")
|
||||
|
||||
// HEADERS
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user