Compare commits

..

1 Commits

Author SHA1 Message Date
3bb961448d feat(render): add game rendering 2025-12-27 18:36:03 -03:00
11 changed files with 187 additions and 708 deletions

View File

@ -5,7 +5,6 @@ set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
find_package(SDL3 CONFIG REQUIRED)
find_package(SDL3_net REQUIRED)
set(IMGUI_DIR "thirdparty/imgui")
set(BINDINGS_DIR "thirdparty/dear_bindings")
@ -21,6 +20,8 @@ set(IMGUI_SOURCES
${BINDINGS_DIR}/dcimgui.cpp
${BINDINGS_DIR}/dcimgui_impl_sdl3.cpp
${BINDINGS_DIR}/dcimgui_impl_sdlrenderer3.cpp
gui.c
gui.h
)
add_executable(tibia
@ -41,12 +42,7 @@ add_executable(tibia
bitmap.c
bitmap.h
objects.c
objects.h
network.c
network.h
gui.c
gui.h
)
objects.h)
target_include_directories(tibia PRIVATE
${IMGUI_DIR}
@ -54,4 +50,4 @@ target_include_directories(tibia PRIVATE
${BINDINGS_DIR}
)
target_link_libraries(tibia SDL3::SDL3 SDL3_net::SDL3_net)
target_link_libraries(tibia SDL3::SDL3)

14
app.c
View File

@ -2,6 +2,7 @@
#include "window.h"
#include "render.h"
#include "input.h"
#include "gui.h"
void App_Init(App_t *app) {
app->isRunning = false;
@ -12,8 +13,7 @@ void App_Init(App_t *app) {
System_QueryMetrics(&app->metrics);
// TODO: start this window maximized
if (!Window_Init(800, 600)) {
if (!Window_Init(app->metrics.screenWidth, app->metrics.screenHeight)) {
return;
}
@ -28,14 +28,13 @@ void App_Init(App_t *app) {
return;
}
if (!Objects_Init(&app->graphics)) {
if (!Objects_Init(&app->objects)) {
return;
}
if (!Network_Init(&app->network)) {
return;
}
Map_LoadSampleData();
app->isInGame = true;
app->isRunning = true;
}
@ -47,8 +46,7 @@ void App_Run(App_t *app) {
}
void App_Shutdown(const App_t* app) {
Network_Shutdown(&app->network);
Objects_Destroy(&app->graphics);
Objects_Destroy(&app->objects);
Map_Destroy(&app->map);
Bitmap_Destroy(&app->bitmap);
Gui_Shutdown();

5
app.h
View File

@ -9,17 +9,16 @@
#include "objects.h"
#include "map.h"
#include "system.h"
#include "network.h"
typedef struct App {
bool isRunning;
bool isInGame;
SystemMetrics_t metrics;
ConfigParams_t configParams;
Map_t map;
Bitmap_t bitmap;
Objects_t graphics;
Objects_t objects;
Gui_t gui;
Network_t network;
} App_t;
void App_Init(App_t* app);

448
gui.c
View File

@ -5,8 +5,6 @@
#include "gui.h"
#include <float.h>
#include <stdio.h>
#include "dcimgui.h"
#include "dcimgui_impl_sdl3.h"
#include "dcimgui_impl_sdlrenderer3.h"
@ -14,14 +12,6 @@
#include "window.h"
#include "config.h"
#define BASE_BODY_SPRITE_ID 155
#define BASE_HEAD_SPRITE_ID 176
#define BASE_LEGS_SPRITE_ID 158
#define BASE_SHOES_SPRITE_ID 161
static SDL_Texture *cachedCharPreview = NULL;
Character_t character = {};
void Gui_Init(Gui_t *gui) {
ImGui_CreateContext(NULL);
@ -37,9 +27,6 @@ void Gui_Init(Gui_t *gui) {
cImGui_ImplSDLRenderer3_Init(renderer);
gui->isPreferencesDialogOpen = false;
gui->isNewGameDialogOpen = false;
gui->isJourneyOnwardDialogOpen = false;
Gui_UpdateStatusBar(gui, "Welcome to Tibia!");
}
void Gui_ProcessEvent(const SDL_Event *event) {
@ -52,13 +39,9 @@ void Gui_StartRender() {
ImGui_NewFrame();
}
void Gui_Render(Gui_t *gui, const Objects_t *objects, Network_t *network, ConfigParams_t *configParams) {
Gui_RenderMainMenu(gui);
Gui_RenderStatusBar(gui);
void Gui_Render(Gui_t *gui, ConfigParams_t *configParams, const bool inGame) {
Gui_RenderMainMenu(gui, inGame);
Gui_RenderPreferences_Dialog(gui, configParams);
Gui_RenderNewGame_Dialog(gui, objects, configParams, network);
Gui_RenderJourneyOnward_Dialog(gui, configParams, network);
}
void Gui_FinishRender() {
@ -72,10 +55,9 @@ void Gui_Shutdown() {
cImGui_ImplSDLRenderer3_Shutdown();
cImGui_ImplSDL3_Shutdown();
ImGui_DestroyContext(NULL);
SDL_DestroyTexture(cachedCharPreview);
}
void Gui_RenderMainMenu(Gui_t *gui) {
void Gui_RenderMainMenu(Gui_t *gui, const bool inGame) {
if (!ImGui_BeginMainMenuBar()) {
return;
}
@ -95,27 +77,25 @@ void Gui_RenderMainMenu(Gui_t *gui) {
if (ImGui_BeginMenu("Game")) {
if (ImGui_MenuItem("New Game")) {
gui->isNewGameDialogOpen = true;
}
if (ImGui_MenuItem("Journey Onward")) {
gui->isJourneyOnwardDialogOpen = true;
}
if (ImGui_MenuItem("End Game")) {
if (ImGui_MenuItemEx("End Game", NULL, NULL, inGame)) {
}
ImGui_EndMenu();
}
if (ImGui_BeginMenu("Info")) {
if (ImGui_MenuItem("Change Data")) {
if (ImGui_MenuItemEx("Change Data", NULL, NULL, inGame)) {
}
if (ImGui_MenuItem("Userlist")) {
if (ImGui_MenuItemEx("Userlist", NULL, NULL, inGame)) {
}
if (ImGui_MenuItem("Comments")) {
if (ImGui_MenuItemEx("Comments", NULL, NULL, inGame)) {
}
ImGui_EndMenu();
@ -142,39 +122,6 @@ void Gui_RenderMainMenu(Gui_t *gui) {
ImGui_EndMainMenuBar();
}
void Gui_RenderStatusBar(const Gui_t *gui) {
const ImGuiViewport *viewport = ImGui_GetMainViewport();
const float height = ImGui_GetFrameHeight();
const ImVec2 position = {viewport->Pos.x, viewport->Pos.y + viewport->Size.y - height};
const ImVec2 size = {viewport->Size.x, height};
ImGui_SetNextWindowPosEx(position, ImGuiCond_Always, (ImVec2){0.0f, 0.0f});
ImGui_SetNextWindowSize(size, ImGuiCond_Always);
// Remove rounding, borders and reduce padding
ImGui_PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui_PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui_PushStyleVarImVec2(ImGuiStyleVar_WindowPadding, (ImVec2){8.0f, 2.0f});
// Set background and text colors
ImGui_PushStyleColorImVec4(ImGuiCol_WindowBg, (ImVec4){0.85f, 0.85f, 0.85f, 1.0f});
ImGui_PushStyleColorImVec4(ImGuiCol_Text, (ImVec4){0.0f, 0.0f, 0.0f, 1.0f});
const ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNav;
if (ImGui_Begin("StatusBar", NULL, windowFlags)) {
ImGui_Text(gui->statusBarText);
}
ImGui_End();
ImGui_PopStyleColorEx(2);
ImGui_PopStyleVarEx(3);
}
void Gui_RenderPreferences_Dialog(Gui_t *gui, ConfigParams_t *configParams) {
if (!gui->isPreferencesDialogOpen) {
return;
@ -209,7 +156,7 @@ void Gui_RenderPreferences_Dialog(Gui_t *gui, ConfigParams_t *configParams) {
ImGui_SetNextItemWidth(-FLT_MIN);
ImGui_InputText("##Address", configParams->serverAddress, IM_ARRAYSIZE(configParams->serverAddress), 0);
// Server Port
// Server Address
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
@ -284,382 +231,3 @@ void Gui_RenderPreferences_Dialog(Gui_t *gui, ConfigParams_t *configParams) {
ImGui_End();
}
void Gui_RenderCustomizerRow(const char *label, uint8_t *value) {
if (ImGui_BeginTable(label, 3, ImGuiTableFlags_SizingFixedFit)) {
ImGui_TableSetupColumnEx("Left", ImGuiTableColumnFlags_WidthFixed, 20.0f, 0);
ImGui_TableSetupColumnEx("Text", ImGuiTableColumnFlags_WidthFixed, 45.0f, 0);
ImGui_TableSetupColumnEx("Right", ImGuiTableColumnFlags_WidthFixed, 20.0f, 0);
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
if (ImGui_ButtonEx("<", (ImVec2){20, 20})) {
(*value)--;
SDL_DestroyTexture(cachedCharPreview);
cachedCharPreview = NULL;
}
ImGui_TableSetColumnIndex(1);
const float currentX = ImGui_GetCursorPosX();
const float textWidth = ImGui_CalcTextSize(label).x;
ImGui_SetCursorPosX(currentX + (45.0f - textWidth) * 0.5f);
ImGui_Text("%s", label);
ImGui_TableSetColumnIndex(2);
if (ImGui_ButtonEx(">", (ImVec2){20, 20})) {
(*value)++;
SDL_DestroyTexture(cachedCharPreview);
cachedCharPreview = NULL;
}
ImGui_EndTable();
}
}
SDL_Texture *Gui_CombineCharacterTextures(SDL_Texture *bodyTexture, SDL_Texture *headTexture, SDL_Texture *legsTexture,
SDL_Texture *shoesTexture) {
SDL_Renderer *renderer = Window_GetRenderer();
SDL_Texture *target = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET, SPRITE_SIZE, SPRITE_SIZE);
SDL_SetTextureBlendMode(target, SDL_BLENDMODE_BLEND);
SDL_SetTextureScaleMode(target, SDL_SCALEMODE_NEAREST);
SDL_SetRenderTarget(renderer, target);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
// Draw layers in order
SDL_RenderTexture(renderer, shoesTexture, NULL, NULL);
SDL_RenderTexture(renderer, legsTexture, NULL, NULL);
SDL_RenderTexture(renderer, bodyTexture, NULL, NULL);
// The head texture needs to be offset
// This is hardcoded in the original client
SDL_RenderTexture(renderer, headTexture, NULL, &(SDL_FRect){1.0f, -14.0f, SPRITE_SIZE, SPRITE_SIZE});
SDL_SetRenderTarget(renderer, NULL);
return target;
}
void Gui_RenderCharacterCustomizer(const Objects_t *objects) {
if (ImGui_BeginTable("CharCustomizerMain", 2, ImGuiTableFlags_SizingFixedFit)) {
ImGui_TableSetupColumnEx("Preview", ImGuiTableColumnFlags_WidthFixed, SPRITE_SIZE * 2, 0);
ImGui_TableSetupColumn("Controls", ImGuiTableColumnFlags_WidthStretch);
ImGui_TableNextRow();
// Character Preview
ImGui_TableSetColumnIndex(0);
const float frameHeight = ImGui_GetFrameHeight();
const float spacing = ImGui_GetStyle()->ItemSpacing.y;
const float controlsTotalHeight = frameHeight * 4.0f + spacing * 3.0f;
const float offset = (controlsTotalHeight - SPRITE_SIZE * 2) * 0.5f;
if (offset > 0.0f) {
ImGui_SetCursorPosY(ImGui_GetCursorPosY() + offset);
}
if (cachedCharPreview == NULL) {
SDL_Texture *bodyTexture = Objects_GetSpriteTexture(objects, 155 + character.body);
SDL_Texture *headTexture = Objects_GetSpriteTexture(objects, 176 + character.head);
SDL_Texture *legsTexture = Objects_GetSpriteTexture(objects, 158 + character.legs);
SDL_Texture *shoesTexture = Objects_GetSpriteTexture(objects, 161 + character.shoes);
cachedCharPreview = Gui_CombineCharacterTextures(bodyTexture, headTexture, legsTexture, shoesTexture);
}
ImGui_Image((ImTextureRef){NULL, (ImTextureID) cachedCharPreview}, (ImVec2){SPRITE_SIZE * 2, SPRITE_SIZE * 2});
ImGui_TableSetColumnIndex(1);
ImGui_SetCursorPosX(ImGui_GetCursorPosX() + 10.0f);
ImGui_BeginGroup();
ImGui_PushID("Head");
Gui_RenderCustomizerRow("Head", &character.head);
ImGui_PopID();
ImGui_PushID("Body");
Gui_RenderCustomizerRow("Body", &character.body);
ImGui_PopID();
ImGui_PushID("Legs");
Gui_RenderCustomizerRow("Legs", &character.legs);
ImGui_PopID();
ImGui_PushID("Shoes");
Gui_RenderCustomizerRow("Shoes", &character.shoes);
ImGui_PopID();
ImGui_EndGroup();
ImGui_EndTable();
}
}
uint16_t Gui_PackCharacter() {
// Ensure indices are within 4-bit range
character.head &= 0x0F;
character.body &= 0x0F;
character.legs &= 0x0F;
character.shoes &= 0x0F;
uint16_t packed = 0;
packed |= character.head << 12;
packed |= character.body << 8;
packed |= character.legs << 4;
packed |= character.shoes;
return packed;
}
void Gui_RenderNewGame_Dialog(Gui_t *gui, const Objects_t *objects, const ConfigParams_t *configParams, Network_t *network) {
if (!gui->isNewGameDialogOpen) {
return;
}
static int sex = 1;
static char realName[50] = "";
static char email[50] = "";
static char location[50] = "";
const ImGuiViewport *viewport = ImGui_GetMainViewport();
const ImVec2 viewportCenter = {
viewport->Pos.x + viewport->Size.x * 0.5f,
viewport->Pos.y + viewport->Size.y * 0.5f
};
ImGui_SetNextWindowPosEx(viewportCenter, ImGuiCond_Appearing, (ImVec2){0.5f, 0.5f});
ImGui_SetNextWindowSize((ImVec2){550, 0}, ImGuiCond_Always);
if (ImGui_Begin("New Game", &gui->isNewGameDialogOpen,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) {
if (ImGui_BeginTable("TopLayout", 2, ImGuiTableFlags_BordersInnerV)) {
ImGui_TableSetupColumnEx("TopInputs", ImGuiTableColumnFlags_WidthStretch, 0.0f, 0);
ImGui_TableSetupColumnEx("TopButtons", ImGuiTableColumnFlags_WidthFixed, 140.0f, 0);
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
if (ImGui_BeginTable("TopForm", 2, ImGuiTableFlags_SizingFixedFit)) {
ImGui_TableSetupColumnEx("Label", ImGuiTableColumnFlags_WidthFixed, 90.0f, 0);
ImGui_TableSetupColumn("Input", ImGuiTableColumnFlags_WidthStretch);
// Real Name
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
ImGui_Text("Real Name:");
ImGui_TableSetColumnIndex(1);
ImGui_SetNextItemWidth(-FLT_MIN);
ImGui_InputText("##RealName", realName, sizeof(realName), 0);
// Location
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
ImGui_Text("Location:");
ImGui_TableSetColumnIndex(1);
ImGui_SetNextItemWidth(-FLT_MIN);
ImGui_InputText("##Location", location, sizeof(location), 0);
// E-Mail
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
ImGui_Text("E-Mail:");
ImGui_TableSetColumnIndex(1);
ImGui_SetNextItemWidth(-FLT_MIN);
ImGui_InputText("##Email", email, sizeof(email), 0);
ImGui_EndTable();
}
ImGui_TableSetColumnIndex(1);
// "Create Character"
if (ImGui_ButtonEx("Create Character", (ImVec2){-FLT_MIN, 0})) {
// TODO: verify is both passwords match
uint8_t buffer[30 + 30 + 1 + 2 + 1 + 50 + 50 + 50] = {0};
strncpy((char *) buffer, configParams->lastAccount, 30);
strncpy((char *) buffer + 30, configParams->lastPassword, 30);
buffer[30 + 30] = (uint8_t) sex;
const uint16_t packedCharacter = Gui_PackCharacter();
memcpy(buffer + 30 + 30 + 1, &packedCharacter, 2);
strncpy((char *) buffer + 30 + 30 + 1 + 2 + 1, realName, 50);
strncpy((char *) buffer + 30 + 30 + 1 + 2 + 1 + 50, location, 50);
strncpy((char *) buffer + 30 + 30 + 1 + 2 + 1 + 50 + 50, email, 50);
Network_ConnectAndReportStatus(network, configParams, gui);
Network_SendPacket(network, HANDLER_LOGIN_OR_CREATE_CHAR, 0x00, buffer, 30 + 30 + 1 + 2 + 1 + 50 + 50 + 50);
gui->isNewGameDialogOpen = false;
}
ImGui_Dummy((ImVec2){0.0f, 10.0f}); // Spacing between buttons
// "Oops, Cancel"
if (ImGui_ButtonEx("Oops, Cancel", (ImVec2){-FLT_MIN, 0})) {
gui->isNewGameDialogOpen = false;
}
ImGui_EndTable();
}
ImGui_Spacing();
ImGui_Separator();
ImGui_Spacing();
if (ImGui_BeginTable("BottomLayout", 2, ImGuiTableFlags_BordersInnerV)) {
ImGui_TableSetupColumnEx("BotInputs", ImGuiTableColumnFlags_WidthStretch, 0.0f, 0);
ImGui_TableSetupColumnEx("BotVisuals", ImGuiTableColumnFlags_WidthFixed, 180.0f, 0);
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
if (ImGui_BeginTable("BotForm", 2, ImGuiTableFlags_SizingFixedFit)) {
ImGui_TableSetupColumnEx("Label", ImGuiTableColumnFlags_WidthFixed, 130.0f, 0);
ImGui_TableSetupColumn("Input", ImGuiTableColumnFlags_WidthStretch);
// Your Name
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
ImGui_Text("Your Name:");
ImGui_TableSetColumnIndex(1);
ImGui_SetNextItemWidth(-FLT_MIN);
static char charName[64] = "";
ImGui_InputText("##CharName", charName, sizeof(charName), 0);
// Your Sex (Radio Buttons)
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
ImGui_Text("Your Sex:");
ImGui_TableSetColumnIndex(1);
ImGui_RadioButtonIntPtr("male", &sex, 1);
ImGui_SameLine();
ImGui_RadioButtonIntPtr("female", &sex, 0);
// Enter Password
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
ImGui_Text("Enter Password:");
ImGui_TableSetColumnIndex(1);
ImGui_SetNextItemWidth(-FLT_MIN);
static char pass1[64] = "";
ImGui_InputText("##Pass1", pass1, sizeof(pass1), ImGuiInputTextFlags_Password);
// Retype Password
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
ImGui_Text("Retype Password:");
ImGui_TableSetColumnIndex(1);
ImGui_SetNextItemWidth(-FLT_MIN);
static char pass2[64] = "";
ImGui_InputText("##Pass2", pass2, sizeof(pass2), ImGuiInputTextFlags_Password);
ImGui_EndTable();
}
ImGui_TableSetColumnIndex(1);
Gui_RenderCharacterCustomizer(objects);
ImGui_EndTable();
}
}
ImGui_End();
}
void Gui_RenderJourneyOnward_Dialog(Gui_t *gui, ConfigParams_t *configParams, Network_t *network) {
if (!gui->isJourneyOnwardDialogOpen) {
return;
}
const ImGuiViewport *viewport = ImGui_GetMainViewport();
const ImVec2 viewportCenter = {
viewport->Pos.x + viewport->Size.x * 0.5f,
viewport->Pos.y + viewport->Size.y * 0.5f
};
ImGui_SetNextWindowPosEx(viewportCenter, ImGuiCond_Appearing, (ImVec2){0.5f, 0.5f});
ImGui_SetNextWindowSize((ImVec2){380, 0}, ImGuiCond_Always);
if (ImGui_Begin("Journey Onward", &gui->isJourneyOnwardDialogOpen,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) {
if (ImGui_BeginTable("Split", 2, ImGuiTableFlags_BordersInnerV)) {
ImGui_TableSetupColumnEx("Data", ImGuiTableColumnFlags_WidthStretch, 0.0f, 0);
ImGui_TableSetupColumnEx("Actions", ImGuiTableColumnFlags_WidthFixed, 100.0f, 0);
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_Text("Player Data:");
if (ImGui_BeginTable("InputTable", 2, ImGuiTableFlags_SizingFixedFit)) {
ImGui_TableSetupColumnEx("Label", ImGuiTableColumnFlags_WidthFixed, 110.0f, 0);
ImGui_TableSetupColumn("Input", ImGuiTableColumnFlags_WidthStretch);
// Player Name
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
ImGui_Text("Player Name:");
ImGui_TableSetColumnIndex(1);
ImGui_SetNextItemWidth(-FLT_MIN);
ImGui_InputText("##PName", configParams->lastAccount, IM_ARRAYSIZE(configParams->lastAccount), 0);
// Player Password
ImGui_TableNextRow();
ImGui_TableSetColumnIndex(0);
ImGui_AlignTextToFramePadding();
ImGui_Text("Player Password:");
ImGui_TableSetColumnIndex(1);
ImGui_SetNextItemWidth(-FLT_MIN);
ImGui_InputText("##PPass", configParams->lastPassword, IM_ARRAYSIZE(configParams->lastPassword),
ImGuiInputTextFlags_Password);
ImGui_EndTable();
}
ImGui_TableSetColumnIndex(1);
// Cancel
if (ImGui_ButtonEx("Cancel", (ImVec2){-FLT_MIN, 0})) {
gui->isJourneyOnwardDialogOpen = false;
}
ImGui_Dummy((ImVec2){0.0f, 10.0f});
// Let's Go
if (ImGui_ButtonEx("Let's Go", (ImVec2){-FLT_MIN, 0})) {
gui->isJourneyOnwardDialogOpen = false;
uint8_t buffer[60] = {0};
strncpy((char *) buffer, configParams->lastAccount, 30);
strncpy((char *) buffer + 30, configParams->lastPassword, 30);
Network_ConnectAndReportStatus(network, configParams, gui);
Network_SendPacket(network, HANDLER_LOGIN_OR_CREATE_CHAR, 0x01, buffer, 60);
}
ImGui_EndTable();
}
}
ImGui_End();
}
void Gui_UpdateStatusBar(Gui_t *gui, const char *message) {
strncpy(gui->statusBarText, message, sizeof(gui->statusBarText) - 1);
gui->statusBarText[sizeof(gui->statusBarText) - 1] = '\0';
}

22
gui.h
View File

@ -8,37 +8,19 @@
#include <SDL3/SDL.h>
#include "config.h"
#include "network.h"
#include "objects.h"
typedef struct Character {
uint8_t head;
uint8_t body;
uint8_t legs;
uint8_t shoes;
} Character_t;
typedef struct Gui {
bool isPreferencesDialogOpen;
bool isNewGameDialogOpen;
bool isJourneyOnwardDialogOpen;
char statusBarText[255];
} Gui_t;
void Gui_Init(Gui_t* gui);
void Gui_ProcessEvent(const SDL_Event* event);
void Gui_StartRender();
void Gui_Render(Gui_t* gui, const Objects_t* objects, Network_t* network, ConfigParams_t* configParams);
void Gui_Render(Gui_t* gui, ConfigParams_t* configParams, bool inGame);
void Gui_FinishRender();
void Gui_Shutdown();
void Gui_RenderMainMenu(Gui_t* gui);
void Gui_RenderStatusBar(const Gui_t* gui);
void Gui_RenderMainMenu(Gui_t* gui, bool inGame);
void Gui_RenderPreferences_Dialog(Gui_t* gui, ConfigParams_t* configParams);
void Gui_RenderNewGame_Dialog(Gui_t* gui, const Objects_t* objects, const ConfigParams_t* configParams, Network_t* network);
void Gui_RenderJourneyOnward_Dialog(Gui_t* gui, ConfigParams_t* configParams, Network_t* network);
void Gui_UpdateStatusBar(Gui_t* gui, const char* message);
#endif //TIBIA_GUI_H

140
network.c
View File

@ -1,140 +0,0 @@
//
// Created by rov on 12/27/25.
//
#include "network.h"
#include <SDL3/SDL.h>
#include <SDL3_net/SDL_net.h>
#define NET_TIMEOUT 30000
typedef enum ConnectionResult {
CONN_SUCCESS = 0,
CONN_ERR_BAD_HOSTNAME = 1,
CONN_ERR_NO_SERVER = 2
} CONNECTION_RESULT;
void Gui_UpdateStatusBar(const Gui_t* gui, const char *message);
bool Network_Init(Network_t *network) {
if (!NET_Init()) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", "Failed to initialize SDL_net.", NULL);
return false;
}
network->socket = NULL;
return true;
}
void Network_Shutdown(const Network_t *network) {
NET_DestroyStreamSocket(network->socket);
NET_Quit();
}
CONNECTION_RESULT ConnectToServer(Network_t *network, const ConfigParams_t *configParams, const Gui_t *gui) {
Gui_UpdateStatusBar(gui, "Searching for hostname...");
NET_Address *netAddress = NET_ResolveHostname(configParams->serverAddress);
const NET_Status resolveHostnameStatus = NET_WaitUntilResolved(netAddress, NET_TIMEOUT);
if (resolveHostnameStatus != NET_SUCCESS) {
NET_UnrefAddress(netAddress);
return CONN_ERR_BAD_HOSTNAME;
}
Gui_UpdateStatusBar(gui, "Trying to connect...");
network->socket = NET_CreateClient(netAddress, configParams->serverPort);
const NET_Status createClientStatus = NET_WaitUntilConnected(network->socket, NET_TIMEOUT);
if (createClientStatus != NET_SUCCESS) {
NET_DestroyStreamSocket(network->socket);
network->socket = NULL;
NET_UnrefAddress(netAddress);
return CONN_ERR_NO_SERVER;
}
// TODO: is this okay?
NET_UnrefAddress(netAddress);
return CONN_SUCCESS;
}
void Network_ConnectAndReportStatus(Network_t *network, const ConfigParams_t *configParams, const Gui_t *gui) {
if (network->socket != NULL) {
return;
}
const CONNECTION_RESULT result = ConnectToServer(network, configParams, gui);
const char *message = NULL;
switch (result) {
case CONN_SUCCESS:
message = "Connection established.";
break;
case CONN_ERR_BAD_HOSTNAME:
message = "Error: IP address or host not found.";
break;
case CONN_ERR_NO_SERVER:
message = "Error: No server running on host.";
break;
}
Gui_UpdateStatusBar(gui, message);
}
void Network_Send(NET_StreamSocket* socket, uint8_t* packetBuf, const uint16_t packetLen) {
const uint16_t payloadLength = packetLen - 2;
*(uint16_t*) packetBuf = payloadLength;
if (!NET_WriteToStreamSocket(socket, packetBuf, packetLen)) {
SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Network_Send: Failed to send packet.");
}
}
void Network_HandleLoginOrCreateChar(const uint16_t opcode, uint8_t* packetBuf, uint16_t* packetLen, const void* data, const int dataLength) {
// Copy the opcode
packetBuf[*packetLen] = (uint8_t) opcode;
*packetLen += 1;
// Write unk0
const uint16_t unk0 = 1;
memcpy(&packetBuf[*packetLen], &unk0, 2);
*packetLen += 2;
// Write unk1
const uint16_t unk1 = 'g';
memcpy(&packetBuf[*packetLen], &unk1, 2);
*packetLen += 2;
memcpy(&packetBuf[*packetLen], data, dataLength);
*packetLen += dataLength;
}
void Network_SendPacket(const Network_t* network, const PACKET_HANDLER handler, const uint16_t opcode, void* data, const int dataLength) {
if (network->socket == NULL) {
return;
}
uint8_t packetBuf[1050];
uint16_t packetLen = 2;
// TODO: need this?
memset(packetBuf, 0, 1049);
// Write handler to buffer at offset 2
memcpy(&packetBuf[packetLen], &handler, 2);
packetLen += 2;
switch (handler) {
case HANDLER_LOGIN_OR_CREATE_CHAR:
Network_HandleLoginOrCreateChar(opcode, packetBuf, &packetLen, data, dataLength);
break;
default:
break;
}
Network_Send(network->socket, packetBuf, packetLen);
}

View File

@ -1,29 +0,0 @@
//
// Created by rov on 12/27/25.
//
#ifndef TIBIA_NETWORK_H
#define TIBIA_NETWORK_H
#include <stdbool.h>
#include <SDL3_net/SDL_net.h>
#include "config.h"
typedef struct Gui Gui_t;
typedef struct Network {
NET_StreamSocket* socket;
} Network_t;
typedef enum PacketHandler {
HANDLER_LOGIN_OR_CREATE_CHAR = 0
} PACKET_HANDLER;
bool Network_Init(Network_t* network);
void Network_Shutdown(const Network_t* network);
void Network_ConnectAndReportStatus(Network_t* network, const ConfigParams_t* configParams, const Gui_t* gui);
void Network_SendPacket(const Network_t* network, PACKET_HANDLER handler, uint16_t opcode, void* data, int dataLength);
#endif //TIBIA_NETWORK_H

103
objects.c
View File

@ -3,15 +3,15 @@
//
#include "objects.h"
#include "window.h"
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL_iostream.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_messagebox.h>
#include <SDL3/SDL.h>
#include "window.h"
bool Objects_Init(Objects_t *objects) {
memset(objects->spriteTable, 0, sizeof(objects->spriteTable));
for (int i = 0; i < THING_DATA_POOL_SIZE; i++) {
objects->thingDataPool[i] = malloc(1536);
@ -21,6 +21,7 @@ bool Objects_Init(Objects_t *objects) {
}
memset(objects->activeObjectList, 0, sizeof(objects->activeObjectList));
memset(objects->activeTextures, 0, sizeof(objects->activeTextures));
SDL_LogInfo(SDL_LOG_CATEGORY_CUSTOM, "Objects_Init: memory allocated");
@ -113,15 +114,14 @@ bool Objects_LoadData(Objects_t *objects) {
}
bool Objects_LoadSprites(Objects_t *objects) {
SDL_LogInfo(SDL_LOG_CATEGORY_CUSTOM, "Objects_LoadSprites: loading object sprites.");
SDL_Renderer* renderer = Window_GetRenderer();
SDL_LogInfo(SDL_LOG_CATEGORY_CUSTOM, "Objects_LoadSprites: loading object sprites.");
// Free existing sprites to prevent memory leaks
for (int i = 0; i < MAX_SPRITES; i++) {
if (objects->spriteTextures[i] != NULL) {
SDL_DestroyTexture(objects->spriteTextures[i]);
objects->spriteTextures[i] = NULL;
for (int i = 0; i < SPRITE_TABLE_SIZE; i++) {
if (objects->spriteTable[i] != NULL) {
free(objects->spriteTable[i]);
objects->spriteTable[i] = NULL;
}
}
@ -157,34 +157,39 @@ bool Objects_LoadSprites(Objects_t *objects) {
}
// Ensure ID is within bounds
if (spriteID >= MAX_SPRITES) {
if (spriteID >= SPRITE_TABLE_SIZE) {
SDL_LogWarn(SDL_LOG_CATEGORY_CUSTOM, "Objects_LoadSprites: Sprite ID %d exceeds table size (%d). Skipping.",
spriteID, MAX_SPRITES);
spriteID, SPRITE_TABLE_SIZE);
// Skip the data bytes so we don't desync the file stream
SDL_SeekIO(file, spriteSize - 2, SDL_IO_SEEK_CUR);
continue;
}
const uint16_t bytesToRead = spriteSize - 2;
if (bytesToRead <= 0) {
continue;
}
uint8_t *spriteBuffer = malloc(spriteSize);
if (!spriteBuffer) {
SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Objects_LoadSprites: cannot allocate memory");
break;
}
if (SDL_ReadIO(file, spriteBuffer, bytesToRead) != bytesToRead) {
SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Objects_LoadSprites: couldn't read full sprite data");
break;
objects->spriteTable[spriteID] = spriteBuffer;
// Store size in the first 2 bytes of the buffer
*(uint16_t *) spriteBuffer = spriteSize;
// Read data
// Read (Size - 2) bytes into Buffer + 2
const size_t bytesToRead = spriteSize - 2;
if (bytesToRead > 0) {
if (SDL_ReadIO(file, spriteBuffer + 2, bytesToRead) != bytesToRead) {
SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Objects_LoadSprites: couldn't read full sprite data");
break;
}
}
objects->spriteTextures[spriteID] = Objects_TextureFromRaw(renderer, spriteBuffer, bytesToRead);
objects->spriteTable[spriteID] = spriteBuffer;
objects->activeTextures[spriteID] = Objects_TextureFromRaw(renderer, spriteBuffer);
free(spriteBuffer);
spritesLoaded++;
}
@ -199,76 +204,70 @@ void Objects_Destroy(const Objects_t *objects) {
free(objects->thingDataPool[i]);
}
for (int i = 0; i < MAX_SPRITES; i++) {
SDL_DestroyTexture(objects->spriteTextures[i]);
for (int i = 0; i < ACTIVE_OBJECT_LIST_SIZE; i++) {
if (objects->activeTextures[i]) {
SDL_DestroyTexture(objects->activeTextures[i]);
}
}
}
SDL_Texture * Objects_GetSpriteTexture(const Objects_t* objects, const uint16_t spriteID) {
if (spriteID < 0 || spriteID >= MAX_SPRITES) {
SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Objects_GetSpriteTexture: sprite ID out of range (ID: %d).", spriteID);
SDL_Texture* Objects_GetSpriteTexture(const Objects_t *objects, const uint16_t spriteID) {
if (spriteID >= SPRITE_TABLE_SIZE) {
return NULL;
}
return objects->spriteTextures[spriteID];
return objects->activeTextures[spriteID];
}
SDL_Texture * Objects_TextureFromRaw(SDL_Renderer *renderer, const uint8_t *rawData, const uint16_t size) {
SDL_Texture* Objects_TextureFromRaw(SDL_Renderer* renderer, uint8_t* rawData) {
if (!rawData) {
return NULL;
}
SDL_Surface* surface = SDL_CreateSurface(SPRITE_SIZE, SPRITE_SIZE, SDL_PIXELFORMAT_ARGB8888);
const uint16_t totalSize = *(uint16_t*) rawData;
uint8_t* data = rawData + 2;
int readOffset = 0;
SDL_Surface* surface = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_ARGB8888);
if (!surface) {
SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Objects_TextureFromRaw: couldn't create surface");
return NULL;
}
SDL_LockSurface(surface);
uint32_t* pixels = surface->pixels;
memset(pixels, 0, SPRITE_SIZE * SPRITE_SIZE * 4);
memset(pixels, 0, 32 * 32 * 4);
int currentPixel = 0;
int readOffset = 0;
while (readOffset < size && currentPixel < 1024) {
const uint16_t transparentRaw = *(uint16_t*) (rawData + readOffset);
while (readOffset < totalSize - 2 && currentPixel < 1024) {
const uint16_t transparentRaw = *(uint16_t*)(data + readOffset);
readOffset += 2;
const uint16_t transparentBytes = transparentRaw % 0x5A0;
const uint16_t skipPixels = transparentBytes / 3;
const int skipPixels = transparentBytes / 3;
currentPixel += skipPixels;
const uint16_t colorBytes = *(uint16_t*)(rawData + readOffset);
const uint16_t colorBytes = *(uint16_t*)(data + readOffset);
readOffset += 2;
const uint16_t drawPixels = colorBytes / 3;
const int drawPixels = colorBytes / 3;
for (int i = 0; i < drawPixels; i++) {
if (currentPixel >= 1024) {
break;
}
const uint8_t b = rawData[readOffset++];
const uint8_t g = rawData[readOffset++];
const uint8_t r = rawData[readOffset++];
const uint8_t r = data[readOffset++];
const uint8_t g = data[readOffset++];
const uint8_t b = data[readOffset++];
// Flip image
const int x = currentPixel % SPRITE_SIZE;
const int y = currentPixel / SPRITE_SIZE;
const int flippedY = 31 - y;
pixels[flippedY * SPRITE_SIZE + x] = SDL_MapRGBA(SDL_GetPixelFormatDetails(surface->format), NULL, r, g, b, 255);
currentPixel++;
pixels[currentPixel++] = SDL_MapRGBA(SDL_GetPixelFormatDetails(surface->format), NULL, r, g, b, 255);
}
}
SDL_UnlockSurface(surface);
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);
SDL_DestroySurface(surface);
return texture;

View File

@ -5,12 +5,10 @@
#ifndef TIBIA_OBJECTS_H
#define TIBIA_OBJECTS_H
#define MAX_SPRITES 500
#define SPRITE_TABLE_SIZE 500
#define THING_DATA_POOL_SIZE 256
#define ACTIVE_OBJECT_LIST_SIZE 256
#define SPRITE_SIZE 32
#include <stdbool.h>
#include <stdint.h>
#include <SDL3/SDL_render.h>
@ -25,17 +23,19 @@ typedef struct {
typedef struct Objects {
// TODO: not sure about the name/purpose of these.
SDL_Texture* spriteTextures[MAX_SPRITES];
void* spriteTable[SPRITE_TABLE_SIZE];
void* thingDataPool[THING_DATA_POOL_SIZE];
void* activeObjectList[ACTIVE_OBJECT_LIST_SIZE];
SDL_Texture* activeTextures[SPRITE_TABLE_SIZE];
} Objects_t;
bool Objects_Init(Objects_t* objects);
bool Objects_LoadData(Objects_t* objects);
bool Objects_LoadSprites(Objects_t* objects);
void Objects_Destroy(const Objects_t* objects);
SDL_Texture* Objects_GetSpriteTexture(const Objects_t* objects, uint16_t spriteID);
SDL_Texture* Objects_TextureFromRaw(SDL_Renderer* renderer, const uint8_t* rawData, uint16_t size);
// TODO: move this somewhere else
SDL_Texture* Objects_TextureFromRaw(SDL_Renderer* renderer, uint8_t* rawData);
#endif //TIBIA_OBJECTS_H

104
render.c
View File

@ -6,6 +6,48 @@
#include "window.h"
#include "gui.h"
#define MAP_WIDTH_TILES 15
#define MAP_HEIGHT_TILES 11
#define TILE_SIZE 32
typedef struct {
uint16_t groundSpriteID;
} MapTile;
MapTile g_ClientMapData[MAP_WIDTH_TILES][MAP_HEIGHT_TILES];
void Map_LoadSampleData() {
SDL_Log("Map_LoadSampleData: Populating dummy map...");
// These IDs are guesses for Tibia 1.03 based on standard asset order.
// If these show up as items (like swords/apples), try swapping them.
// Usually: 0-100 contains basic ground tiles.
uint16_t ID_GRASS = 45;
uint16_t ID_DIRT = 40;
uint16_t ID_STONE = 50;
for (int x = 0; x < MAP_WIDTH_TILES; x++) {
for (int y = 0; y < MAP_HEIGHT_TILES; y++) {
// 1. Default to Grass
uint16_t tileID = ID_GRASS;
// 2. Create a Checkerboard pattern
if ((x + y) % 2 == 0) {
tileID = ID_DIRT;
}
// 3. Create a Stone path in the middle row
if (y == 5) {
tileID = ID_STONE;
}
// 4. Assign to the map
g_ClientMapData[x][y].groundSpriteID = x * MAP_HEIGHT_TILES + y;
}
}
}
void Render_Frame(App_t* app) {
Gui_StartRender();
@ -14,7 +56,9 @@ void Render_Frame(App_t* app) {
SDL_RenderClear(renderer);
Render_MainWindowBackground(app);
Gui_Render(&app->gui, &app->graphics, &app->network, &app->configParams);
Render_GameView(app);
Gui_Render(&app->gui, &app->configParams, app->isInGame);
Gui_FinishRender();
SDL_RenderPresent(renderer);
@ -29,7 +73,7 @@ void Render_MainWindowBackground(const App_t *app) {
return;
}
const int TILE_SIZE = 128;
// const int TILE_SIZE = 128;
// Calculate how many tiles we need to cover the screen
const int tilesX = app->metrics.screenWidth / TILE_SIZE + 1;
@ -48,3 +92,59 @@ void Render_MainWindowBackground(const App_t *app) {
}
}
}
void Render_GameView(const App_t *app) {
if (!app->isInGame) {
Render_TitleScreen(app);
return;
}
Render_Game(app);
}
void Render_TitleScreen(const App_t *app) {
// TODO: properly calculate the window sizes and spacings
SDL_Renderer *renderer = Window_GetRenderer();
int winW, winH;
SDL_GetRenderOutputSize(renderer, &winW, &winH);
const SDL_FRect destRect = {
0.0f, 30.0f,
(float)winW, (float)winH - 150.0f
};
SDL_RenderTexture(renderer, app->bitmap.tibiaTexture, NULL, &destRect);
}
void Render_Game(const App_t *app) {
SDL_Renderer *renderer = Window_GetRenderer();
for (int x = 0; x < MAP_WIDTH_TILES; x++) {
for (int y = 0; y < MAP_HEIGHT_TILES; y++) {
const MapTile* tile = &g_ClientMapData[x][y];
if (tile->groundSpriteID == 0) {
continue;
}
const int screenX = x * TILE_SIZE;
const int screenY = y * TILE_SIZE;
SDL_Texture* texture = Objects_GetSpriteTexture(&app->objects, tile->groundSpriteID);
if (texture) {
SDL_FRect destRect = {
(float)screenX,
(float)screenY,
(float)TILE_SIZE,
(float)TILE_SIZE
};
SDL_RenderTexture(renderer, texture, NULL, &destRect);
} else {
SDL_FRect destRect = { (float)screenX, (float)screenY, 32.0f, 32.0f };
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); // Green for ground
SDL_RenderFillRect(renderer, &destRect);
}
}
}
}

View File

@ -8,8 +8,14 @@
#include "app.h"
void Map_LoadSampleData();
void Render_Frame(App_t* app);
void Render_MainWindowBackground(const App_t* app);
void Render_GameView(const App_t* app);
void Render_TitleScreen(const App_t* app);
void Render_Game(const App_t* app);
void Render_DrawWindowFrame(SDL_Renderer* renderer, int x, int y, int w, int h);