mirror of
https://github.com/ClassiCube/ClassiCube.git
synced 2025-09-17 03:25:14 -04:00
Fix crash if opening audio device fails, more work on porting launcher to C
This commit is contained in:
parent
55a5008913
commit
a3b8cd48c2
@ -97,7 +97,7 @@ namespace ClassicalSharp.Audio {
|
||||
void PlaySound(IAudioOutput output, float volume) {
|
||||
try {
|
||||
output.SetVolume(volume);
|
||||
output.SetFormat(format);
|
||||
output.SetFormat(format);
|
||||
output.PlayData(0, chunk);
|
||||
} catch (InvalidOperationException ex) {
|
||||
ErrorHandler.LogError("AudioPlayer.PlayCurrentSound()", ex);
|
||||
|
@ -94,6 +94,7 @@ namespace SharpWave {
|
||||
public override void Play() { }
|
||||
|
||||
public override void Stop() {
|
||||
if (devHandle == IntPtr.Zero) return;
|
||||
uint result = WinMM.waveOutReset(devHandle);
|
||||
CheckError(result, "Reset");
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ static ReturnCode Sound_ReadWave(const String* filename, struct Sound* snd) {
|
||||
ReturnCode res;
|
||||
|
||||
String_InitArray(path, pathBuffer);
|
||||
String_Format2(&path, "audio%r%s", &Directory_Separator, filename);
|
||||
String_Format1(&path, "audio/%s", filename);
|
||||
|
||||
res = Stream_OpenFile(&stream, &path);
|
||||
if (res) return res;
|
||||
@ -460,7 +460,7 @@ static void Music_RunLoop(void) {
|
||||
file = StringsBuffer_UNSAFE_Get(&files, musicFiles[idx]);
|
||||
|
||||
path.length = 0;
|
||||
String_Format2(&path, "audio%r%s", &Directory_Separator, &file);
|
||||
String_Format1(&path, "audio/%s", &file);
|
||||
Platform_Log1("playing music file: %s", &file);
|
||||
|
||||
res = Stream_OpenFile(&stream, &path);
|
||||
|
@ -101,7 +101,7 @@ static void Chat_OpenLog(struct DateTime* now) {
|
||||
/* Ensure multiple instances do not end up overwriting each other's log entries. */
|
||||
for (i = 0; i < 20; i++) {
|
||||
path->length = 0;
|
||||
String_Format4(path, "logs%r%p4-%p2-%p2 ", &Directory_Separator, &now->Year, &now->Month, &now->Day);
|
||||
String_Format3(path, "logs/%p4-%p2-%p2 ", &now->Year, &now->Month, &now->Day);
|
||||
|
||||
if (i > 0) {
|
||||
String_Format2(path, "%s _%i.log", &Chat_LogName, &i);
|
||||
|
@ -210,6 +210,7 @@
|
||||
<ClInclude Include="Gui.h" />
|
||||
<ClInclude Include="HeldBlockRenderer.h" />
|
||||
<ClInclude Include="Launcher.h" />
|
||||
<ClInclude Include="LWidgets.h" />
|
||||
<ClInclude Include="Model.h" />
|
||||
<ClInclude Include="Input.h" />
|
||||
<ClInclude Include="InputHandler.h" />
|
||||
@ -226,6 +227,7 @@
|
||||
<ClInclude Include="BlockPhysics.h" />
|
||||
<ClInclude Include="Picking.h" />
|
||||
<ClInclude Include="PickedPosRenderer.h" />
|
||||
<ClInclude Include="Resources.h" />
|
||||
<ClInclude Include="Screens.h" />
|
||||
<ClInclude Include="SelectionBox.h" />
|
||||
<ClInclude Include="ServerConnection.h" />
|
||||
@ -271,6 +273,7 @@
|
||||
<ClCompile Include="InputHandler.c" />
|
||||
<ClCompile Include="Inventory.c" />
|
||||
<ClCompile Include="Launcher.c" />
|
||||
<ClCompile Include="LWidgets.c" />
|
||||
<ClCompile Include="MapGenerator.c" />
|
||||
<ClCompile Include="Deflate.c" />
|
||||
<ClCompile Include="Model.c" />
|
||||
@ -290,6 +293,7 @@
|
||||
<ClCompile Include="Picking.c" />
|
||||
<ClCompile Include="Platform.c" />
|
||||
<ClCompile Include="Program.c" />
|
||||
<ClCompile Include="Resources.c" />
|
||||
<ClCompile Include="Screens.c" />
|
||||
<ClCompile Include="SelectionBox.c" />
|
||||
<ClCompile Include="ServerConnection.c" />
|
||||
|
@ -306,6 +306,12 @@
|
||||
<ClInclude Include="Launcher.h">
|
||||
<Filter>Header Files\Launcher</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Resources.h">
|
||||
<Filter>Header Files\Launcher</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LWidgets.h">
|
||||
<Filter>Header Files\Launcher</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="String.c">
|
||||
@ -545,5 +551,11 @@
|
||||
<ClCompile Include="Launcher.c">
|
||||
<Filter>Source Files\Launcher</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Resources.c">
|
||||
<Filter>Source Files\Launcher</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LWidgets.c">
|
||||
<Filter>Source Files\Launcher</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -95,8 +95,8 @@ bool Drawer2D_Clamp(Bitmap* bmp, int* x, int* y, int* width, int* height) {
|
||||
}
|
||||
#define Drawer2D_ClampPixel(p) (p < 0 ? 0 : (p > 255 ? 255 : p))
|
||||
|
||||
void Gradient_Noise(Bitmap* bmp, int x, int y, int width, int height,
|
||||
BitmapCol col, int variation) {
|
||||
void Gradient_Noise(Bitmap* bmp, BitmapCol col, int variation,
|
||||
int x, int y, int width, int height) {
|
||||
BitmapCol* dst;
|
||||
int xx, yy, n;
|
||||
float noise;
|
||||
@ -122,8 +122,8 @@ void Gradient_Noise(Bitmap* bmp, int x, int y, int width, int height,
|
||||
}
|
||||
}
|
||||
|
||||
void Gradient_Vertical(Bitmap* bmp, int x, int y, int width, int height,
|
||||
PackedCol a, PackedCol b) {
|
||||
void Gradient_Vertical(Bitmap* bmp, BitmapCol a, BitmapCol b,
|
||||
int x, int y, int width, int height) {
|
||||
BitmapCol* row, col;
|
||||
int xx, yy;
|
||||
float t;
|
||||
@ -142,8 +142,8 @@ void Gradient_Vertical(Bitmap* bmp, int x, int y, int width, int height,
|
||||
}
|
||||
}
|
||||
|
||||
void Gradient_Blend(Bitmap* bmp, int x, int y, int width, int height,
|
||||
PackedCol col, int blend) {
|
||||
void Gradient_Blend(Bitmap* bmp, BitmapCol col, int blend,
|
||||
int x, int y, int width, int height) {
|
||||
BitmapCol* dst;
|
||||
int xx, yy, t;
|
||||
if (!Drawer2D_Clamp(bmp, &x, &y, &width, &height)) return;
|
||||
|
@ -30,12 +30,12 @@ extern BitmapCol Drawer2D_Cols[DRAWER2D_MAX_COLS];
|
||||
/* Returns false if rectangle is completely outside bitmap's rectangle. */
|
||||
bool Drawer2D_Clamp(Bitmap* bmp, int* x, int* y, int* width, int* height);
|
||||
|
||||
void Gradient_Noise(Bitmap* bmp, int x, int y, int width, int height,
|
||||
BitmapCol col, int variation);
|
||||
void Gradient_Vertical(Bitmap* bmp, int x, int y, int width, int height,
|
||||
PackedCol a, PackedCol b);
|
||||
void Gradient_Blend(Bitmap* bmp, int x, int y, int width, int height,
|
||||
PackedCol col, int blend);
|
||||
void Gradient_Noise(Bitmap* bmp, BitmapCol col, int variation,
|
||||
int x, int y, int width, int height);
|
||||
void Gradient_Vertical(Bitmap* bmp, BitmapCol a, BitmapCol b,
|
||||
int x, int y, int width, int height);
|
||||
void Gradient_Blend(Bitmap* bmp, BitmapCol col, int blend,
|
||||
int x, int y, int width, int height);
|
||||
|
||||
void Drawer2D_BmpIndexed(Bitmap* bmp, int x, int y, int size,
|
||||
uint8_t* indices, BitmapCol* palette);
|
||||
|
@ -894,10 +894,9 @@ static void EnvRenderer_EnvVariableChanged(void* obj, int envVar) {
|
||||
*--------------------------------------------------EnvRenderer component--------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
static void EnvRenderer_Init(void) {
|
||||
String renderType; char renderTypeBuffer[STRING_SIZE];
|
||||
String renderType;
|
||||
int flags;
|
||||
String_InitArray(renderType, renderTypeBuffer);
|
||||
Options_Get(OPT_RENDER_TYPE, &renderType, "normal");
|
||||
Options_UNSAFE_Get(OPT_RENDER_TYPE, &renderType);
|
||||
|
||||
flags = Game_CalcRenderType(&renderType);
|
||||
if (flags == -1) flags = 0;
|
||||
|
@ -130,7 +130,7 @@ void Game_GetDefaultTexturePack(String* texPack) {
|
||||
String texPath; char texPathBuffer[STRING_SIZE];
|
||||
|
||||
String_InitArray(texPath, texPathBuffer);
|
||||
String_Format2(&texPath, "texpacks%r%s", &Directory_Separator, &game_defTexPack);
|
||||
String_Format1(&texPath, "texpacks/%s", &game_defTexPack);
|
||||
|
||||
if (File_Exists(&texPath) && !Game_ClassicMode) {
|
||||
String_AppendString(texPack, &game_defTexPack);
|
||||
@ -421,7 +421,7 @@ static void Game_LoadGuiOptions(void) {
|
||||
|
||||
Game_TabAutocomplete = Options_GetBool(OPT_TAB_AUTOCOMPLETE, false);
|
||||
Options_Get(OPT_FONT_NAME, &Game_FontName, Font_DefaultName);
|
||||
if (Game_ClassicMode) {
|
||||
if (!Game_ClassicMode) {
|
||||
Game_FontName.length = 0;
|
||||
String_AppendConst(&Game_FontName, Font_DefaultName);
|
||||
}
|
||||
@ -641,7 +641,7 @@ void Game_TakeScreenshot(void) {
|
||||
String_Format3(&filename, "screenshot_%p2-%p2-%p4", &now.Day, &now.Month, &now.Year);
|
||||
String_Format3(&filename, "-%p2-%p2-%p2.png", &now.Hour, &now.Minute, &now.Second);
|
||||
String_InitArray(path, pathBuffer);
|
||||
String_Format2(&path, "screenshots%r%s", &Directory_Separator, &filename);
|
||||
String_Format1(&path, "screenshots/%s", &filename);
|
||||
|
||||
res = Stream_CreateFile(&stream, &path);
|
||||
if (res) { Chat_LogError2(res, "creating", &path); return; }
|
||||
|
@ -74,7 +74,7 @@ void InputHandler_ScreenChanged(struct Screen* oldScreen, struct Screen* newScre
|
||||
static bool InputHandler_IsShutdown(Key key) {
|
||||
if (key == KEY_F4 && Key_IsAltPressed()) return true;
|
||||
|
||||
/* On OSX, Cmd+Q should also terminate the process. */
|
||||
/* On OSX, Cmd+Q should also terminate the process */
|
||||
#ifdef CC_BUILD_OSX
|
||||
return key == KEY_Q && Key_IsWinPressed();
|
||||
#else
|
||||
|
181
src/LWidgets.c
Normal file
181
src/LWidgets.c
Normal file
@ -0,0 +1,181 @@
|
||||
#include "LWidgets.h"
|
||||
#include "Gui.h"
|
||||
#include "Game.h"
|
||||
#include "Drawer2D.h"
|
||||
#include "Launcher.h"
|
||||
#include "ExtMath.h"
|
||||
|
||||
void LWidget_SetLocation(void* widget, uint8_t horAnchor, uint8_t verAnchor, int xOffset, int yOffset) {
|
||||
struct Widget* w = widget;
|
||||
w->HorAnchor = horAnchor; w->VerAnchor = verAnchor;
|
||||
w->XOffset = xOffset; w->YOffset = yOffset;
|
||||
LWidget_CalcPosition(widget);
|
||||
}
|
||||
|
||||
void LWidget_CalcPosition(void* widget) {
|
||||
struct LWidget* w = widget;
|
||||
w->X = Gui_CalcPos(w->HorAnchor, w->XOffset, w->Width, Game_Width);
|
||||
w->Y = Gui_CalcPos(w->VerAnchor, w->YOffset, w->Height, Game_Height);
|
||||
}
|
||||
|
||||
void LWidget_Reset(void* widget) {
|
||||
struct LWidget* w = widget;
|
||||
w->Active = false;
|
||||
w->Hidden = false;
|
||||
w->X = 0; w->Y = 0;
|
||||
w->Width = 0; w->Height = 0;
|
||||
w->HorAnchor = ANCHOR_MIN;
|
||||
w->VerAnchor = ANCHOR_MIN;
|
||||
w->XOffset = 0; w->YOffset = 0;
|
||||
|
||||
w->TabSelectable = false;
|
||||
w->OnClick = NULL;
|
||||
w->Redraw = NULL;
|
||||
}
|
||||
|
||||
|
||||
/*########################################################################################################################*
|
||||
*------------------------------------------------------ButtonWidget-------------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
#define BTN_BORDER 1
|
||||
static BitmapCol Expand(BitmapCol a, int amount) {
|
||||
int r, g, b;
|
||||
r = a.R + amount; Math_Clamp(r, 0, 255); a.R = r;
|
||||
g = a.G + amount; Math_Clamp(g, 0, 255); a.G = g;
|
||||
b = a.B + amount; Math_Clamp(b, 0, 255); a.B = b;
|
||||
return a;
|
||||
}
|
||||
|
||||
static void LButton_DrawBackground(struct LButton* w) {
|
||||
BitmapCol activeCol = BITMAPCOL_CONST(126, 136, 191, 255);
|
||||
BitmapCol inactiveCol = BITMAPCOL_CONST(111, 111, 111, 255);
|
||||
BitmapCol col;
|
||||
|
||||
if (Launcher_ClassicBackground) {
|
||||
col = w->Active ? activeCol : inactiveCol;
|
||||
Gradient_Noise(&Launcher_Framebuffer, col, 8,
|
||||
w->X + BTN_BORDER, w->Y + BTN_BORDER,
|
||||
w->Width - 2 * BTN_BORDER, w->Height - 2 * BTN_BORDER);
|
||||
} else {
|
||||
col = w->Active ? Launcher_ButtonForeActiveCol : Launcher_ButtonForeCol;
|
||||
BitmapCol top = Expand(col, 8), bottom = Expand(col, -8);
|
||||
Gradient_Vertical(&Launcher_Framebuffer, top, bottom,
|
||||
w->X + BTN_BORDER, w->Y + BTN_BORDER,
|
||||
w->Width - 2 * BTN_BORDER, w->Height - 2 * BTN_BORDER);
|
||||
}
|
||||
}
|
||||
|
||||
static void LButton_DrawBorder(struct LButton* w) {
|
||||
BitmapCol black = BITMAPCOL_CONST(0, 0, 0, 255);
|
||||
BitmapCol backCol = Launcher_ClassicBackground ? black : Launcher_ButtonBorderCol;
|
||||
|
||||
Drawer2D_Clear(&Launcher_Framebuffer, backCol,
|
||||
w->X + BTN_BORDER, w->Y,
|
||||
w->Width - 2 * BTN_BORDER, BTN_BORDER);
|
||||
Drawer2D_Clear(&Launcher_Framebuffer, backCol,
|
||||
w->X + BTN_BORDER, w->Y + w->Height - BTN_BORDER,
|
||||
w->Width - 2 * BTN_BORDER, BTN_BORDER);
|
||||
Drawer2D_Clear(&Launcher_Framebuffer, backCol,
|
||||
w->X, w->Y + BTN_BORDER,
|
||||
BTN_BORDER, w->Height - 2 * BTN_BORDER);
|
||||
Drawer2D_Clear(&Launcher_Framebuffer, backCol,
|
||||
w->X + w->Width - BTN_BORDER, w->Y + BTN_BORDER,
|
||||
BTN_BORDER, w->Height - 2 * BTN_BORDER);
|
||||
}
|
||||
|
||||
static void LButton_DrawHighlight(struct LButton* w) {
|
||||
BitmapCol activeCol = BITMAPCOL_CONST(189, 198, 255, 255);
|
||||
BitmapCol inactiveCol = BITMAPCOL_CONST(168, 168, 168, 255);
|
||||
BitmapCol highlightCol;
|
||||
|
||||
if (Launcher_ClassicBackground) {
|
||||
highlightCol = w->Active ? activeCol : inactiveCol;
|
||||
Drawer2D_Clear(&Launcher_Framebuffer, highlightCol,
|
||||
w->X + BTN_BORDER * 2, w->Y + BTN_BORDER,
|
||||
w->Width - BTN_BORDER * 4, BTN_BORDER);
|
||||
Drawer2D_Clear(&Launcher_Framebuffer, highlightCol,
|
||||
w->X + BTN_BORDER, w->Y + BTN_BORDER * 2,
|
||||
BTN_BORDER, w->Height - BTN_BORDER * 4);
|
||||
} else if (!w->Active) {
|
||||
Drawer2D_Clear(&Launcher_Framebuffer, Launcher_ButtonHighlightCol,
|
||||
w->X + BTN_BORDER * 2, w->Y + BTN_BORDER,
|
||||
w->Width - BTN_BORDER * 4, BTN_BORDER);
|
||||
}
|
||||
}
|
||||
|
||||
static void LButton_Redraw(void* widget) {
|
||||
struct DrawTextArgs args;
|
||||
struct LButton* w = widget;
|
||||
int xOffset, yOffset;
|
||||
if (w->Hidden) return;
|
||||
|
||||
xOffset = w->Width - w->__TextSize.Width;
|
||||
yOffset = w->Height - w->__TextSize.Height;
|
||||
DrawTextArgs_Make(&args, &w->Text, &w->Font, true);
|
||||
|
||||
LButton_DrawBackground(w);
|
||||
LButton_DrawBorder(w);
|
||||
LButton_DrawHighlight(w);
|
||||
|
||||
if (!w->Active) Drawer2D_Cols['f'] = Drawer2D_Cols['7'];
|
||||
Drawer2D_DrawText(&Launcher_Framebuffer, &args,
|
||||
w->X + xOffset / 2, w->Y + yOffset / 2);
|
||||
if (!w->Active) Drawer2D_Cols['f'] = Drawer2D_Cols['F'];
|
||||
}
|
||||
|
||||
void LButton_Init(struct LButton* w, int width, int height) {
|
||||
Widget_Reset(w);
|
||||
w->TabSelectable = true;
|
||||
w->Width = width; w->Height = height;
|
||||
w->Redraw = LButton_Redraw;
|
||||
String_InitArray(w->Text, w->__TextBuffer);
|
||||
}
|
||||
|
||||
void LButton_SetText(struct LButton* w, const String* text, const FontDesc* font) {
|
||||
struct DrawTextArgs args;
|
||||
w->Font = *font;
|
||||
String_Copy(&w->Text, text);
|
||||
|
||||
DrawTextArgs_Make(&args, text, font, true);
|
||||
w->__TextSize = Drawer2D_MeasureText(&args);
|
||||
}
|
||||
|
||||
|
||||
CC_NOINLINE void LInput_Init(struct LInput* w, int width, int height, const char* hintText, const FontDesc* hintFont);
|
||||
CC_NOINLINE void LInput_SetText(struct LInput* w, const String* text, const FontDesc* font);
|
||||
|
||||
|
||||
static void LLabel_Redraw(void* widget) {
|
||||
struct DrawTextArgs args;
|
||||
struct LLabel* w = widget;
|
||||
if (w->Hidden) return;
|
||||
|
||||
DrawTextArgs_Make(&args, &w->Text, &w->Font, true);
|
||||
Drawer2D_DrawText(&Launcher_Framebuffer, &args, w->X, w->Y);
|
||||
}
|
||||
|
||||
void LLabel_Init(struct LLabel* w) {
|
||||
Widget_Reset(w);
|
||||
w->Redraw = LLabel_Redraw;
|
||||
String_InitArray(w->Text, w->__TextBuffer);
|
||||
}
|
||||
|
||||
void LLabel_SetText(struct LLabel* w, const String* text, const FontDesc* font) {
|
||||
struct DrawTextArgs args;
|
||||
Size2D size;
|
||||
w->Font = *font;
|
||||
String_Copy(&w->Text, text);
|
||||
|
||||
DrawTextArgs_Make(&args, &w->Text, &w->Font, true);
|
||||
size = Drawer2D_MeasureText(&args);
|
||||
w->Width = size.Width; w->Height = size.Height;
|
||||
LWidget_CalcPosition(w);
|
||||
}
|
||||
|
||||
|
||||
static void LSlider_Redraw(void* widget) {
|
||||
struct LSlider* w = widget;
|
||||
|
||||
}
|
||||
|
||||
void LSlider_Init(struct LSlider* w, int width, int height);
|
69
src/LWidgets.h
Normal file
69
src/LWidgets.h
Normal file
@ -0,0 +1,69 @@
|
||||
#ifndef CC_LWIDGETS_H
|
||||
#define CC_LWIDGETS_H
|
||||
#include "Bitmap.h"
|
||||
#include "String.h"
|
||||
#include "Constants.h"
|
||||
/* Describes and manages individual 2D GUI elements in the launcher.
|
||||
Copyright 2014-2017 ClassicalSharp | Licensed under BSD-3
|
||||
*/
|
||||
|
||||
#define LWidget_Layout \
|
||||
int X, Y, Width, Height; /* Top left corner, and dimensions, of this widget */ \
|
||||
bool Active; /* Whether this widget is currently being moused over*/ \
|
||||
bool Hidden; /* Whether this widget is hidden from view */ \
|
||||
bool TabSelectable; /* Whether this widget gets selected when pressing tab */ \
|
||||
uint8_t HorAnchor, VerAnchor; /* Specifies the reference point for when this widget is resized */ \
|
||||
int XOffset, YOffset; /* Offset from the reference point */ \
|
||||
void (*OnClick)(void* widget, int x, int y); /* Called when widget is clicked */ \
|
||||
void (*Redraw)(void* widget); /* Called to redraw contents of this widget */
|
||||
|
||||
/* Represents an individual 2D gui component in the launcher. */
|
||||
struct LWidget { LWidget_Layout };
|
||||
void LWidget_SetLocation(void* widget, uint8_t horAnchor, uint8_t verAnchor, int xOffset, int yOffset);
|
||||
void LWidget_CalcPosition(void* widget);
|
||||
void LWidget_Reset(void* widget);
|
||||
|
||||
struct LButton {
|
||||
LWidget_Layout
|
||||
String Text;
|
||||
FontDesc Font;
|
||||
Size2D __TextSize;
|
||||
char __TextBuffer[STRING_SIZE];
|
||||
};
|
||||
CC_NOINLINE void LButton_Init(struct LButton* w, int width, int height);
|
||||
CC_NOINLINE void LButton_SetText(struct LButton* w, const String* text, const FontDesc* font);
|
||||
|
||||
struct LInput {
|
||||
LWidget_Layout
|
||||
int BaseWidth, RealWidth;
|
||||
/* Text displayed when the user has not entered anything in the text field. */
|
||||
const char* HintText;
|
||||
/* Whether all characters should be rendered as *. */
|
||||
bool Password;
|
||||
FontDesc Font, HintFont;
|
||||
String Text;
|
||||
int __TextHeight;
|
||||
char __TextBuffer[STRING_SIZE];
|
||||
};
|
||||
CC_NOINLINE void LInput_Init(struct LInput* w, int width, int height, const char* hintText, const FontDesc* hintFont);
|
||||
CC_NOINLINE void LInput_SetText(struct LInput* w, const String* text, const FontDesc* font);
|
||||
|
||||
|
||||
struct LLabel {
|
||||
LWidget_Layout
|
||||
FontDesc Font;
|
||||
String Text;
|
||||
Size2D __TextSize;
|
||||
char __TextBuffer[STRING_SIZE];
|
||||
};
|
||||
CC_NOINLINE void LLabel_Init(struct LLabel* w);
|
||||
CC_NOINLINE void LLabel_SetText(struct LLabel* w, const String* text, const FontDesc* font);
|
||||
|
||||
/* Represents a slider bar that may or may not be modifiable by the user. */
|
||||
struct LSlider {
|
||||
LWidget_Layout
|
||||
int Value, MaxValue;
|
||||
BitmapCol ProgCol;
|
||||
};
|
||||
CC_NOINLINE void LSlider_Init(struct LSlider* w, int width, int height);
|
||||
#endif
|
273
src/Launcher.c
273
src/Launcher.c
@ -3,21 +3,189 @@
|
||||
#include "Game.h"
|
||||
#include "Deflate.h"
|
||||
#include "Stream.h"
|
||||
#include "Utils.h"
|
||||
#include "Input.h"
|
||||
#include "Window.h"
|
||||
#include "GameStructs.h"
|
||||
|
||||
struct LSCreen* Launcher_Screen;
|
||||
bool Launcher_Dirty, Launcher_PendingRedraw;
|
||||
Rect2D Launcher_DirtyArea;
|
||||
Bitmap Launcher_Framebuffer;
|
||||
bool Launcher_ClassicBackground;
|
||||
bool Launcher_ClassicBackground;
|
||||
|
||||
bool Launcher_ShouldExit, Launcher_ShouldUpdate;
|
||||
TimeMS Launcher_PatchTime;
|
||||
struct ServerListEntry* Launcher_PublicServers;
|
||||
int Launcher_NumServers;
|
||||
|
||||
/* TODO: FIX THESE STUBS!!! */
|
||||
void Launcher_ShowError(ReturnCode res, const char* place) { }
|
||||
void Launcher_SecureSetOpt(const char* opt, const String* data, const String* key) { }
|
||||
|
||||
/*internal UpdateCheckTask checkTask;
|
||||
bool fullRedraw;
|
||||
|
||||
Font logoFont;
|
||||
static void Launcher_Init(void) {
|
||||
BitmapCol col = BITMAPCOL_CONST(125, 125, 125, 255);
|
||||
Window.Resize += Resize;
|
||||
Window.FocusedChanged += RedrawAll;
|
||||
Window.WindowStateChanged += Resize;
|
||||
Window.Redraw += RedrawPending;
|
||||
Keyboard.KeyDown += KeyDown;
|
||||
|
||||
Options_Load();
|
||||
Options_Get(OPT_FONT_NAME, &Game_FontName, Font_DefaultName);
|
||||
/* TODO: Handle Arial font not working */
|
||||
/*logoFont = new Font(FontName, 32, FontStyle.Regular);
|
||||
|
||||
Drawer2D_Cols['g'] = col;
|
||||
Utils_EnsureDirectory("texpacks");
|
||||
Utils_EnsureDirectory("audio");
|
||||
}
|
||||
|
||||
void Resize() {
|
||||
UpdateClientSize();
|
||||
platformDrawer.Resize();
|
||||
RedrawAll();
|
||||
}
|
||||
|
||||
void RedrawPending() {
|
||||
// in case we get multiple of these events
|
||||
pendingRedraw = true;
|
||||
Dirty = true;
|
||||
}
|
||||
|
||||
void RedrawAll() {
|
||||
RedrawBackground();
|
||||
if (Screen != null) Screen.Resize();
|
||||
fullRedraw = true;
|
||||
}
|
||||
|
||||
void SetScreen(Screen screen) {
|
||||
if (Launcher_Screen) Screen.Dispose();
|
||||
Launcher_ResetPixels();
|
||||
Screen = screen;
|
||||
screen.Init();
|
||||
// for selecting active button etc
|
||||
Screen.MouseMove(0, 0);
|
||||
}
|
||||
|
||||
void UpdateClientSize() {
|
||||
Size size = Window.ClientSize;
|
||||
Width = Math.Max(size.Width, 1);
|
||||
Height = Math.Max(size.Height, 1);
|
||||
}
|
||||
|
||||
void Run() {
|
||||
Window = Factory.CreateWindow(640, 400, Program.AppName,
|
||||
GraphicsMode.Default, DisplayDevice.Default);
|
||||
Window_SetVisible(true);
|
||||
Drawer2D_Component.Init();
|
||||
UpdateClientSize();
|
||||
|
||||
Launcher_Init();
|
||||
Launcher_TryLoadTexturePack();
|
||||
|
||||
Launcher_Framebuffer.Width = Game_Width;
|
||||
Launcher_Framebuffer.Height = Game_Height;
|
||||
Window_InitRaw(&Launcher_Framebuffer);
|
||||
|
||||
Downloader = new AsyncDownloader(Drawer);
|
||||
Downloader.Init("");
|
||||
Downloader.Cookies = new CookieContainer();
|
||||
Downloader.KeepAlive = true;
|
||||
|
||||
fetcher = new ResourceFetcher();
|
||||
fetcher.CheckResourceExistence();
|
||||
checkTask = new UpdateCheckTask();
|
||||
checkTask.RunAsync(this);
|
||||
|
||||
if (!fetcher.AllResourcesExist) {
|
||||
SetScreen(new ResourcesScreen(this));
|
||||
} else {
|
||||
SetScreen(new MainScreen(this));
|
||||
}
|
||||
|
||||
while (true) {
|
||||
Window_ProcessEvents();
|
||||
if (!Window_Exists) break;
|
||||
if (Launcher_ShouldExit) break;
|
||||
|
||||
checkTask.Tick();
|
||||
Screen.Tick();
|
||||
if (Launcher_Dirty) Launcher_Display();
|
||||
Thread_Sleep(10);
|
||||
}
|
||||
|
||||
if (Options.Load()) {
|
||||
LauncherSkin.SaveToOptions();
|
||||
Options.Save();
|
||||
}
|
||||
|
||||
if (Launcher_Screen) {
|
||||
Screen.Dispose();
|
||||
Launcher_Screen = NULL;
|
||||
}
|
||||
|
||||
if (Launcher_ShouldUpdate)
|
||||
Updater.Applier.ApplyUpdate();
|
||||
if (Window_Exists)
|
||||
Window_Close();
|
||||
}
|
||||
|
||||
void Display() {
|
||||
if (pendingRedraw) {
|
||||
RedrawAll();
|
||||
pendingRedraw = false;
|
||||
}
|
||||
|
||||
Screen.OnDisplay();
|
||||
Dirty = false;
|
||||
|
||||
Rectangle rec = new Rectangle(0, 0, Framebuffer.Width, Framebuffer.Height);
|
||||
if (!fullRedraw && DirtyArea.Width > 0) {
|
||||
rec = DirtyArea;
|
||||
}
|
||||
|
||||
Window_DrawRaw(rec);
|
||||
DirtyArea = Rectangle.Empty;
|
||||
fullRedraw = false;
|
||||
}
|
||||
|
||||
void KeyDown(Key key) {
|
||||
if (IsShutdown(key)) Launcher_ShouldExit = true;
|
||||
}
|
||||
|
||||
void Dispose() {
|
||||
Window.Resize -= Resize;
|
||||
Window.FocusedChanged -= RedrawAll;
|
||||
Window.WindowStateChanged -= Resize;
|
||||
Window.Redraw -= RedrawPending;
|
||||
Keyboard.KeyDown -= KeyDown;
|
||||
|
||||
List<FastBitmap> bitmaps = FetchFlagsTask.Bitmaps;
|
||||
for (int i = 0; i < bitmaps.Count; i++) {
|
||||
bitmaps[i].Dispose();
|
||||
bitmaps[i].Bitmap.Dispose();
|
||||
}
|
||||
logoFont.Dispose();
|
||||
}*/
|
||||
|
||||
static bool Launcher_IsShutdown(Key key) {
|
||||
if (key == KEY_F4 && Key_IsAltPressed()) return true;
|
||||
|
||||
/* On OSX, Cmd+Q should also terminate the process */
|
||||
#ifdef CC_BUILD_OSX
|
||||
return key == Key.Q && Key_IsWinPressed();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*########################################################################################################################*
|
||||
*---------------------------------------------------------Colours/Skin-----------------------------------------------------*
|
||||
*---------------------------------------------------------Colours/Skin----------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
BitmapCol Launcher_BackgroundCol = BITMAPCOL_CONST(153, 127, 172, 255);
|
||||
BitmapCol Launcher_ButtonBorderCol = BITMAPCOL_CONST( 97, 81, 110, 255);
|
||||
@ -40,13 +208,13 @@ void Launcher_ResetSkin(void) {
|
||||
Launcher_ButtonHighlightCol = defaultButtonHighlightCol;
|
||||
}
|
||||
|
||||
/*CC_NOINLINE static void Launcher_GetCol(const char* key, BitmapCol* col) {
|
||||
CC_NOINLINE static void Launcher_GetCol(const char* key, BitmapCol* col) {
|
||||
PackedCol tmp;
|
||||
string value = Options.Get(key, null);
|
||||
if (String.IsNullOrEmpty(value)) return;
|
||||
String value;
|
||||
if (!Options_UNSAFE_Get(key, &value)) return;
|
||||
if (!PackedCol_TryParseHex(&value, &tmp)) return;
|
||||
|
||||
if (!PackedCol.TryParse(value, out col))
|
||||
col = defaultCol;
|
||||
col->R = tmp.R; col->G = tmp.G; col->B = tmp.B;
|
||||
}
|
||||
|
||||
void Launcher_LoadSkin(void) {
|
||||
@ -58,12 +226,13 @@ void Launcher_LoadSkin(void) {
|
||||
}
|
||||
|
||||
CC_NOINLINE static void Launcher_SetCol(const char* key, BitmapCol col) {
|
||||
String value; char valueBuffer[8];
|
||||
PackedCol tmp;
|
||||
string value = Options.Get(key, null);
|
||||
if (String.IsNullOrEmpty(value)) return;
|
||||
|
||||
if (!PackedCol.TryParse(value, out col))
|
||||
col = defaultCol;
|
||||
tmp.R = col.R; tmp.G = col.G; tmp.B = col.B; tmp.A = 0;
|
||||
|
||||
String_InitArray(value, valueBuffer);
|
||||
PackedCol_ToHex(&value, tmp);
|
||||
Options_Set(key, &value);
|
||||
}
|
||||
|
||||
void Launcher_SaveSkin(void) {
|
||||
@ -72,7 +241,7 @@ void Launcher_SaveSkin(void) {
|
||||
Launcher_SetCol("launcher-btn-fore-active-col", Launcher_ButtonForeActiveCol);
|
||||
Launcher_SetCol("launcher-btn-fore-inactive-col", Launcher_ButtonForeCol);
|
||||
Launcher_SetCol("launcher-btn-highlight-inactive-col", Launcher_ButtonHighlightCol);
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
/*########################################################################################################################*
|
||||
@ -150,28 +319,30 @@ static void Launcher_ExtractTexturePack(const String* path) {
|
||||
stream.Close(&stream);
|
||||
}
|
||||
|
||||
/*void Launcher_TryLoadTexturePack(void) {
|
||||
if (Options.Get("nostalgia-classicbg", null) != null) {
|
||||
void Launcher_TryLoadTexturePack(void) {
|
||||
static String defZipPath = String_FromConst("texpacks/default.zip");
|
||||
String path; char pathBuffer[FILENAME_SIZE];
|
||||
String texPack;
|
||||
|
||||
if (Options_UNSAFE_Get("nostalgia-classicbg", &texPack)) {
|
||||
Launcher_ClassicBackground = Options_GetBool("nostalgia-classicbg", false);
|
||||
} else {
|
||||
Launcher_ClassicBackground = Options_GetBool(OPT_CLASSIC_MODE, false);
|
||||
}
|
||||
|
||||
string texPack = Options.Get(OptionsKey.DefaultTexturePack, "default.zip");
|
||||
string texPath = Path.Combine("texpacks", texPack);
|
||||
Options_UNSAFE_Get(OPT_DEFAULT_TEX_PACK, &texPack);
|
||||
String_InitArray(path, pathBuffer);
|
||||
String_Format1(&path, "texpacks/", &texPack);
|
||||
|
||||
if (!Platform_FileExists(texPath)) {
|
||||
texPath = Path.Combine("texpacks", "default.zip");
|
||||
}
|
||||
if (!Platform_FileExists(texPath)) return;
|
||||
if (!texPack.length || !File_Exists(&path)) path = defZipPath;
|
||||
if (!File_Exists(&path)) return;
|
||||
|
||||
Launcher_ExtractTexturePack(texPath);
|
||||
// user selected texture pack is missing some required .png files
|
||||
Launcher_ExtractTexturePack(&path);
|
||||
/* user selected texture pack is missing some required .png files */
|
||||
if (!fontBmp.Scan0 || !terrainBmp.Scan0) {
|
||||
texPath = Path.Combine("texpacks", "default.zip");
|
||||
ExtractTexturePack(texPath);
|
||||
Launcher_ExtractTexturePack(&defZipPath);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
static void Launcher_ClearTile(int x, int y, int width, int height, int srcX) {
|
||||
Drawer2D_BmpTiled(&Launcher_Framebuffer, x, y, width, height,
|
||||
@ -182,7 +353,7 @@ void Launcher_ResetArea(int x, int y, int width, int height) {
|
||||
if (Launcher_ClassicBackground && terrainBmp.Scan0) {
|
||||
Launcher_ClearTile(x, y, width, height, 0);
|
||||
} else {
|
||||
Gradient_Noise(&Launcher_Framebuffer, x, y, width, height, Launcher_BackgroundCol, 6);
|
||||
Gradient_Noise(&Launcher_Framebuffer, Launcher_BackgroundCol, 6, x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,3 +382,51 @@ void Launcher_ResetPixels(void) {
|
||||
Drawer2D_BitmappedText = false;
|
||||
Launcher_Dirty = true;
|
||||
}
|
||||
|
||||
static TimeMS lastJoin;
|
||||
bool Launcher_StartGame(const String* user, const String* mppass, const String* ip, const String* port, const String* server) {
|
||||
#ifdef CC_BUILD_WINDOWS
|
||||
static String exe = String_FromConst("ClassiCube.exe");
|
||||
#else
|
||||
static String exe = String_FromConst("ClassiCube");
|
||||
#endif
|
||||
String args; char argsBuffer[512];
|
||||
TimeMS now;
|
||||
ReturnCode res;
|
||||
|
||||
now = DateTime_CurrentUTC_MS();
|
||||
if (lastJoin + 1000 < now) return false;
|
||||
lastJoin = now;
|
||||
|
||||
/* Make sure if the client has changed some settings in the meantime, we keep the changes */
|
||||
Options_Load();
|
||||
Launcher_ShouldExit = Options_GetBool(OPT_AUTO_CLOSE_LAUNCHER, false);
|
||||
|
||||
/* Save resume info */
|
||||
if (server) {
|
||||
Options_Set("launcher-server", server);
|
||||
Options_Set("launcher-username", user);
|
||||
Options_Set("launcher-ip", ip);
|
||||
Options_Set("launcher-port", port);
|
||||
Launcher_SecureSetOpt("launcher-mppass", mppass, user);
|
||||
Options_Save();
|
||||
}
|
||||
|
||||
String_InitArray(args, argsBuffer);
|
||||
String_AppendString(&args, user);
|
||||
if (mppass->length) String_Format3(&args, "%s %s %s", mppass, ip, port);
|
||||
|
||||
res = Platform_StartProcess(&exe, &args);
|
||||
#ifdef CC_BUILD_WINDOWS
|
||||
/* TODO: Check this*/
|
||||
/* HRESULT when user clicks 'cancel' to 'are you sure you want to run ClassiCube.exe' */
|
||||
if (res == 0x80004005) return;
|
||||
#endif
|
||||
|
||||
if (res) {
|
||||
Launcher_ShowError(res, "starting game");
|
||||
Launcher_ShouldExit = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
#define CC_LAUNCHER_H
|
||||
#include "Bitmap.h"
|
||||
#include "String.h"
|
||||
|
||||
/* Implements the launcher part of the game.
|
||||
Copyright 2014-2017 ClassicalSharp | Licensed under BSD-3
|
||||
*/
|
||||
struct LScreen;
|
||||
|
||||
/* Currently active screen/menu. */
|
||||
@ -11,7 +13,7 @@ extern struct LSCreen* Launcher_Screen;
|
||||
extern bool Launcher_Dirty, Launcher_PendingRedraw;
|
||||
/* The specific area/region of the window that needs to be redrawn. */
|
||||
extern Rect2D Launcher_DirtyArea;
|
||||
/* Pixels containing contents of the window.*/
|
||||
/* Contains the pixels that are drawn to the window. */
|
||||
extern Bitmap Launcher_Framebuffer;
|
||||
/* Whether to use stone tile background like minecraft.net. */
|
||||
extern bool Launcher_ClassicBackground;
|
||||
@ -67,8 +69,12 @@ void Launcher_SetScreen(struct LScreen* screen);
|
||||
bool Launcher_ConnectToServer(const String* hash);
|
||||
/* Launcher main loop. */
|
||||
void Launcher_Run();
|
||||
|
||||
/* Shows a message box for an error. */
|
||||
void Launcher_ShowError(ReturnCode res, const char* place);
|
||||
/* Attempts to securely encode an option. */
|
||||
/* NOTE: Not all platforms support secure saving, DO NOT rely on this being secure. */
|
||||
void Launcher_SecureSetOpt(const char* opt, const String* data, const String* key);
|
||||
|
||||
/* Starts the game from the given arguments. */
|
||||
bool Launcher_StartGame(const String* user, const String* mppass, const String* ip, const String* port, const String* server);
|
||||
|
@ -1284,7 +1284,7 @@ static void SaveLevelScreen_Save(void* screen, void* widget, const char* ext) {
|
||||
SaveLevelScreen_MakeDesc(s, &fileMsg); return;
|
||||
}
|
||||
String_InitArray(path, pathBuffer);
|
||||
String_Format3(&path, "maps%r%s%c", &Directory_Separator, &file, ext);
|
||||
String_Format2(&path, "maps/%s%c", &file, ext);
|
||||
|
||||
if (File_Exists(&path) && !btn->OptName) {
|
||||
ButtonWidget_Set(btn, &overMsg, &s->TitleFont);
|
||||
@ -1387,7 +1387,7 @@ static void TexturePackScreen_EntryClick(void* screen, void* widget) {
|
||||
|
||||
filename = ListScreen_UNSAFE_GetCur(s, widget);
|
||||
String_InitArray(path, pathBuffer);
|
||||
String_Format2(&path, "texpacks%r%s", &Directory_Separator, &filename);
|
||||
String_Format1(&path, "texpacks/%s", &filename);
|
||||
if (!File_Exists(&path)) return;
|
||||
|
||||
idx = s->CurrentIndex;
|
||||
@ -1573,7 +1573,7 @@ static void LoadLevelScreen_EntryClick(void* screen, void* widget) {
|
||||
|
||||
filename = ListScreen_UNSAFE_GetCur(s, widget);
|
||||
String_InitArray(path, pathBuffer);
|
||||
String_Format2(&path, "maps%r%s", &Directory_Separator, &filename);
|
||||
String_Format1(&path, "maps/%s", &filename);
|
||||
|
||||
if (!File_Exists(&path)) return;
|
||||
Map_LoadFrom(&path);
|
||||
|
@ -31,7 +31,7 @@ bool Options_HasChanged(const String* key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool Options_TryGetValue(const char* keyRaw, String* value) {
|
||||
bool Options_UNSAFE_Get(const char* keyRaw, String* value) {
|
||||
int idx;
|
||||
String key = String_FromReadonly(keyRaw);
|
||||
|
||||
@ -50,7 +50,7 @@ static bool Options_TryGetValue(const char* keyRaw, String* value) {
|
||||
|
||||
void Options_Get(const char* key, String* value, const char* defValue) {
|
||||
String str;
|
||||
Options_TryGetValue(key, &str);
|
||||
Options_UNSAFE_Get(key, &str);
|
||||
value->length = 0;
|
||||
|
||||
if (str.length) {
|
||||
@ -63,7 +63,7 @@ void Options_Get(const char* key, String* value, const char* defValue) {
|
||||
int Options_GetInt(const char* key, int min, int max, int defValue) {
|
||||
String str;
|
||||
int value;
|
||||
if (!Options_TryGetValue(key, &str)) return defValue;
|
||||
if (!Options_UNSAFE_Get(key, &str)) return defValue;
|
||||
if (!Convert_TryParseInt(&str, &value)) return defValue;
|
||||
|
||||
Math_Clamp(value, min, max);
|
||||
@ -73,7 +73,7 @@ int Options_GetInt(const char* key, int min, int max, int defValue) {
|
||||
bool Options_GetBool(const char* key, bool defValue) {
|
||||
String str;
|
||||
bool value;
|
||||
if (!Options_TryGetValue(key, &str)) return defValue;
|
||||
if (!Options_UNSAFE_Get(key, &str)) return defValue;
|
||||
if (!Convert_TryParseBool(&str, &value)) return defValue;
|
||||
|
||||
return value;
|
||||
@ -82,7 +82,7 @@ bool Options_GetBool(const char* key, bool defValue) {
|
||||
float Options_GetFloat(const char* key, float min, float max, float defValue) {
|
||||
String str;
|
||||
float value;
|
||||
if (!Options_TryGetValue(key, &str)) return defValue;
|
||||
if (!Options_UNSAFE_Get(key, &str)) return defValue;
|
||||
if (!Convert_TryParseFloat(&str, &value)) return defValue;
|
||||
|
||||
Math_Clamp(value, min, max);
|
||||
@ -91,7 +91,7 @@ float Options_GetFloat(const char* key, float min, float max, float defValue) {
|
||||
|
||||
int Options_GetEnum(const char* key, int defValue, const char** names, int namesCount) {
|
||||
String str;
|
||||
if (!Options_TryGetValue(key, &str)) return defValue;
|
||||
if (!Options_UNSAFE_Get(key, &str)) return defValue;
|
||||
return Utils_ParseEnum(&str, defValue, names, namesCount);
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,9 @@ bool Options_HasAnyChanged(void);
|
||||
/* Frees any memory allocated in storing options. */
|
||||
void Options_Free(void);
|
||||
|
||||
/* Sets value to value of option directly in Options.Buffer if found, String_Empty if not. */
|
||||
/* Returns whether the option was actually found. */
|
||||
STRING_REF bool Options_UNSAFE_Get(const char* keyRaw, String* value);
|
||||
/* Returns value of given option, or defalt value if not found. */
|
||||
CC_EXPORT void Options_Get(const char* key, String* value, const char* defValue);
|
||||
/* Returns value of given option as an integer, or defalt value if could not be converted. */
|
||||
|
@ -44,7 +44,6 @@ void* DisplayDevice_Meta;
|
||||
|
||||
static HANDLE heap;
|
||||
char* Platform_NewLine = "\r\n";
|
||||
char Directory_Separator = '\\';
|
||||
char* Font_DefaultName = "Arial";
|
||||
|
||||
const ReturnCode ReturnCode_FileShareViolation = ERROR_SHARING_VIOLATION;
|
||||
@ -77,7 +76,6 @@ const ReturnCode ReturnCode_SocketWouldBlock = WSAEWOULDBLOCK;
|
||||
#define Nix_Return(success) ((success) ? 0 : errno)
|
||||
|
||||
char* Platform_NewLine = "\n";
|
||||
char Directory_Separator = '/'; /* TODO: Is this right for old OSX though?? */
|
||||
pthread_mutex_t event_mutex;
|
||||
|
||||
const ReturnCode ReturnCode_FileShareViolation = 1000000000; /* TODO: not used apparently */
|
||||
@ -412,7 +410,7 @@ ReturnCode Directory_Enum(const String* dirPath, void* obj, Directory_EnumCallba
|
||||
|
||||
do {
|
||||
path.length = 0;
|
||||
String_Format2(&path, "%s%r", dirPath, &Directory_Separator);
|
||||
String_Format1(&path, "%s/", dirPath);
|
||||
|
||||
/* ignore . and .. entry */
|
||||
TCHAR* src = entry.cFileName;
|
||||
@ -545,7 +543,7 @@ ReturnCode Directory_Enum(const String* dirPath, void* obj, Directory_EnumCallba
|
||||
|
||||
while ((entry = readdir(dirPtr))) {
|
||||
path.length = 0;
|
||||
String_Format2(&path, "%s%r", dirPath, &Directory_Separator);
|
||||
String_Format1(&path, "%s/", dirPath);
|
||||
|
||||
/* ignore . and .. entry */
|
||||
src = entry->d_name;
|
||||
@ -1587,9 +1585,9 @@ ReturnCode Audio_Free(AudioHandle handle) {
|
||||
ReturnCode res;
|
||||
ctx = &Audio_Contexts[handle];
|
||||
|
||||
if (!ctx->Count) return 0;
|
||||
ctx->Count = 0;
|
||||
ctx->Format = fmt;
|
||||
if (!ctx->Handle) return 0;
|
||||
|
||||
res = waveOutClose(ctx->Handle);
|
||||
ctx->Handle = NULL;
|
||||
@ -1637,6 +1635,7 @@ ReturnCode Audio_Play(AudioHandle handle) { return 0; }
|
||||
|
||||
ReturnCode Audio_Stop(AudioHandle handle) {
|
||||
struct AudioContext* ctx = &Audio_Contexts[handle];
|
||||
if (!ctx->Handle) return 0;
|
||||
return waveOutReset(ctx->Handle);
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,6 @@ enum File_SeekFrom { FILE_SEEKFROM_BEGIN, FILE_SEEKFROM_CURRENT, FILE_SEEKFROM_E
|
||||
|
||||
/* Newline for console and text files. */
|
||||
extern char* Platform_NewLine;
|
||||
/* Character in a path that distinguishes directories. (usually / or \) */
|
||||
extern char Directory_Separator;
|
||||
/* Name of default system font used. (e.g. Arial) */
|
||||
extern char* Font_DefaultName;
|
||||
extern const ReturnCode ReturnCode_FileShareViolation;
|
||||
|
@ -40,8 +40,8 @@ int main_imdct() {
|
||||
#endif
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
String defPath; char defPathBuffer[STRING_SIZE];
|
||||
String title; char titleBuffer[STRING_SIZE];
|
||||
static String defPath = String_FromConst("texpacks/default.zip");
|
||||
String title; char titleBuffer[STRING_SIZE];
|
||||
String args[PROGRAM_MAX_CMDARGS];
|
||||
int argsCount;
|
||||
|
||||
@ -67,9 +67,6 @@ int main(int argc, char** argv) {
|
||||
Utils_EnsureDirectory("texpacks");
|
||||
Utils_EnsureDirectory("texturecache");
|
||||
|
||||
String_InitArray(defPath, defPathBuffer);
|
||||
String_Format1(&defPath, "texpacks%rdefault.zip", &Directory_Separator);
|
||||
|
||||
if (!File_Exists(&defPath)) {
|
||||
Window_ShowDialog("Missing file",
|
||||
"default.zip is missing, try running launcher first.\n\nThe game will still run, but without any textures");
|
||||
|
182
src/Resources.c
Normal file
182
src/Resources.c
Normal file
@ -0,0 +1,182 @@
|
||||
#include "Resources.h"
|
||||
#include "Funcs.h"
|
||||
#include "String.h"
|
||||
#include "Constants.h"
|
||||
#include "Deflate.h"
|
||||
#include "Stream.h"
|
||||
#include "Platform.h"
|
||||
#include "Launcher.h"
|
||||
#include "Utils.h"
|
||||
|
||||
|
||||
/*########################################################################################################################*
|
||||
*---------------------------------------------------------List/Checker----------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
bool DigSoundsExist, StepSoundsExist;
|
||||
int Resources_Size, Resources_Count;
|
||||
|
||||
static void Resources_CheckFiles(void) {
|
||||
int flags = Resources_GetFetchFlags();
|
||||
if (flags & FLAG_CLASSIC) { Resources_Size += 291; Resources_Count++; }
|
||||
if (flags & FLAG_MODERN) { Resources_Size += 4621; Resources_Count++; }
|
||||
if (flags & FLAG_TERRAIN) { Resources_Size += 7; Resources_Count++; }
|
||||
if (flags & FLAG_GUI) { Resources_Size += 21; Resources_Count++; }
|
||||
}
|
||||
|
||||
static void Resources_CheckMusic(void) {
|
||||
String path; char pathBuffer[FILENAME_SIZE];
|
||||
int i = 0;
|
||||
|
||||
String_InitArray(path, pathBuffer);
|
||||
for (i = 0; i < Array_Elems(Resources_Music); i++) {
|
||||
path.length = 0;
|
||||
String_Format1(&path, "audio/%c", Resources_Music[i].Name);
|
||||
|
||||
Resources_Music[i].Exists = File_Exists(&path);
|
||||
if (Resources_Music[i].Exists) continue;
|
||||
|
||||
Resources_Size += Resources_Music[i].Size;
|
||||
Resources_Count++;
|
||||
}
|
||||
}
|
||||
|
||||
CC_NOINLINE static bool Resources_CheckExist(const char* prefix, struct ResourceSound* sounds, int count) {
|
||||
String path; char pathBuffer[FILENAME_SIZE];
|
||||
int i = 0;
|
||||
|
||||
String_InitArray(path, pathBuffer);
|
||||
for (i = 0; i < count; i++) {
|
||||
path.length = 0;
|
||||
String_Format2(&path, "audio/%c_%c", prefix, sounds[i].Name);
|
||||
|
||||
if (!File_Exists(&path)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void Resources_CheckSounds(void) {
|
||||
DigSoundsExist = Resources_CheckExist("dig", Resources_Dig, Array_Elems(Resources_Dig));
|
||||
if (!DigSoundsExist) {
|
||||
Resources_Count += Array_Elems(Resources_Dig);
|
||||
Resources_Size += 173;
|
||||
}
|
||||
|
||||
StepSoundsExist = Resources_CheckExist("step", Resources_Step, Array_Elems(Resources_Step));
|
||||
if (!StepSoundsExist) {
|
||||
Resources_Count += Array_Elems(Resources_Step);
|
||||
Resources_Size += 244;
|
||||
}
|
||||
}
|
||||
|
||||
static bool Resources_SelectZipEntry(const String* path) {
|
||||
String name;
|
||||
int i;
|
||||
|
||||
name = *path;
|
||||
Utils_UNSAFE_GetFilename(&name);
|
||||
for (i = 0; i < Array_Elems(Resources_Files); i++) {
|
||||
if (Resources_Files[i].Exists) continue;
|
||||
|
||||
if (!String_CaselessEqualsConst(&name, Resources_Files[i].Filename)) continue;
|
||||
Resources_Files[i].Exists = true;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void Resources_CheckDefaultZip(void) {
|
||||
static String path = String_FromConst("texpacks/default.zip");
|
||||
struct Stream stream;
|
||||
struct ZipState state;
|
||||
ReturnCode res;
|
||||
|
||||
if (!File_Exists(&path)) return;
|
||||
res = Stream_OpenFile(&stream, &path);
|
||||
|
||||
if (res) { Launcher_ShowError(res, "checking default.zip"); return; }
|
||||
Zip_Init(&state, &stream);
|
||||
state.SelectEntry = Resources_SelectZipEntry;
|
||||
|
||||
res = Zip_Extract(&state);
|
||||
stream.Close(&stream);
|
||||
if (res) { Launcher_ShowError(res, "inspecting default.zip"); }
|
||||
}
|
||||
|
||||
int Resources_GetFetchFlags(void) {
|
||||
int flags = 0, i;
|
||||
for (i = 0; i < Array_Elems(Resources_Files); i++) {
|
||||
if (Resources_Files[i].Exists) continue;
|
||||
|
||||
flags |= Resources_Files[i].Flags;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
void Resources_CheckExistence(void) {
|
||||
Resources_CheckDefaultZip();
|
||||
Resources_CheckFiles();
|
||||
Resources_CheckMusic();
|
||||
Resources_CheckSounds();
|
||||
}
|
||||
|
||||
struct ResourceFile Resources_Files[19] = {
|
||||
/* classic jar files */
|
||||
{ "char.png", FLAG_CLASSIC }, { "clouds.png", FLAG_CLASSIC },
|
||||
{ "default.png", FLAG_CLASSIC }, { "particles.png", FLAG_CLASSIC },
|
||||
{ "rain.png", FLAG_CLASSIC }, { "gui_classic.png", FLAG_CLASSIC },
|
||||
{ "icons.png", FLAG_CLASSIC },
|
||||
{ "terrain.png", FLAG_CLASSIC | FLAG_TERRAIN | FLAG_MODERN },
|
||||
{ "creeper.png", FLAG_CLASSIC }, { "pig.png", FLAG_CLASSIC },
|
||||
{ "sheep.png", FLAG_CLASSIC }, { "sheep_fur.png", FLAG_CLASSIC },
|
||||
{ "skeleton.png", FLAG_CLASSIC }, { "spider.png", FLAG_CLASSIC },
|
||||
{ "zombie.png", FLAG_CLASSIC }, /* "arrows.png", "sign.png" */
|
||||
/* other files */
|
||||
{ "snow.png", FLAG_MODERN }, { "chicken.png", FLAG_MODERN },
|
||||
{ "animations.png", FLAG_MODERN }, { "gui.png", FLAG_GUI }
|
||||
};
|
||||
|
||||
struct ResourceSound Resources_Dig[31] = {
|
||||
{ "cloth1", "5f/5fd568d724ba7d53911b6cccf5636f859d2662e8" }, { "cloth2", "56/56c1d0ac0de2265018b2c41cb571cc6631101484" },
|
||||
{ "cloth3", "9c/9c63f2a3681832dc32d206f6830360bfe94b5bfc" }, { "cloth4", "55/55da1856e77cfd31a7e8c3d358e1f856c5583198" },
|
||||
{ "grass1", "41/41cbf5dd08e951ad65883854e74d2e034929f572" }, { "grass2", "86/86cb1bb0c45625b18e00a64098cd425a38f6d3f2" },
|
||||
{ "grass3", "f7/f7d7e5c7089c9b45fa5d1b31542eb455fad995db" }, { "grass4", "c7/c7b1005d4926f6a2e2387a41ab1fb48a72f18e98" },
|
||||
{ "gravel1", "e8/e8b89f316f3e9989a87f6e6ff12db9abe0f8b09f" }, { "gravel2", "c3/c3b3797d04cb9640e1d3a72d5e96edb410388fa3" },
|
||||
{ "gravel3", "48/48f7e1bb098abd36b9760cca27b9d4391a23de26" }, { "gravel4", "7b/7bf3553a4fe41a0078f4988a13d6e1ed8663ef4c" },
|
||||
{ "sand1", "9e/9e59c3650c6c3fc0a475f1b753b2fcfef430bf81" }, { "sand2", "0f/0fa4234797f336ada4e3735e013e44d1099afe57" },
|
||||
{ "sand3", "c7/c75589cc0087069f387de127dd1499580498738e" }, { "sand4", "37/37afa06f97d58767a1cd1382386db878be1532dd" },
|
||||
{ "snow1", "e9/e9bab7d3d15541f0aaa93fad31ad37fd07e03a6c" }, { "snow2", "58/5887d10234c4f244ec5468080412f3e6ef9522f3" },
|
||||
{ "snow3", "a4/a4bc069321a96236fde04a3820664cc23b2ea619" }, { "snow4", "e2/e26fa3036cdab4c2264ceb19e1cd197a2a510227" },
|
||||
{ "stone1", "4e/4e094ed8dfa98656d8fec52a7d20c5ee6098b6ad" }, { "stone2", "9c/9c92f697142ae320584bf64c0d54381d59703528" },
|
||||
{ "stone3", "8f/8f23c02475d388b23e5faa680eafe6b991d7a9d4" }, { "stone4", "36/363545a76277e5e47538b2dd3a0d6aa4f7a87d34" },
|
||||
{ "wood1", "9b/9bc2a84d0aa98113fc52609976fae8fc88ea6333" }, { "wood2", "98/98102533e6085617a2962157b4f3658f59aea018" },
|
||||
{ "wood3", "45/45b2aef7b5049e81b39b58f8d631563fadcc778b" }, { "wood4", "dc/dc66978374a46ab2b87db6472804185824868095" },
|
||||
{ "glass1", "72/7274a2231ed4544a37e599b7b014e589e5377094" }, { "glass2", "87/87c47bda3645c68f18a49e83cbf06e5302d087ff" },
|
||||
{ "glass3", "ad/ad7d770b7fff3b64121f75bd60cecfc4866d1cd6" }
|
||||
};
|
||||
|
||||
struct ResourceSound Resources_Step[28] = {
|
||||
{ "cloth1", "5f/5fd568d724ba7d53911b6cccf5636f859d2662e8" }, { "cloth2", "56/56c1d0ac0de2265018b2c41cb571cc6631101484" },
|
||||
{ "cloth3", "9c/9c63f2a3681832dc32d206f6830360bfe94b5bfc" }, { "cloth4", "55/55da1856e77cfd31a7e8c3d358e1f856c5583198" },
|
||||
{ "grass1", "22/227ab99bf7c6cf0b2002e0f7957d0ff7e5cb0c96" }, { "grass2", "5c/5c971029d9284676dce1dda2c9d202f8c47163b2" },
|
||||
{ "grass3", "76/76de0a736928eac5003691d73bdc2eda92116198" }, { "grass4", "bc/bc28801224a0aa77fdc93bb7c6c94beacdf77331" },
|
||||
{ "gravel1", "1d/1d761cb3bcb45498719e4fba0751e1630e134f1a" }, { "gravel2", "ac/ac7a7c8d106e26abc775b1b46150c083825d8ddc" },
|
||||
{ "gravel3", "c1/c109b985a7e6d5d3828c92e00aefa49deca0eb8c" }, { "gravel4", "a4/a47adece748059294c5f563c0fcac02fa0e4bab4" },
|
||||
{ "sand1", "98/9813c8185197f4a4296649f27a9d738c4a6dfc78" }, { "sand2", "bd/bd1750c016f6bab40934eff0b0fb64c41c86e44b" },
|
||||
{ "sand3", "ab/ab07279288fa49215bada5c17627e6a54ad0437c" }, { "sand4", "a4/a474236fb0c75bd65a6010e87dbc000d345fc185" },
|
||||
{ "snow1", "e9/e9bab7d3d15541f0aaa93fad31ad37fd07e03a6c" }, { "snow2", "58/5887d10234c4f244ec5468080412f3e6ef9522f3" },
|
||||
{ "snow3", "a4/a4bc069321a96236fde04a3820664cc23b2ea619" }, { "snow4", "e2/e26fa3036cdab4c2264ceb19e1cd197a2a510227" },
|
||||
{ "stone1", "4a/4a2e3795ffd4d3aab0834b7e41903af3a8f7d197" }, { "stone2", "22/22a383d9c22342305a4f16fec0bb479a885f8da2" },
|
||||
{ "stone3", "a5/a533e7ae975e62592de868e0d0572778614bd587" }, { "stone4", "d5/d5218034051a13322d7b5efc0b5a9af739615f04" },
|
||||
{ "wood1", "af/afb01196e2179e3b15b48f3373cea4c155d56b84" }, { "wood2", "1e/1e82a43c30cf8fcbe05d0bc2760ecba5d2320314" },
|
||||
{ "wood3", "27/27722125968ac60c0f191a961b17e406f1351c6e" }, { "wood4", "29/29586f60bfe6f521dbc748919d4f0dc5b28beefd" }
|
||||
};
|
||||
|
||||
struct ResourceMusic Resources_Music[7] = {
|
||||
{ "calm1.ogg", "50/50a59a4f56e4046701b758ddbb1c1587efa4cadf", 2472 },
|
||||
{ "calm2.ogg", "74/74da65c99aa578486efa7b69983d3533e14c0d6e", 1931 },
|
||||
{ "calm3.ogg", "14/14ae57a6bce3d4254daa8be2b098c2d99743cc3f", 2181 },
|
||||
{ "hal1.ogg", "df/df1ff11b79757432c5c3f279e5ecde7b63ceda64", 1926 },
|
||||
{ "hal2.ogg", "ce/ceaaaa1d57dfdfbb0bd4da5ea39628b42897a687", 1714 },
|
||||
{ "hal3.ogg", "dd/dd85fb564e96ee2dbd4754f711ae9deb08a169f9", 1879 },
|
||||
{ "hal4.ogg", "5e/5e7d63e75c6e042f452bc5e151276911ef92fed8", 2499 }
|
||||
};
|
37
src/Resources.h
Normal file
37
src/Resources.h
Normal file
@ -0,0 +1,37 @@
|
||||
#ifndef CC_RESOURCES_H
|
||||
#define CC_RESOURCES_H
|
||||
#include "Core.h"
|
||||
/* Implements checking, fetching, and patching the default game assets.
|
||||
Copyright 2014-2017 ClassicalSharp | Licensed under BSD-3
|
||||
*/
|
||||
|
||||
#define FLAG_CLASSIC 0x01 /* file depends on classic.jar */
|
||||
#define FLAG_MODERN 0x02 /* file depends on modern jar */
|
||||
#define FLAG_GUI 0x04 /* file depends on patched gui.png */
|
||||
#define FLAG_TERRAIN 0x08 /* file depends on patched terrain.png */
|
||||
|
||||
extern struct ResourceFile {
|
||||
const char* Filename;
|
||||
uint8_t Flags;
|
||||
bool Exists;
|
||||
} Resources_Files[19];
|
||||
|
||||
extern bool DigSoundsExist, StepSoundsExist;
|
||||
/* Number and total size of resources that need to be downloaded. */
|
||||
extern int Resources_Size, Resources_Count;
|
||||
/* Returns flags of files that need to be fetched. */
|
||||
int Resources_GetFetchFlags(void);
|
||||
|
||||
extern struct ResourceSound {
|
||||
const char* Name;
|
||||
const char* Hash;
|
||||
} Resources_Dig[31], Resources_Step[28];
|
||||
|
||||
extern struct ResourceMusic {
|
||||
const char* Name;
|
||||
const char* Hash;
|
||||
uint16_t Size;
|
||||
bool Exists;
|
||||
} Resources_Music[7];
|
||||
|
||||
#endif
|
@ -143,6 +143,7 @@ int PingList_AveragePingMs(void) {
|
||||
/*########################################################################################################################*
|
||||
*-------------------------------------------------Singleplayer connection-------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
#define SP_HasDir(path) (String_IndexOf(&path, '/', 0) >= 0 || String_IndexOf(&path, '\\', 0) >= 0)
|
||||
static void SPConnection_BeginConnect(void) {
|
||||
static String logName = String_FromConst("Singleplayer");
|
||||
String path;
|
||||
@ -161,7 +162,7 @@ static void SPConnection_BeginConnect(void) {
|
||||
|
||||
/* For when user drops a map file onto ClassiCube.exe */
|
||||
path = Game_Username;
|
||||
if (String_IndexOf(&path, Directory_Separator, 0) >= 0 && File_Exists(&path)) {
|
||||
if (SP_HasDir(path) && File_Exists(&path)) {
|
||||
Map_LoadFrom(&path);
|
||||
Gui_CloseActive();
|
||||
return;
|
||||
|
@ -267,12 +267,12 @@ static void Animations_Apply(struct AnimationData* data) {
|
||||
}
|
||||
|
||||
static bool Animations_IsDefaultZip(void) {
|
||||
String texPack; char texPackBuffer[STRING_SIZE];
|
||||
String texPack;
|
||||
bool optExists;
|
||||
if (World_TextureUrl.length) return false;
|
||||
|
||||
String_InitArray(texPack, texPackBuffer);
|
||||
Options_Get(OPT_DEFAULT_TEX_PACK, &texPack, "default.zip");
|
||||
return String_CaselessEqualsConst(&texPack, "default.zip");
|
||||
optExists = Options_UNSAFE_Get(OPT_DEFAULT_TEX_PACK, &texPack);
|
||||
return !optExists || String_CaselessEqualsConst(&texPack, "default.zip");
|
||||
}
|
||||
|
||||
static void Animations_Clear(void) {
|
||||
@ -497,16 +497,15 @@ void Atlas_Free(void) {
|
||||
/*########################################################################################################################*
|
||||
*------------------------------------------------------TextureCache-------------------------------------------------------*
|
||||
*#########################################################################################################################*/
|
||||
#define TEXCACHE_FOLDER "texturecache"
|
||||
/* Because I didn't store milliseconds in original C# client */
|
||||
#define TEXCACHE_TICKS_PER_MS 10000
|
||||
static struct EntryList cache_accepted, cache_denied, cache_eTags, cache_lastModified;
|
||||
|
||||
void TextureCache_Init(void) {
|
||||
EntryList_Init(&cache_accepted, TEXCACHE_FOLDER, "acceptedurls.txt", ' ');
|
||||
EntryList_Init(&cache_denied, TEXCACHE_FOLDER, "deniedurls.txt", ' ');
|
||||
EntryList_Init(&cache_eTags, TEXCACHE_FOLDER, "etags.txt", ' ');
|
||||
EntryList_Init(&cache_lastModified, TEXCACHE_FOLDER, "lastmodified.txt", ' ');
|
||||
EntryList_Init(&cache_accepted, "texturecache", "acceptedurls.txt", ' ');
|
||||
EntryList_Init(&cache_denied, "texturecache", "deniedurls.txt", ' ');
|
||||
EntryList_Init(&cache_eTags, "texturecache", "etags.txt", ' ');
|
||||
EntryList_Init(&cache_lastModified, "texturecache", "lastmodified.txt", ' ');
|
||||
}
|
||||
|
||||
bool TextureCache_HasAccepted(const String* url) { return EntryList_Find(&cache_accepted, url) >= 0; }
|
||||
@ -526,7 +525,7 @@ CC_NOINLINE static void TextureCache_MakePath(String* path, const String* url) {
|
||||
String_InitArray(key, keyBuffer);
|
||||
|
||||
String_AppendUInt32(&key, Utils_CRC32(url->buffer, url->length));
|
||||
String_Format2(path, TEXCACHE_FOLDER "%r%s", &Directory_Separator, &key);
|
||||
String_Format1(path, "texturecache/%s", &key);
|
||||
}
|
||||
|
||||
bool TextureCache_Has(const String* url) {
|
||||
@ -589,7 +588,7 @@ void TextureCache_Set(const String* url, uint8_t* data, uint32_t length) {
|
||||
|
||||
String_InitArray(path, pathBuffer);
|
||||
TextureCache_MakePath(&path, url);
|
||||
if (!Utils_EnsureDirectory(TEXCACHE_FOLDER)) return;
|
||||
if (!Utils_EnsureDirectory("texturecache")) return;
|
||||
|
||||
res = Stream_CreateFile(&stream, &path);
|
||||
if (res) { Chat_LogError2(res, "creating cache for", &path); return; }
|
||||
@ -650,7 +649,7 @@ void TexturePack_ExtractZip_File(const String* filename) {
|
||||
ReturnCode res;
|
||||
|
||||
String_InitArray(path, pathBuffer);
|
||||
String_Format2(&path, "texpacks%r%s", &Directory_Separator, filename);
|
||||
String_Format1(&path, "texpacks/%s", filename);
|
||||
|
||||
res = Stream_OpenFile(&stream, &path);
|
||||
if (res) { Chat_LogError2(res, "opening", &path); return; }
|
||||
|
@ -241,7 +241,7 @@ void EntryList_Load(struct EntryList* list, EntryList_Filter filter) {
|
||||
|
||||
String_InitArray(path, pathBuffer);
|
||||
if (list->Folder) {
|
||||
String_Format3(&path, "%c%r%c", list->Folder, &Directory_Separator, list->Filename);
|
||||
String_Format2(&path, "%c/c", list->Folder, list->Filename);
|
||||
} else {
|
||||
String_AppendConst(&path, list->Filename);
|
||||
}
|
||||
@ -281,7 +281,7 @@ void EntryList_Save(struct EntryList* list) {
|
||||
|
||||
String_InitArray(path, pathBuffer);
|
||||
if (list->Folder) {
|
||||
String_Format3(&path, "%c%r%c", list->Folder, &Directory_Separator, list->Filename);
|
||||
String_Format2(&path, "%c/%c", list->Folder, list->Filename);
|
||||
if (!Utils_EnsureDirectory(list->Folder)) return;
|
||||
} else {
|
||||
String_AppendConst(&path, list->Filename);
|
||||
|
Loading…
x
Reference in New Issue
Block a user