Switch audio to hash table implementation
This commit is contained in:
parent
f6943e5efb
commit
5cccd3ebe8
@ -231,8 +231,9 @@ Texture *asset_loadTextureImage(AssetManager *assetManager, MemoryArena *arena,
|
|||||||
* Animation Asset Managing
|
* Animation Asset Managing
|
||||||
*********************************
|
*********************************
|
||||||
*/
|
*/
|
||||||
INTERNAL Animation *getFreeAnimationSlot(AssetManager *assetManager,
|
INTERNAL Animation *getFreeAnimationSlot(AssetManager *const assetManager,
|
||||||
MemoryArena *arena, char *key)
|
MemoryArena *const arena,
|
||||||
|
const char *const key)
|
||||||
{
|
{
|
||||||
HashTableEntry *entry = getFreeHashSlot(&assetManager->anims, arena, key);
|
HashTableEntry *entry = getFreeHashSlot(&assetManager->anims, arena, key);
|
||||||
|
|
||||||
@ -248,9 +249,11 @@ INTERNAL Animation *getFreeAnimationSlot(AssetManager *assetManager,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void asset_addAnimation(AssetManager *assetManager, MemoryArena *arena,
|
void asset_addAnimation(AssetManager *const assetManager,
|
||||||
char *animName, TexAtlas *atlas, char **subTextureNames,
|
MemoryArena *const arena, const char *const animName,
|
||||||
i32 numSubTextures, f32 frameDuration)
|
TexAtlas *const atlas,
|
||||||
|
char **const subTextureNames,
|
||||||
|
const i32 numSubTextures, const f32 frameDuration)
|
||||||
{
|
{
|
||||||
Animation *anim = getFreeAnimationSlot(assetManager, arena, animName);
|
Animation *anim = getFreeAnimationSlot(assetManager, arena, animName);
|
||||||
|
|
||||||
@ -307,8 +310,10 @@ typedef struct XmlToken
|
|||||||
i32 len;
|
i32 len;
|
||||||
} XmlToken;
|
} XmlToken;
|
||||||
|
|
||||||
INTERNAL XmlToken *tokeniseXmlBuffer(MemoryArena *arena, char *buffer,
|
INTERNAL XmlToken *const tokeniseXmlBuffer(MemoryArena *const arena,
|
||||||
i32 bufferSize, int *numTokens)
|
const char *const buffer,
|
||||||
|
const i32 bufferSize,
|
||||||
|
int *const numTokens)
|
||||||
{
|
{
|
||||||
XmlToken *xmlTokens = PLATFORM_MEM_ALLOC(arena, 8192, XmlToken);
|
XmlToken *xmlTokens = PLATFORM_MEM_ALLOC(arena, 8192, XmlToken);
|
||||||
i32 tokenIndex = 0;
|
i32 tokenIndex = 0;
|
||||||
@ -412,8 +417,9 @@ INTERNAL XmlToken *tokeniseXmlBuffer(MemoryArena *arena, char *buffer,
|
|||||||
return xmlTokens;
|
return xmlTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
INTERNAL XmlNode *buildXmlTree(MemoryArena *arena, XmlToken *xmlTokens,
|
INTERNAL XmlNode *const buildXmlTree(MemoryArena *const arena,
|
||||||
i32 numTokens)
|
XmlToken *const xmlTokens,
|
||||||
|
const i32 numTokens)
|
||||||
{
|
{
|
||||||
XmlNode *root = PLATFORM_MEM_ALLOC(arena, 1, XmlNode);
|
XmlNode *root = PLATFORM_MEM_ALLOC(arena, 1, XmlNode);
|
||||||
if (!root) return NULL;
|
if (!root) return NULL;
|
||||||
@ -732,7 +738,8 @@ INTERNAL void parseXmlTreeToGame(AssetManager *assetManager, MemoryArena *arena,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
INTERNAL void recursiveFreeXmlTree(MemoryArena *arena, XmlNode *node)
|
INTERNAL void recursiveFreeXmlTree(MemoryArena *const arena,
|
||||||
|
XmlNode *node)
|
||||||
{
|
{
|
||||||
if (!node)
|
if (!node)
|
||||||
{
|
{
|
||||||
@ -759,11 +766,12 @@ INTERNAL void recursiveFreeXmlTree(MemoryArena *arena, XmlNode *node)
|
|||||||
node->name = NULL;
|
node->name = NULL;
|
||||||
node->isClosed = FALSE;
|
node->isClosed = FALSE;
|
||||||
PLATFORM_MEM_FREE(arena, node, sizeof(XmlNode));
|
PLATFORM_MEM_FREE(arena, node, sizeof(XmlNode));
|
||||||
|
node = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
INTERNAL void freeXmlData(MemoryArena *arena, XmlToken *tokens, i32 numTokens,
|
INTERNAL void freeXmlData(MemoryArena *const arena, XmlToken *tokens,
|
||||||
XmlNode *tree)
|
const i32 numTokens, XmlNode *tree)
|
||||||
{
|
{
|
||||||
if (tree) recursiveFreeXmlTree(arena, tree);
|
if (tree) recursiveFreeXmlTree(arena, tree);
|
||||||
if (tokens) PLATFORM_MEM_FREE(arena, tokens, numTokens * sizeof(XmlToken));
|
if (tokens) PLATFORM_MEM_FREE(arena, tokens, numTokens * sizeof(XmlToken));
|
||||||
@ -774,8 +782,9 @@ INTERNAL void freeXmlData(MemoryArena *arena, XmlToken *tokens, i32 numTokens,
|
|||||||
* Everything else
|
* Everything else
|
||||||
*********************************
|
*********************************
|
||||||
*/
|
*/
|
||||||
i32 asset_loadXmlFile(AssetManager *assetManager, MemoryArena *arena,
|
const i32 asset_loadXmlFile(AssetManager *const assetManager,
|
||||||
PlatformFileRead *fileRead)
|
MemoryArena *const arena,
|
||||||
|
const PlatformFileRead *const fileRead)
|
||||||
{
|
{
|
||||||
i32 result = 0;
|
i32 result = 0;
|
||||||
/* Tokenise buffer */
|
/* Tokenise buffer */
|
||||||
@ -802,49 +811,49 @@ i32 asset_loadXmlFile(AssetManager *assetManager, MemoryArena *arena,
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(doyle): Switch to hash based lookup
|
AudioVorbis *const asset_getVorbis(AssetManager *const assetManager,
|
||||||
// TODO(doyle): Use pointers, so we can forward declare all assets?
|
const char *const key)
|
||||||
AudioVorbis *asset_getVorbis(AssetManager *assetManager,
|
|
||||||
const enum AudioList type)
|
|
||||||
{
|
{
|
||||||
if (type < audiolist_count)
|
|
||||||
return &assetManager->audio[type];
|
|
||||||
|
|
||||||
#ifdef DENGINE_DEBUG
|
HashTableEntry *entry = getEntryFromHash(&assetManager->audio, key);
|
||||||
ASSERT(INVALID_CODE_PATH);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return NULL;
|
AudioVorbis *result = NULL;
|
||||||
|
if (entry) result = CAST(AudioVorbis *)entry->data;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const i32 asset_loadVorbis(AssetManager *assetManager, MemoryArena *arena,
|
const i32 asset_loadVorbis(AssetManager *assetManager, MemoryArena *arena,
|
||||||
const char *const path, const enum AudioList type)
|
const char *const path, const char *const key)
|
||||||
{
|
{
|
||||||
|
HashTableEntry *entry = getFreeHashSlot(&assetManager->audio, arena, key);
|
||||||
|
if (!entry) return -1;
|
||||||
|
|
||||||
// TODO(doyle): Remember to free vorbis file if we remove from memory
|
// TODO(doyle): Remember to free vorbis file if we remove from memory
|
||||||
PlatformFileRead fileRead = {0};
|
PlatformFileRead fileRead = {0};
|
||||||
platform_readFileToBuffer(arena, path, &fileRead);
|
platform_readFileToBuffer(arena, path, &fileRead);
|
||||||
|
|
||||||
|
entry->data = PLATFORM_MEM_ALLOC(arena, 1, AudioVorbis);
|
||||||
|
|
||||||
i32 error;
|
i32 error;
|
||||||
AudioVorbis audio = {0};
|
AudioVorbis *audio = CAST(AudioVorbis *) entry->data;
|
||||||
audio.type = type;
|
audio->file =
|
||||||
audio.file =
|
|
||||||
stb_vorbis_open_memory(fileRead.buffer, fileRead.size, &error, NULL);
|
stb_vorbis_open_memory(fileRead.buffer, fileRead.size, &error, NULL);
|
||||||
|
|
||||||
if (!audio.file)
|
if (!audio->file)
|
||||||
{
|
{
|
||||||
printf("stb_vorbis_open_memory() failed: Error code %d\n", error);
|
printf("stb_vorbis_open_memory() failed: Error code %d\n", error);
|
||||||
platform_closeFileRead(arena, &fileRead);
|
platform_closeFileRead(arena, &fileRead);
|
||||||
stb_vorbis_close(audio.file);
|
stb_vorbis_close(audio->file);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.info = stb_vorbis_get_info(audio.file);
|
audio->name = entry->key;
|
||||||
audio.lengthInSamples = stb_vorbis_stream_length_in_samples(audio.file);
|
audio->info = stb_vorbis_get_info(audio->file);
|
||||||
audio.lengthInSeconds = stb_vorbis_stream_length_in_seconds(audio.file);
|
audio->lengthInSamples = stb_vorbis_stream_length_in_samples(audio->file);
|
||||||
audio.data = CAST(u8 *) fileRead.buffer;
|
audio->lengthInSeconds = stb_vorbis_stream_length_in_seconds(audio->file);
|
||||||
audio.size = fileRead.size;
|
audio->data = CAST(u8 *) fileRead.buffer;
|
||||||
|
audio->size = fileRead.size;
|
||||||
assetManager->audio[type] = audio;
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -902,7 +911,8 @@ INTERNAL i32 shaderLoadProgram(Shader *const shader, const GLuint vertexShader,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Shader *asset_getShader(AssetManager *assetManager, const enum ShaderList type)
|
Shader *const asset_getShader(AssetManager *assetManager,
|
||||||
|
const enum ShaderList type)
|
||||||
{
|
{
|
||||||
if (type < shaderlist_count)
|
if (type < shaderlist_count)
|
||||||
return &assetManager->shaders[type];
|
return &assetManager->shaders[type];
|
||||||
@ -1202,7 +1212,8 @@ const i32 asset_loadTTFont(AssetManager *assetManager, MemoryArena *arena,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
v2 asset_stringDimInPixels(const Font *const font, const char *const string)
|
const v2 asset_stringDimInPixels(const Font *const font,
|
||||||
|
const char *const string)
|
||||||
{
|
{
|
||||||
v2 stringDim = V2(0, 0);
|
v2 stringDim = V2(0, 0);
|
||||||
for (i32 i = 0; i < common_strlen(string); i++)
|
for (i32 i = 0; i < common_strlen(string); i++)
|
||||||
|
@ -296,13 +296,7 @@ const i32 audio_streamPlayVorbis(MemoryArena *arena, AudioManager *audioManager,
|
|||||||
// simultaneously, we need unique file pointers into the data to track song
|
// simultaneously, we need unique file pointers into the data to track song
|
||||||
// position uniquely
|
// position uniquely
|
||||||
AudioVorbis *copyAudio = PLATFORM_MEM_ALLOC(arena, 1, AudioVorbis);
|
AudioVorbis *copyAudio = PLATFORM_MEM_ALLOC(arena, 1, AudioVorbis);
|
||||||
copyAudio->type = vorbis->type;
|
*copyAudio = *vorbis;
|
||||||
copyAudio->info = vorbis->info;
|
|
||||||
copyAudio->lengthInSamples = vorbis->lengthInSamples;
|
|
||||||
copyAudio->lengthInSeconds = vorbis->lengthInSeconds;
|
|
||||||
|
|
||||||
copyAudio->data = vorbis->data;
|
|
||||||
copyAudio->size = vorbis->size;
|
|
||||||
|
|
||||||
i32 error;
|
i32 error;
|
||||||
copyAudio->file =
|
copyAudio->file =
|
||||||
|
@ -114,7 +114,12 @@ INTERNAL void rendererInit(GameState *state, v2 windowSize)
|
|||||||
INTERNAL void assetInit(GameState *state)
|
INTERNAL void assetInit(GameState *state)
|
||||||
{
|
{
|
||||||
AssetManager *assetManager = &state->assetManager;
|
AssetManager *assetManager = &state->assetManager;
|
||||||
MemoryArena *arena = &state->arena;
|
MemoryArena *arena = &state->arena;
|
||||||
|
|
||||||
|
i32 audioEntries = 32;
|
||||||
|
assetManager->audio.size = audioEntries;
|
||||||
|
assetManager->audio.entries =
|
||||||
|
PLATFORM_MEM_ALLOC(arena, audioEntries, HashTableEntry);
|
||||||
|
|
||||||
i32 texAtlasEntries = 8;
|
i32 texAtlasEntries = 8;
|
||||||
assetManager->texAtlas.size = texAtlasEntries;
|
assetManager->texAtlas.size = texAtlasEntries;
|
||||||
@ -280,11 +285,11 @@ INTERNAL void assetInit(GameState *state)
|
|||||||
|
|
||||||
char *audioPath =
|
char *audioPath =
|
||||||
"data/audio/Motoi Sakuraba - Stab the sword of justice.ogg";
|
"data/audio/Motoi Sakuraba - Stab the sword of justice.ogg";
|
||||||
asset_loadVorbis(assetManager, arena, audioPath, audiolist_battle);
|
asset_loadVorbis(assetManager, arena, audioPath, "audio_battle");
|
||||||
audioPath = "data/audio/Motoi Sakuraba - Field of Exper.ogg";
|
audioPath = "data/audio/Motoi Sakuraba - Field of Exper.ogg";
|
||||||
asset_loadVorbis(assetManager, arena, audioPath, audiolist_overworld);
|
asset_loadVorbis(assetManager, arena, audioPath, "audio_overworld");
|
||||||
audioPath = "data/audio/nuindependent_hit22.ogg";
|
audioPath = "data/audio/nuindependent_hit22.ogg";
|
||||||
asset_loadVorbis(assetManager, arena, audioPath, audiolist_tackle);
|
asset_loadVorbis(assetManager, arena, audioPath, "audio_tackle");
|
||||||
|
|
||||||
#ifdef DENGINE_DEBUG
|
#ifdef DENGINE_DEBUG
|
||||||
DEBUG_LOG("Sound assets initialised");
|
DEBUG_LOG("Sound assets initialised");
|
||||||
@ -1160,10 +1165,11 @@ void worldTraveller_gameUpdateAndRender(GameState *state, f32 dt)
|
|||||||
if (world->numEntitiesInBattle > 0)
|
if (world->numEntitiesInBattle > 0)
|
||||||
{
|
{
|
||||||
AudioVorbis *battleTheme =
|
AudioVorbis *battleTheme =
|
||||||
asset_getVorbis(assetManager, audiolist_battle);
|
asset_getVorbis(assetManager, "audio_battle");
|
||||||
if (audioRenderer->audio)
|
if (audioRenderer->audio)
|
||||||
{
|
{
|
||||||
if (audioRenderer->audio->type != audiolist_battle)
|
if (common_strcmp(audioRenderer->audio->key,
|
||||||
|
"audio_battle") != 0)
|
||||||
{
|
{
|
||||||
audio_streamPlayVorbis(arena, &state->audioManager,
|
audio_streamPlayVorbis(arena, &state->audioManager,
|
||||||
audioRenderer, battleTheme,
|
audioRenderer, battleTheme,
|
||||||
@ -1180,10 +1186,11 @@ void worldTraveller_gameUpdateAndRender(GameState *state, f32 dt)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
AudioVorbis *overworldTheme =
|
AudioVorbis *overworldTheme =
|
||||||
asset_getVorbis(assetManager, audiolist_overworld);
|
asset_getVorbis(assetManager, "audio_overworld");
|
||||||
if (audioRenderer->audio)
|
if (audioRenderer->audio)
|
||||||
{
|
{
|
||||||
if (audioRenderer->audio->type != audiolist_overworld)
|
if (common_strcmp(audioRenderer->audio->key,
|
||||||
|
"audio_overworld") != 0)
|
||||||
{
|
{
|
||||||
audio_streamPlayVorbis(
|
audio_streamPlayVorbis(
|
||||||
arena, &state->audioManager, audioRenderer,
|
arena, &state->audioManager, audioRenderer,
|
||||||
@ -1484,7 +1491,7 @@ void worldTraveller_gameUpdateAndRender(GameState *state, f32 dt)
|
|||||||
Entity *defender = attackSpec->defender;
|
Entity *defender = attackSpec->defender;
|
||||||
|
|
||||||
audio_playVorbis(arena, audioManager, attacker->audioRenderer,
|
audio_playVorbis(arena, audioManager, attacker->audioRenderer,
|
||||||
asset_getVorbis(assetManager, audiolist_tackle),
|
asset_getVorbis(assetManager, "audio_tackle"),
|
||||||
1);
|
1);
|
||||||
|
|
||||||
/* Get first free string position and store the damage str data */
|
/* Get first free string position and store the damage str data */
|
||||||
|
@ -15,10 +15,10 @@ typedef struct AssetManager
|
|||||||
HashTable texAtlas;
|
HashTable texAtlas;
|
||||||
HashTable textures;
|
HashTable textures;
|
||||||
HashTable anims;
|
HashTable anims;
|
||||||
|
HashTable audio;
|
||||||
|
|
||||||
/* Primitive Array */
|
/* Primitive Array */
|
||||||
Shader shaders[32];
|
Shader shaders[2];
|
||||||
AudioVorbis audio[32];
|
|
||||||
Font font;
|
Font font;
|
||||||
} AssetManager;
|
} AssetManager;
|
||||||
|
|
||||||
@ -45,26 +45,33 @@ Texture *asset_loadTextureImage(AssetManager *assetManager, MemoryArena *arena,
|
|||||||
* Animation Asset Managing
|
* Animation Asset Managing
|
||||||
*********************************
|
*********************************
|
||||||
*/
|
*/
|
||||||
void asset_addAnimation(AssetManager *assetManager, MemoryArena *arena,
|
void asset_addAnimation(AssetManager *const assetManager,
|
||||||
char *animName, TexAtlas *atlas, char **subTextureNames,
|
MemoryArena *const arena, const char *const animName,
|
||||||
i32 numSubTextures, f32 frameDuration);
|
TexAtlas *const atlas, char **const subTextureNames,
|
||||||
|
const i32 numSubTextures, const f32 frameDuration);
|
||||||
Animation *asset_getAnim(AssetManager *const assetManager,
|
Animation *asset_getAnim(AssetManager *const assetManager,
|
||||||
const char *const key);
|
const char *const key);
|
||||||
|
|
||||||
|
/*
|
||||||
|
*********************************
|
||||||
|
* Audio
|
||||||
|
*********************************
|
||||||
|
*/
|
||||||
|
AudioVorbis *const asset_getVorbis(AssetManager *const assetManager,
|
||||||
|
const char *const key);
|
||||||
|
const i32 asset_loadVorbis(AssetManager *assetManager, MemoryArena *arena,
|
||||||
|
const char *const path, const char *const key);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
*********************************
|
*********************************
|
||||||
* Everything else
|
* Everything else
|
||||||
*********************************
|
*********************************
|
||||||
*/
|
*/
|
||||||
i32 asset_loadXmlFile(AssetManager *assetManager, MemoryArena *arena,
|
const i32 asset_loadXmlFile(AssetManager *const assetManager,
|
||||||
PlatformFileRead *fileRead);
|
MemoryArena *const arena,
|
||||||
|
const PlatformFileRead *const fileRead);
|
||||||
|
|
||||||
AudioVorbis *asset_getVorbis(AssetManager *assetManager,
|
Shader *const asset_getShader(AssetManager *assetManager, const enum ShaderList type);
|
||||||
const enum AudioList type);
|
|
||||||
const i32 asset_loadVorbis(AssetManager *assetManager, MemoryArena *arena,
|
|
||||||
const char *const path, const enum AudioList type);
|
|
||||||
|
|
||||||
Shader *asset_getShader(AssetManager *assetManager, const enum ShaderList type);
|
|
||||||
const i32 asset_loadShaderFiles(AssetManager *assetManager, MemoryArena *arena,
|
const i32 asset_loadShaderFiles(AssetManager *assetManager, MemoryArena *arena,
|
||||||
const char *const vertexPath,
|
const char *const vertexPath,
|
||||||
const char *const fragmentPath,
|
const char *const fragmentPath,
|
||||||
@ -72,7 +79,8 @@ const i32 asset_loadShaderFiles(AssetManager *assetManager, MemoryArena *arena,
|
|||||||
|
|
||||||
const i32 asset_loadTTFont(AssetManager *assetManager, MemoryArena *arena,
|
const i32 asset_loadTTFont(AssetManager *assetManager, MemoryArena *arena,
|
||||||
const char *filePath);
|
const char *filePath);
|
||||||
v2 asset_stringDimInPixels(const Font *const font, const char *const string);
|
const v2 asset_stringDimInPixels(const Font *const font,
|
||||||
|
const char *const string);
|
||||||
|
|
||||||
void asset_unitTest(MemoryArena *arena);
|
void asset_unitTest(MemoryArena *arena);
|
||||||
|
|
||||||
|
@ -48,33 +48,6 @@ enum ShaderList
|
|||||||
shaderlist_count,
|
shaderlist_count,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
*********************************
|
|
||||||
* Audio
|
|
||||||
*********************************
|
|
||||||
*/
|
|
||||||
enum AudioList
|
|
||||||
{
|
|
||||||
audiolist_battle,
|
|
||||||
audiolist_overworld,
|
|
||||||
audiolist_tackle,
|
|
||||||
audiolist_count,
|
|
||||||
audiolist_invalid,
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct AudioVorbis
|
|
||||||
{
|
|
||||||
enum AudioList type;
|
|
||||||
stb_vorbis *file;
|
|
||||||
stb_vorbis_info info;
|
|
||||||
|
|
||||||
u32 lengthInSamples;
|
|
||||||
f32 lengthInSeconds;
|
|
||||||
|
|
||||||
u8 *data;
|
|
||||||
i32 size;
|
|
||||||
} AudioVorbis;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
*********************************
|
*********************************
|
||||||
* Hash Table
|
* Hash Table
|
||||||
@ -94,6 +67,28 @@ typedef struct HashTable
|
|||||||
i32 size;
|
i32 size;
|
||||||
} HashTable;
|
} HashTable;
|
||||||
|
|
||||||
|
/*
|
||||||
|
*********************************
|
||||||
|
* Audio
|
||||||
|
*********************************
|
||||||
|
*/
|
||||||
|
typedef struct AudioVorbis
|
||||||
|
{
|
||||||
|
union {
|
||||||
|
char *key;
|
||||||
|
char *name;
|
||||||
|
};
|
||||||
|
|
||||||
|
stb_vorbis *file;
|
||||||
|
stb_vorbis_info info;
|
||||||
|
|
||||||
|
u32 lengthInSamples;
|
||||||
|
f32 lengthInSeconds;
|
||||||
|
|
||||||
|
u8 *data;
|
||||||
|
i32 size;
|
||||||
|
} AudioVorbis;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
*********************************
|
*********************************
|
||||||
* Texture Assets
|
* Texture Assets
|
||||||
|
Loading…
Reference in New Issue
Block a user