mirror of
https://github.com/thunderbrewhq/squall.git
synced 2026-02-04 08:59:07 +00:00
chore(error): add error codes and fix macro accuracy
This commit is contained in:
parent
0854138653
commit
6397c2fa17
6 changed files with 165 additions and 54 deletions
|
|
@ -3,8 +3,9 @@
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
void SARC4PrepareKey(const void* data, uint32_t len, SARC4Key* key) {
|
void SARC4PrepareKey(const void* data, uint32_t len, SARC4Key* key) {
|
||||||
STORM_ASSERT(data);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_VALIDATE(data, ERROR_INVALID_PARAMETER);
|
STORM_VALIDATE(data);
|
||||||
|
STORM_VALIDATE_END_VOID;
|
||||||
|
|
||||||
key->x = 0;
|
key->x = 0;
|
||||||
key->y = 0;
|
key->y = 0;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ int32_t SErrDisplayError(uint32_t errorcode, const char* filename, int32_t linen
|
||||||
|
|
||||||
printf("\n=========================================================\n");
|
printf("\n=========================================================\n");
|
||||||
|
|
||||||
if (linenumber == -5) {
|
if (linenumber == SERR_LINECODE_EXCEPTION) {
|
||||||
printf("Exception Raised!\n\n");
|
printf("Exception Raised!\n\n");
|
||||||
|
|
||||||
printf(" App: %s\n", "GenericBlizzardApp");
|
printf(" App: %s\n", "GenericBlizzardApp");
|
||||||
|
|
|
||||||
|
|
@ -9,27 +9,100 @@
|
||||||
|
|
||||||
#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX)
|
#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX)
|
||||||
#define ERROR_SUCCESS 0x0
|
#define ERROR_SUCCESS 0x0
|
||||||
|
#define ERROR_INVALID_HANDLE 0x6
|
||||||
|
#define ERROR_NOT_ENOUGH_MEMORY 0x8
|
||||||
#define ERROR_INVALID_PARAMETER 0x57
|
#define ERROR_INVALID_PARAMETER 0x57
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(NDEBUG)
|
|
||||||
#define STORM_ASSERT(x) \
|
#ifdef _DEBUG
|
||||||
(void)0
|
#ifndef ASSERTIONS_ENABLED
|
||||||
#else
|
#define ASSERTIONS_ENABLED
|
||||||
#define STORM_ASSERT(x) \
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#if defined(ASSERTIONS_ENABLED)
|
||||||
|
#define STORM_ASSERT(x) \
|
||||||
|
if (!(x)) { \
|
||||||
|
SErrDisplayError(STORM_ERROR_ASSERTION, __FILE__, __LINE__, #x, 0, 1, 0x11111111); \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define STORM_ASSERT_FATAL(x) \
|
||||||
if (!(x)) { \
|
if (!(x)) { \
|
||||||
SErrPrepareAppFatal(__FILE__, __LINE__); \
|
SErrPrepareAppFatal(__FILE__, __LINE__); \
|
||||||
SErrDisplayAppFatal(#x); \
|
SErrDisplayAppFatal(#x); \
|
||||||
} \
|
}
|
||||||
(void)0
|
#else
|
||||||
|
#define STORM_ASSERT(x)
|
||||||
|
#define STORM_ASSERT_FATAL(x)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define STORM_VALIDATE(x, y, ...) \
|
|
||||||
if (!(x)) { \
|
#if defined(NDEBUG)
|
||||||
SErrSetLastError(y); \
|
#define STORM_VALIDATE_BEGIN { bool __storm_result = true
|
||||||
return __VA_ARGS__; \
|
#define STORM_VALIDATE(x) __storm_result &= !!(x); STORM_ASSERT(x)
|
||||||
} \
|
#define STORM_VALIDATE_END \
|
||||||
(void)0
|
if (!__storm_result) { \
|
||||||
|
SErrSetLastError(ERROR_INVALID_PARAMETER); \
|
||||||
|
return 0; \
|
||||||
|
} }
|
||||||
|
#define STORM_VALIDATE_END_VOID \
|
||||||
|
if (!__storm_result) { \
|
||||||
|
SErrSetLastError(ERROR_INVALID_PARAMETER); \
|
||||||
|
return; \
|
||||||
|
} }
|
||||||
|
#else
|
||||||
|
#define STORM_VALIDATE_BEGIN {
|
||||||
|
#define STORM_VALIDATE(x) STORM_ASSERT_FATAL(x)
|
||||||
|
#define STORM_VALIDATE_END }
|
||||||
|
#define STORM_VALIDATE_END_VOID }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#define STORM_ERROR_ASSERTION 0x85100000
|
||||||
|
#define STORM_ERROR_BAD_ARGUMENT 0x85100065
|
||||||
|
#define STORM_ERROR_GAME_ALREADY_STARTED 0x85100066
|
||||||
|
#define STORM_ERROR_GAME_FULL 0x85100067
|
||||||
|
#define STORM_ERROR_GAME_NOT_FOUND 0x85100068
|
||||||
|
#define STORM_ERROR_GAME_TERMINATED 0x85100069
|
||||||
|
#define STORM_ERROR_INVALID_PLAYER 0x8510006A
|
||||||
|
#define STORM_ERROR_NO_MESSAGES_WAITING 0x8510006B
|
||||||
|
#define STORM_ERROR_NOT_ARCHIVE 0x8510006C
|
||||||
|
#define STORM_ERROR_NOT_ENOUGH_ARGUMENTS 0x8510006D
|
||||||
|
#define STORM_ERROR_NOT_IMPLEMENTED 0x8510006E
|
||||||
|
#define STORM_ERROR_NOT_IN_ARCHIVE 0x8510006F
|
||||||
|
#define STORM_ERROR_NOT_IN_GAME 0x85100070
|
||||||
|
#define STORM_ERROR_NOT_INITIALIZED 0x85100071
|
||||||
|
#define STORM_ERROR_NOT_PLAYING 0x85100072
|
||||||
|
#define STORM_ERROR_NOT_REGISTERED 0x85100073
|
||||||
|
#define STORM_ERROR_REQUIRES_CODEC 0x85100074
|
||||||
|
#define STORM_ERROR_REQUIRES_DDRAW 0x85100075
|
||||||
|
#define STORM_ERROR_REQUIRES_DSOUND 0x85100076
|
||||||
|
#define STORM_ERROR_REQUIRES_UPGRADE 0x85100077
|
||||||
|
#define STORM_ERROR_STILL_ACTIVE 0x85100078
|
||||||
|
#define STORM_ERROR_VERSION_MISMATCH 0x85100079
|
||||||
|
#define STORM_ERROR_MEMORY_ALREADY_FREED 0x8510007A
|
||||||
|
#define STORM_ERROR_MEMORY_CORRUPT 0x8510007B
|
||||||
|
#define STORM_ERROR_MEMORY_INVALID_BLOCK 0x8510007C
|
||||||
|
#define STORM_ERROR_MEMORY_MANAGER_INACTIVE 0x8510007D
|
||||||
|
#define STORM_ERROR_MEMORY_NEVER_RELEASED 0x8510007E
|
||||||
|
#define STORM_ERROR_HANDLE_NEVER_RELEASED 0x8510007F
|
||||||
|
#define STORM_ERROR_ACCESS_OUT_OF_BOUNDS 0x85100080
|
||||||
|
#define STORM_ERROR_MEMORY_NULL_POINTER 0x85100081
|
||||||
|
#define STORM_ERROR_CDKEY_MISMATCH 0x85100082
|
||||||
|
#define STORM_ERROR_DATA_FILE_CORRUPT 0x85100083
|
||||||
|
#define STORM_ERROR_FATAL_EXCEPTION 0x85100084
|
||||||
|
#define STORM_ERROR_GAME_TYPE_UNAVAILABLE 0x85100085
|
||||||
|
#define STORM_ERROR_FATAL_CONDITION 0x85100086
|
||||||
|
|
||||||
|
|
||||||
|
#define SERR_LINECODE_FUNCTION -1
|
||||||
|
#define SERR_LINECODE_OBJECT -2
|
||||||
|
#define SERR_LINECODE_HANDLE -3
|
||||||
|
#define SERR_LINECODE_FILE -4
|
||||||
|
#define SERR_LINECODE_EXCEPTION -5 // exception handler
|
||||||
|
|
||||||
|
|
||||||
[[noreturn]] void SErrDisplayAppFatal(const char* format, ...);
|
[[noreturn]] void SErrDisplayAppFatal(const char* format, ...);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,10 +159,12 @@ void ProcessBooleanOperation(TSGrowableArray<SOURCE>* sourceArray, int32_t combi
|
||||||
}
|
}
|
||||||
|
|
||||||
void SRgnCombineRectf(HSRGN handle, RECTF* rect, void* param, int32_t combineMode) {
|
void SRgnCombineRectf(HSRGN handle, RECTF* rect, void* param, int32_t combineMode) {
|
||||||
STORM_ASSERT(handle);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_ASSERT(rect);
|
STORM_VALIDATE(handle);
|
||||||
STORM_ASSERT(combineMode >= 1);
|
STORM_VALIDATE(rect);
|
||||||
STORM_ASSERT(combineMode <= 6);
|
STORM_VALIDATE(combineMode >= 1);
|
||||||
|
STORM_VALIDATE(combineMode <= 6);
|
||||||
|
STORM_VALIDATE_END_VOID;
|
||||||
|
|
||||||
HLOCKEDRGN lockedHandle;
|
HLOCKEDRGN lockedHandle;
|
||||||
auto rgn = s_rgntable.Lock(handle, &lockedHandle, 0);
|
auto rgn = s_rgntable.Lock(handle, &lockedHandle, 0);
|
||||||
|
|
@ -190,8 +192,10 @@ void SRgnCombineRectf(HSRGN handle, RECTF* rect, void* param, int32_t combineMod
|
||||||
}
|
}
|
||||||
|
|
||||||
void SRgnCreate(HSRGN* handlePtr, uint32_t reserved) {
|
void SRgnCreate(HSRGN* handlePtr, uint32_t reserved) {
|
||||||
STORM_ASSERT(handlePtr);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_ASSERT(!reserved);
|
STORM_VALIDATE(handlePtr);
|
||||||
|
STORM_VALIDATE(!reserved);
|
||||||
|
STORM_VALIDATE_END_VOID;
|
||||||
|
|
||||||
HLOCKEDRGN lockedHandle = nullptr;
|
HLOCKEDRGN lockedHandle = nullptr;
|
||||||
auto rgn = s_rgntable.NewLock(handlePtr, &lockedHandle);
|
auto rgn = s_rgntable.NewLock(handlePtr, &lockedHandle);
|
||||||
|
|
@ -202,14 +206,18 @@ void SRgnCreate(HSRGN* handlePtr, uint32_t reserved) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void SRgnDelete(HSRGN handle) {
|
void SRgnDelete(HSRGN handle) {
|
||||||
STORM_ASSERT(handle);
|
STORM_VALIDATE_BEGIN;
|
||||||
|
STORM_VALIDATE(handle);
|
||||||
|
STORM_VALIDATE_END_VOID;
|
||||||
|
|
||||||
s_rgntable.Delete(handle);
|
s_rgntable.Delete(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SRgnGetBoundingRectf(HSRGN handle, RECTF* rect) {
|
void SRgnGetBoundingRectf(HSRGN handle, RECTF* rect) {
|
||||||
STORM_ASSERT(handle);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_ASSERT(rect);
|
STORM_VALIDATE(handle);
|
||||||
|
STORM_VALIDATE(rect);
|
||||||
|
STORM_VALIDATE_END_VOID;
|
||||||
|
|
||||||
rect->left = std::numeric_limits<float>::max();
|
rect->left = std::numeric_limits<float>::max();
|
||||||
rect->bottom = std::numeric_limits<float>::max();
|
rect->bottom = std::numeric_limits<float>::max();
|
||||||
|
|
|
||||||
|
|
@ -207,8 +207,9 @@ void SStrInitialize() {
|
||||||
}
|
}
|
||||||
|
|
||||||
char* SStrChr(char* string, char search) {
|
char* SStrChr(char* string, char search) {
|
||||||
STORM_ASSERT(string);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_VALIDATE(string, ERROR_INVALID_PARAMETER, nullptr);
|
STORM_VALIDATE(string);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
if (!*string) {
|
if (!*string) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
@ -226,8 +227,9 @@ char* SStrChr(char* string, char search) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* SStrChr(const char* string, char search) {
|
const char* SStrChr(const char* string, char search) {
|
||||||
STORM_ASSERT(string);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_VALIDATE(string, ERROR_INVALID_PARAMETER, nullptr);
|
STORM_VALIDATE(string);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
if (!*string) {
|
if (!*string) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
@ -245,8 +247,9 @@ const char* SStrChr(const char* string, char search) {
|
||||||
}
|
}
|
||||||
|
|
||||||
char* SStrChrR(char* string, char search) {
|
char* SStrChrR(char* string, char search) {
|
||||||
STORM_ASSERT(string);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_VALIDATE(string, ERROR_INVALID_PARAMETER, nullptr);
|
STORM_VALIDATE(string);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
char* result;
|
char* result;
|
||||||
|
|
||||||
|
|
@ -260,8 +263,9 @@ char* SStrChrR(char* string, char search) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* SStrChrR(const char* string, char search) {
|
const char* SStrChrR(const char* string, char search) {
|
||||||
STORM_ASSERT(string);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_VALIDATE(string, ERROR_INVALID_PARAMETER, nullptr);
|
STORM_VALIDATE(string);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
const char* result;
|
const char* result;
|
||||||
|
|
||||||
|
|
@ -275,10 +279,20 @@ const char* SStrChrR(const char* string, char search) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t SStrCmp(const char* string1, const char* string2, size_t maxchars) {
|
int32_t SStrCmp(const char* string1, const char* string2, size_t maxchars) {
|
||||||
|
STORM_VALIDATE_BEGIN;
|
||||||
|
STORM_VALIDATE(string1);
|
||||||
|
STORM_VALIDATE(string2);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
return strncmp(string1, string2, maxchars);
|
return strncmp(string1, string2, maxchars);
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t SStrCmpI(const char* string1, const char* string2, size_t maxchars) {
|
int32_t SStrCmpI(const char* string1, const char* string2, size_t maxchars) {
|
||||||
|
STORM_VALIDATE_BEGIN;
|
||||||
|
STORM_VALIDATE(string1);
|
||||||
|
STORM_VALIDATE(string2);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
#if defined(WHOA_SYSTEM_WIN)
|
#if defined(WHOA_SYSTEM_WIN)
|
||||||
return _strnicmp(string1, string2, maxchars);
|
return _strnicmp(string1, string2, maxchars);
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -289,9 +303,10 @@ int32_t SStrCmpI(const char* string1, const char* string2, size_t maxchars) {
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t SStrCopy(char* dest, const char* source, size_t destsize) {
|
size_t SStrCopy(char* dest, const char* source, size_t destsize) {
|
||||||
STORM_ASSERT(dest);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_ASSERT(source);
|
STORM_VALIDATE(dest);
|
||||||
STORM_VALIDATE(dest && source, ERROR_INVALID_PARAMETER, 0);
|
STORM_VALIDATE(source);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
char* destbuf = dest;
|
char* destbuf = dest;
|
||||||
|
|
||||||
|
|
@ -323,6 +338,10 @@ size_t SStrCopy(char* dest, const char* source, size_t destsize) {
|
||||||
}
|
}
|
||||||
|
|
||||||
char* SStrDupA(const char* string, const char* filename, uint32_t linenumber) {
|
char* SStrDupA(const char* string, const char* filename, uint32_t linenumber) {
|
||||||
|
STORM_VALIDATE_BEGIN;
|
||||||
|
STORM_VALIDATE(string);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
size_t len = SStrLen(string) + 1;
|
size_t len = SStrLen(string) + 1;
|
||||||
char* dup = static_cast<char*>(SMemAlloc(len, filename, linenumber, 0x0));
|
char* dup = static_cast<char*>(SMemAlloc(len, filename, linenumber, 0x0));
|
||||||
memcpy(dup, string, len);
|
memcpy(dup, string, len);
|
||||||
|
|
@ -362,7 +381,9 @@ uint32_t SStrHashHT(const char* string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t SStrLen(const char* string) {
|
size_t SStrLen(const char* string) {
|
||||||
STORM_ASSERT(string);
|
STORM_VALIDATE_BEGIN;
|
||||||
|
STORM_VALIDATE(string);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
auto stringEnd = string;
|
auto stringEnd = string;
|
||||||
while (*stringEnd) {
|
while (*stringEnd) {
|
||||||
|
|
@ -380,8 +401,10 @@ void SStrLower(char* string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t SStrPack(char* dest, const char* source, uint32_t destsize) {
|
uint32_t SStrPack(char* dest, const char* source, uint32_t destsize) {
|
||||||
STORM_ASSERT(dest);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_ASSERT(source);
|
STORM_VALIDATE(dest);
|
||||||
|
STORM_VALIDATE(source);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
if (!destsize) {
|
if (!destsize) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -426,15 +449,19 @@ size_t SStrPrintf(char* dest, size_t maxchars, const char* format, ...) {
|
||||||
va_list va;
|
va_list va;
|
||||||
va_start(va, format);
|
va_start(va, format);
|
||||||
|
|
||||||
STORM_ASSERT(dest);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_ASSERT(format);
|
STORM_VALIDATE(dest);
|
||||||
|
STORM_VALIDATE(format);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
return ISStrVPrintf(format, va, dest, maxchars);
|
return ISStrVPrintf(format, va, dest, maxchars);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* SStrStr(const char* string, const char* search) {
|
const char* SStrStr(const char* string, const char* search) {
|
||||||
STORM_ASSERT(string);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_ASSERT(search);
|
STORM_VALIDATE(string);
|
||||||
|
STORM_VALIDATE(search);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
if (!*string) {
|
if (!*string) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
@ -460,14 +487,12 @@ const char* SStrStr(const char* string, const char* search) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void SStrTokenize(const char** string, char* buffer, size_t bufferchars, const char* whitespace, int32_t* quoted) {
|
void SStrTokenize(const char** string, char* buffer, size_t bufferchars, const char* whitespace, int32_t* quoted) {
|
||||||
STORM_ASSERT(string);
|
STORM_VALIDATE_BEGIN;
|
||||||
STORM_VALIDATE(string, ERROR_INVALID_PARAMETER);
|
STORM_VALIDATE(string);
|
||||||
STORM_ASSERT(*string);
|
STORM_VALIDATE(*string);
|
||||||
STORM_VALIDATE(*string, ERROR_INVALID_PARAMETER);
|
STORM_VALIDATE(buffer || bufferchars == 0);
|
||||||
STORM_ASSERT(buffer || !bufferchars);
|
STORM_VALIDATE(whitespace);
|
||||||
STORM_VALIDATE(buffer || !bufferchars, ERROR_INVALID_PARAMETER);
|
STORM_VALIDATE_END_VOID;
|
||||||
STORM_ASSERT(whitespace);
|
|
||||||
STORM_VALIDATE(whitespace, ERROR_INVALID_PARAMETER);
|
|
||||||
|
|
||||||
int32_t checkquotes = SStrChr(whitespace, '"') != nullptr;
|
int32_t checkquotes = SStrChr(whitespace, '"') != nullptr;
|
||||||
|
|
||||||
|
|
@ -529,7 +554,9 @@ void SStrTokenize(const char** string, char* buffer, size_t bufferchars, const c
|
||||||
}
|
}
|
||||||
|
|
||||||
float SStrToFloat(const char* string) {
|
float SStrToFloat(const char* string) {
|
||||||
STORM_ASSERT(string);
|
STORM_VALIDATE_BEGIN;
|
||||||
|
STORM_VALIDATE(string);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
SStrInitialize();
|
SStrInitialize();
|
||||||
|
|
||||||
|
|
@ -626,7 +653,9 @@ float SStrToFloat(const char* string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t SStrToInt(const char* string) {
|
int32_t SStrToInt(const char* string) {
|
||||||
STORM_ASSERT(string);
|
STORM_VALIDATE_BEGIN;
|
||||||
|
STORM_VALIDATE(string);
|
||||||
|
STORM_VALIDATE_END;
|
||||||
|
|
||||||
int32_t result = 0;
|
int32_t result = 0;
|
||||||
bool negative = false;
|
bool negative = false;
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ void TSBaseArray<T>::CheckArrayBounds(uint32_t index) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
SErrDisplayErrorFmt(
|
SErrDisplayErrorFmt(
|
||||||
0x85100080,
|
STORM_ERROR_ACCESS_OUT_OF_BOUNDS,
|
||||||
this->MemFileName(),
|
this->MemFileName(),
|
||||||
this->MemLineNo(),
|
this->MemLineNo(),
|
||||||
1,
|
1,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue