Compare commits
No commits in common. "archive-3d-survival" and "master" have entirely different histories.
archive-3d
...
master
21
.gitignore
vendored
21
.gitignore
vendored
@ -1,3 +1,20 @@
|
||||
/bin
|
||||
.clangd
|
||||
# Build output.
|
||||
/bin/
|
||||
build/
|
||||
|
||||
# Editor/IDE, whatever people use.
|
||||
.vscode/
|
||||
.idea/
|
||||
*.suo
|
||||
*.user
|
||||
*.clangd/
|
||||
compile_commands.json
|
||||
|
||||
# For those messy OS'es that like to put files everywhere.
|
||||
.DS_Store
|
||||
|
||||
# Misc.
|
||||
assets/design_doc.org
|
||||
.clangd
|
||||
*.swp
|
||||
*.db
|
||||
|
||||
@ -1,30 +1,55 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(bettola CXX C)
|
||||
|
||||
project(bettola VERSION 0.1)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Let's use C++17?
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# include directories..
|
||||
include_directories(libbettola/include)
|
||||
include_directories(src)
|
||||
# === Sol2 ===
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
sol2
|
||||
GIT_REPOSITORY https://github.com/ThePhD/sol2.git
|
||||
GIT_TAG v3.3.1
|
||||
)
|
||||
FetchContent_MakeAvailable(sol2)
|
||||
|
||||
# Deps.
|
||||
find_package(SDL3 REQUIRED)
|
||||
find_package(GLEW REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
# === Asio ===
|
||||
FetchContent_Declare(
|
||||
asio
|
||||
GIT_REPOSITORY https://github.com/chriskohlhoff/asio
|
||||
git_TAG asio-1-36-0
|
||||
)
|
||||
FetchContent_MakeAvailable(asio)
|
||||
|
||||
# Build our shared lib.
|
||||
add_subdirectory(libbettola)
|
||||
# === SQLite ===
|
||||
# Supress the developer warning for using Populate with declared content.
|
||||
# We need it because sqlite zip isn't a CMAKE project.
|
||||
cmake_policy(SET CMP0169 OLD)
|
||||
FetchContent_Declare(
|
||||
sqlite_source
|
||||
URL https://sqlite.org/2025/sqlite-amalgamation-3500400.zip
|
||||
URL_HASH SHA256=1d3049dd0f830a025a53105fc79fd2ab9431aea99e137809d064d8ee8356b032
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP true
|
||||
)
|
||||
FetchContent_GetProperties(sqlite_source)
|
||||
if(NOT sqlite_source_POPULATED)
|
||||
FetchContent_Populate(sqlite_source)
|
||||
add_library(sqlite STATIC "${sqlite_source_SOURCE_DIR}/sqlite3.c")
|
||||
target_include_directories(sqlite PUBLIC "${sqlite_source_SOURCE_DIR}")
|
||||
endif()
|
||||
# Revert policy to default.
|
||||
cmake_policy(SET CMP0169 NEW)
|
||||
|
||||
# Will need to clean build each time you add a new file though -.-
|
||||
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||
add_executable(bettola ${SOURCES})
|
||||
# === sqlite_modern_cpp (SQLite wrapper) ===
|
||||
FetchContent_Declare(
|
||||
sqlite_modern_cpp
|
||||
GIT_REPOSITORY https://github.com/SqliteModernCpp/sqlite_modern_cpp.git
|
||||
GIT_TAG v3.2
|
||||
)
|
||||
FetchContent_MakeAvailable(sqlite_modern_cpp)
|
||||
|
||||
target_link_libraries(bettola PRIVATE bettola_lib SDL3::SDL3 GLEW::glew OpenGL::GL)
|
||||
|
||||
# Server executable.
|
||||
file(GLOB_RECURSE SERVER_SOURCES "srv/*.cpp")
|
||||
add_executable(bettola_server ${SERVER_SOURCES})
|
||||
target_link_libraries(bettola_server PRIVATE bettola_lib)
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(client)
|
||||
add_subdirectory(server)
|
||||
|
||||
62
Makefile
62
Makefile
@ -1,46 +1,38 @@
|
||||
SHELL:=/bin/bash
|
||||
BUILD_DIR:=bin
|
||||
PROJECT_NAME:=bettola
|
||||
EXECUTABLE:=$(BUILD_DIR)/$(PROJECT_NAME)
|
||||
# Wrapper Makefile.
|
||||
|
||||
CXX_COMPILER:=clang++
|
||||
CMAKE_MAKEFILE:=$(BUILD_DIR)/Makefile
|
||||
# Build artifacts.
|
||||
BUILD_DIR := bin
|
||||
|
||||
.PHONY: all build config run clean help
|
||||
# Client path.
|
||||
CLIENT_EXE := $(BUILD_DIR)/client/bettolac
|
||||
# Server path.
|
||||
SERVER_EXE := $(BUILD_DIR)/server/bettolas
|
||||
|
||||
# Default.
|
||||
.PHONY: all build config runc sp runs clean
|
||||
|
||||
# Default target when running 'make'.
|
||||
all: build
|
||||
|
||||
# Build project, if not configured, then do that first.
|
||||
build: $(CMAKE_MAKEFILE)
|
||||
@echo "==== Building Bettola ===="
|
||||
@cmake --build $(BUILD_DIR)
|
||||
|
||||
# run 'config' target if the build directory or CMake cache is missing.
|
||||
$(CMAKE_MAKEFILE):
|
||||
$(MAKE) config
|
||||
build: config
|
||||
@echo "=== Building Bettola. ==="
|
||||
@$(MAKE) -C $(BUILD_DIR)
|
||||
|
||||
config:
|
||||
@echo "==== Configuring Bettola with CMake ===="
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@cmake -S . -B$(BUILD_DIR) -DCMAKE_CXX_COMPILER=$(CXX_COMPILER) -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||
@echo "=== Configuring Project. ==="
|
||||
@cmake -B $(BUILD_DIR) -S . -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||
|
||||
# Build and run.
|
||||
run: all
|
||||
@echo "==== Running Bettola ===="
|
||||
$(EXECUTABLE)
|
||||
runc: build
|
||||
@echo "=== Running Bettola Client. ==="
|
||||
@$(CLIENT_EXE)
|
||||
|
||||
sp: build
|
||||
@echo "=== Running Bettola Client (Single Player). ==="
|
||||
@$(CLIENT_EXE) -sp
|
||||
|
||||
runs: build
|
||||
@echo "=== Running Bettola Server. ==="
|
||||
@$(SERVER_EXE)
|
||||
|
||||
# Remove build dir.
|
||||
clean:
|
||||
@echo "==== Cleaning Bettola ===="
|
||||
@echo "=== Cleaning Build Directory. ==="
|
||||
@rm -rf $(BUILD_DIR)
|
||||
@echo "==== Project Cleaned ===="
|
||||
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " make - Build the project (default)."
|
||||
@echo " make build - Build the project."
|
||||
@echo " make run - Build and run the project."
|
||||
@echo " make config - Force CMake to re-configure the project."
|
||||
@echo " make clean - Remove all build files"
|
||||
|
||||
|
||||
79
README.org
79
README.org
@ -1,35 +1,44 @@
|
||||
#+TITLE: Bettola Game
|
||||
#+TITLE: Working Title: Bettola
|
||||
|
||||
A 2D RPG game and engine created with C++, SDL3, and OpenGL.
|
||||
A multiplayer hacking simulator with a graphical OS, built in C++ and OpenGL.
|
||||
|
||||
* Dependencies
|
||||
The following dependencies are requird:
|
||||
The following dependencies are required:
|
||||
- A C++ compiler (e.g., clang, g++)
|
||||
- CMake (3.16 or newer)
|
||||
- SDL3 development libraries
|
||||
- Asio networking library (managed by CMake)
|
||||
- GLEW development libraries
|
||||
- FreeType development libraries
|
||||
- Lua 5.4 development libraries
|
||||
- sol2 (header-only, managed by CMake)
|
||||
|
||||
** Installation (Debian)
|
||||
You can install all required dependencies with the following command:
|
||||
You can install most required dependencies with the following command:
|
||||
#+BEGIN_SRC bash
|
||||
sudo apt update
|
||||
sudo apt install build-essential clang cmake libsdl3-dev libglew-dev
|
||||
sudo apt install build-essential clang cmake libsdl3-dev libglew-dev libfreetype-dev liblua5.4-dev
|
||||
#+END_SRC
|
||||
|
||||
* Build Instructions
|
||||
This project uses a top-level Makefile to simplify the CMake workflow. All build artifacts will be placed in the =bin/= directory.
|
||||
This project uses a top-level Makefile to simplify the CMake workflow. Build
|
||||
artifacts will be placed in the =bin/= directory.
|
||||
|
||||
Run the following from the root of the project directory.
|
||||
Simply run the following commands from the root of the project directory:
|
||||
|
||||
- *Build the project:*
|
||||
The executable will be located in =bin/bettola=.
|
||||
#+BEGIN_SRC bash
|
||||
make
|
||||
#+END_SRC
|
||||
|
||||
- *Build and run the project:*
|
||||
- *Build and run the client:*
|
||||
#+BEGIN_SRC bash
|
||||
make run
|
||||
make runc
|
||||
#+END_SRC
|
||||
|
||||
- *Build and run the server:*
|
||||
#+BEGIN_SRC bash
|
||||
make runs
|
||||
#+END_SRC
|
||||
|
||||
- *Clean the project:*
|
||||
@ -37,3 +46,53 @@ Run the following from the root of the project directory.
|
||||
#+BEGIN_SRC bash
|
||||
make clean
|
||||
#+END_SRC
|
||||
|
||||
* Project Structure
|
||||
The codebase is organised into three main components:
|
||||
- =common/=: A shared library (=libbettola=) containing code used by both the
|
||||
client and server.
|
||||
- =client/=: The game client (=bettolac=), handles rendering, UI, and user input.
|
||||
- =server/=: The game server (=bettolas=) manages game state and the world
|
||||
simulation.
|
||||
|
||||
* Planned Features
|
||||
/Note: [X] indicates a feature that is currently in progress or complete.
|
||||
|
||||
** Core Gameplay & Hacking
|
||||
- [X] Custom-built graphical OS desktop environment.
|
||||
- [X] Interactive terminal with command history and scrolling.
|
||||
- [X] Draggable and focusable UI windows.
|
||||
- [X] Local and remote virtual file systems (VFS).
|
||||
- [X] Core filesystem commands (=ls=, =cd=).
|
||||
- [X] Remote system connections via ingame =ssh= tool.
|
||||
- [X] Network scanning tools to discover hosts, open ports, and running
|
||||
services.
|
||||
- [ ] A deep exploit system based on service versions (e.g., SSH, FTP, HTTP).
|
||||
- [ ] Ability to find, modify, and write new exploits.
|
||||
- [ ] Functionality to upload/download files to and from remote systems.
|
||||
- [ ] Log cleaning utilities and other tools for covering your tracks.
|
||||
- [ ] Social engineering through in-game email and websites.
|
||||
|
||||
** The World
|
||||
- [ ] Narrative-driven main storyline (serves as tutorial before the sandbox
|
||||
world).
|
||||
- [ ] Emergent gameplay arising from the interaction of world systems.
|
||||
- [ ] A persistent, shared "core" universe of high-level NPC networks.
|
||||
- [ ] A unique, procedurally generated "local neighbourhood" for each new
|
||||
player.
|
||||
- [ ] NPC factions and corporations with simulated goals and stock markets.
|
||||
- [ ] Dynamic missions generated organically from the state of the world.
|
||||
- [ ] Active NPC system administrators who patch vulnerabilities and hunt for
|
||||
intruders.
|
||||
|
||||
** Player Systems & Progression
|
||||
- [X] Embedded Lua scripting engine for creating custom tools.
|
||||
- [X] In-game code editor with syntax highlighting etc.
|
||||
- [ ] Secure, sandboxed execution of player scripts with CPU/RAM as a
|
||||
resource.
|
||||
- [ ] An in-game internet with a web browser, email, and online banking.
|
||||
- [ ] Online stores for purchasing virtual hardware, software, and exploits.
|
||||
- [ ] The ability to purchase and upgrade dedicated servers.
|
||||
- [ ] Hosting of player-owned services (web, FTP, etc.).
|
||||
- [ ] Creation of custom websites using HTML and basic JS.
|
||||
- [ ] Player-to-player secure messaging and file transfers.
|
||||
|
||||
24
assets/boot_messages.txt
Normal file
24
assets/boot_messages.txt
Normal file
@ -0,0 +1,24 @@
|
||||
[ 0.000000] Bettola version 6.1.0-bettola (dev@bettola)
|
||||
[ 0.000000] Command line: BOOT_IMAGE=/vmbettola-6.1.0 ro quiet
|
||||
[ 0.134589] ACPI: PM-Timer IO Port: 0x808
|
||||
[ 0.345821] pci 0000:00:02.0: vgaarb: setting as boot VGA device
|
||||
[ 0.345911] pci 0000:00:03.0: enp0s3: identified as [B77A:1337]
|
||||
[ 0.582190] systemd[1]: Starting systemd-journald.service...
|
||||
[ 0.621337] systemd-journald[218]: Journal started.
|
||||
[ 1.123456] EXT4-fs (sda1): mounted filesystem with ordered data mode.
|
||||
[ 1.567890] systemd[1]: Reached target Local File Systems.
|
||||
[ 1.890123] systemd[1]: Starting systemd-logind.service...
|
||||
[ 2.101122] systemd[1]: Starting NetworkManager.service...
|
||||
[ 2.334455] NetworkManager[310]: <info> [1678886400.123] NetworkManager (version 1.40.0) is
|
||||
starting...
|
||||
[ 2.800100] enp0s3: Link is up at 1000 Mbps, full duplex.
|
||||
[ 3.123456] systemd[1]: Reached target Network.
|
||||
[ 3.500000] systemd[1]: Starting Bettola Daemon...
|
||||
[ 3.600000] bettolad[420]: Initializing VFS...
|
||||
[ 3.700000] bettolad[420]: Generating world...
|
||||
[ 4.100000] bettolad[420]: World generation complete. Seed: 0xDEADBEEF
|
||||
[ 4.200000] bettolad[420]: Listening on 0.0.0.0:1337
|
||||
[ 4.500000] systemd[1]: Started Bettola Daemon.
|
||||
[ 4.800000] systemd[1]: Reached target Multi-User System.
|
||||
[ 5.000000] systemd[1]: Starting Graphical Interface.
|
||||
[ 5.500000] bettolac-greeter: Starting display manager...
|
||||
BIN
assets/fonts/hack/Hack-Bold.ttf
Normal file
BIN
assets/fonts/hack/Hack-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/hack/Hack-BoldItalic.ttf
Normal file
BIN
assets/fonts/hack/Hack-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/hack/Hack-Italic.ttf
Normal file
BIN
assets/fonts/hack/Hack-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/hack/Hack-Regular.ttf
Normal file
BIN
assets/fonts/hack/Hack-Regular.ttf
Normal file
Binary file not shown.
34
assets/menu_background_snippets.txt
Normal file
34
assets/menu_background_snippets.txt
Normal file
@ -0,0 +1,34 @@
|
||||
map -sV 10.0.2.15
|
||||
ls -la /etc/
|
||||
ps aux | grep 'sshd'
|
||||
gcc exploit.c -o exploit
|
||||
SELECT * FROM users;
|
||||
import requests
|
||||
while(true){};
|
||||
});
|
||||
sudo rm -rf /
|
||||
netstat -anp
|
||||
arp -a
|
||||
ssh root@192.168.1.1
|
||||
git commit -m 'Initial commit'
|
||||
hydra -l user -P wordlist.txt ftp://...
|
||||
Hello, world.
|
||||
All your base are belong to us
|
||||
// TODO: fix this later
|
||||
42
|
||||
1337
|
||||
zerocool
|
||||
acidburn
|
||||
cereal_killer
|
||||
ping -c 4 google.com
|
||||
traceroute 8.8.8.8
|
||||
</div>
|
||||
<a>
|
||||
password123
|
||||
GOD
|
||||
Have you tried turning it off and on again?
|
||||
Connection established.
|
||||
SYN/ACK
|
||||
Firewall breach detected.
|
||||
Decrypting /etc/shadow...
|
||||
root::0:0:root:/root:/bin/bash
|
||||
27
assets/scripts/bin/build.lua
Normal file
27
assets/scripts/bin/build.lua
Normal file
@ -0,0 +1,27 @@
|
||||
-- /bin/build - "Compiles" a .lua file into an executable.
|
||||
local source_filename = arg[1]
|
||||
|
||||
if not source_filename then
|
||||
return "build: missing file operand"
|
||||
end
|
||||
|
||||
-- Check for .lua extension.
|
||||
if not source_filename:match("%.lua$") then
|
||||
return "build: input file must be a .lua file"
|
||||
end
|
||||
|
||||
local current_dir = bettola.get_current_dir(context)
|
||||
local source_node = current_dir.children[source_filename]
|
||||
|
||||
if not source_node then
|
||||
return "build: cannot open '" .. source_filename .."': No such file"
|
||||
end
|
||||
|
||||
if source_node.type ~= 0 then
|
||||
return "build: '" .. source_filename .. "' is not a regular file"
|
||||
end
|
||||
|
||||
local executable_filename = source_filename:gsub("%.lua$", "")
|
||||
local source_content = source_node.content
|
||||
|
||||
return bettola.create_executable(context, executable_filename, source_content)
|
||||
19
assets/scripts/bin/cat.lua
Normal file
19
assets/scripts/bin/cat.lua
Normal file
@ -0,0 +1,19 @@
|
||||
-- /bin/cat - Concatenate files and print on stdout.
|
||||
local filename = arg[1]
|
||||
if not filename then
|
||||
return "" -- No arguments, return nothing.
|
||||
end
|
||||
|
||||
local current_dir = bettola.get_current_dir(context)
|
||||
local target_node = current_dir.children[filename]
|
||||
|
||||
if not target_node then
|
||||
return "cat: " .. filename .. ": No such file or directory."
|
||||
end
|
||||
|
||||
if target_node.type == 1 then
|
||||
return "cat: " .. filename .. ": Is a directory"
|
||||
end
|
||||
|
||||
-- It's a file, return it's contents. :)
|
||||
return target_node.content
|
||||
7
assets/scripts/bin/cd.lua
Normal file
7
assets/scripts/bin/cd.lua
Normal file
@ -0,0 +1,7 @@
|
||||
-- /bin/cd - Change Directory.
|
||||
local target = arg[1]
|
||||
if not target then
|
||||
return "" -- No argument, just return to prompt.
|
||||
end
|
||||
|
||||
return bettola.cd(context, target)
|
||||
25
assets/scripts/bin/echo.lua
Normal file
25
assets/scripts/bin/echo.lua
Normal file
@ -0,0 +1,25 @@
|
||||
-- /bin/echo - Display contents of a text file.
|
||||
local content_parts = {}
|
||||
local filename = nil
|
||||
local found_redirect = false
|
||||
|
||||
for i, v in ipairs(arg) do
|
||||
if v == ">" then
|
||||
found_redirect = true
|
||||
filename = arg[i+1]
|
||||
break
|
||||
else
|
||||
table.insert(content_parts, v)
|
||||
end
|
||||
end
|
||||
|
||||
if found_redirect then
|
||||
if not filename then
|
||||
return "echo: syntax error: expected filename after '>'"
|
||||
end
|
||||
local content = table.concat(content_parts, " ")
|
||||
return bettola.write_file(context, filename, content)
|
||||
else
|
||||
return table.concat(arg, " ")
|
||||
end
|
||||
|
||||
6
assets/scripts/bin/exit.lua
Normal file
6
assets/scripts/bin/exit.lua
Normal file
@ -0,0 +1,6 @@
|
||||
-- /bin/exit - Disconnects from a remote session or cloes terminal window.
|
||||
if is_remote_session then
|
||||
return bettola.disconnect(context)
|
||||
else
|
||||
return bettola.close_terminal(context)
|
||||
end
|
||||
5
assets/scripts/bin/ls.lua
Normal file
5
assets/scripts/bin/ls.lua
Normal file
@ -0,0 +1,5 @@
|
||||
-- /bin/ls - Lists files in a directory.
|
||||
--
|
||||
-- Iterate over the 'children' map exposed via C++.
|
||||
|
||||
return bettola.ls(context)
|
||||
9
assets/scripts/bin/nmap.lua
Normal file
9
assets/scripts/bin/nmap.lua
Normal file
@ -0,0 +1,9 @@
|
||||
-- /bin/nmap - Network exploration tool and security/port scanner.
|
||||
local target_ip = arg[1]
|
||||
|
||||
if not target_ip then
|
||||
return "nmap: requires a target host to be specified"
|
||||
end
|
||||
|
||||
-- TODO: Add args such as -sV for version detection etc.
|
||||
return bettola.nmap(context, target_ip)
|
||||
3
assets/scripts/bin/rm.lua
Normal file
3
assets/scripts/bin/rm.lua
Normal file
@ -0,0 +1,3 @@
|
||||
local file_to_remove = arg[1]
|
||||
if not file_to_remove then return "rm: missing operand" end
|
||||
return bettola.rm(context, file_to_remove)
|
||||
8
assets/scripts/bin/scp.lua
Normal file
8
assets/scripts/bin/scp.lua
Normal file
@ -0,0 +1,8 @@
|
||||
local source = arg[1]
|
||||
local destination = arg[2]
|
||||
|
||||
if not source or not destination then
|
||||
return "usage: scp source destination"
|
||||
end
|
||||
|
||||
return bettola.scp(context, source, destination)
|
||||
4
assets/scripts/bin/ssh.lua
Normal file
4
assets/scripts/bin/ssh.lua
Normal file
@ -0,0 +1,4 @@
|
||||
-- /bin/ssh - Connects to a remote host.
|
||||
local target = arg[1]
|
||||
if not target then return "ssh: missing operand" end
|
||||
return bettola.ssh(context, target)
|
||||
@ -1,85 +0,0 @@
|
||||
#version 330 core
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec3 TexCoords;
|
||||
|
||||
uniform sampler2D u_CloudTexture;
|
||||
uniform vec3 u_SunPos;
|
||||
|
||||
const float PI = 3.14159265359;
|
||||
|
||||
/* Atmosphere parameters. */
|
||||
const float ATMOSPHERE_RADIUS = 6520275.0;
|
||||
const float PLANET_RADIUS = 6371000.0;
|
||||
const float RAYLEIGH_SCALE_HEIGHT = 8000.0;
|
||||
const float MIE_SCALE_HEIGHT = 1200.0;
|
||||
|
||||
/* Scattering coefficients. */
|
||||
const vec3 RAYLEIGH_SCATTERING_COEFF = vec3(0.0000058, 0.0000135, 0.0000331);
|
||||
const vec3 MIE_SCATTERING_COEFF = vec3(0.00002);
|
||||
|
||||
/* Mie scattering parameters. */
|
||||
const float MIE_G = 0.76;
|
||||
|
||||
/* Number of samples for ray marching. */
|
||||
const int SAMPLES = 16;
|
||||
const int LIGHT_SAMPLES = 8;
|
||||
|
||||
float ray_sphere_intersect(vec3 origin, vec3 dir, float radius) {
|
||||
float a = dot(dir, dir);
|
||||
float b = 2.0 * dot(origin, dir);
|
||||
float c = dot(origin, origin) - radius * radius;
|
||||
float d = b * b - 4.0 * a * c;
|
||||
if (d < 0.0) {
|
||||
return -1.0;
|
||||
}
|
||||
return (-b + sqrt(d)) / (2.0 * a);
|
||||
}
|
||||
|
||||
vec3 get_sky_color(vec3 dir) {
|
||||
vec3 eye = vec3(0.0, PLANET_RADIUS + 1.0, 0.0);
|
||||
float dist = ray_sphere_intersect(eye, dir, ATMOSPHERE_RADIUS);
|
||||
vec3 totalRayleigh = vec3(0.0);
|
||||
vec3 totalMie = vec3(0.0);
|
||||
float stepSize = dist / float(SAMPLES);
|
||||
for (int i = 0; i < SAMPLES; i++) {
|
||||
vec3 p = eye + dir * (float(i) + 0.5) * stepSize;
|
||||
float h = length(p) - PLANET_RADIUS;
|
||||
float rayleighDensity = exp(-h / RAYLEIGH_SCALE_HEIGHT);
|
||||
float mieDensity = exp(-h / MIE_SCALE_HEIGHT);
|
||||
float lightDist = ray_sphere_intersect(p, normalize(u_SunPos), ATMOSPHERE_RADIUS);
|
||||
float lightStepSize = lightDist / float(LIGHT_SAMPLES);
|
||||
float opticalDepthLight = 0.0;
|
||||
for (int j = 0; j < LIGHT_SAMPLES; j++) {
|
||||
vec3 lp = p + normalize(u_SunPos) * (float(j) + 0.5) * lightStepSize;
|
||||
float lh = length(lp) - PLANET_RADIUS;
|
||||
opticalDepthLight += exp(-lh / RAYLEIGH_SCALE_HEIGHT) * lightStepSize;
|
||||
}
|
||||
vec3 transmittance = exp(-(opticalDepthLight * RAYLEIGH_SCATTERING_COEFF
|
||||
+ opticalDepthLight * MIE_SCATTERING_COEFF));
|
||||
totalRayleigh += rayleighDensity * stepSize * transmittance;
|
||||
totalMie += mieDensity * stepSize * transmittance;
|
||||
}
|
||||
|
||||
float mu = dot(dir, normalize(u_SunPos));
|
||||
float rayleighPhase = 3.0 / (16.0 * PI) * (1.0 + mu * mu);
|
||||
float miePhase = 3.0 / (8.0 * PI) * ((1.0 - MIE_G * MIE_G)
|
||||
* (1.0 + mu * mu)) / ((2.0 + MIE_G * MIE_G) * pow(1.0 + MIE_G
|
||||
* MIE_G - 2.0 * MIE_G * mu, 1.5));
|
||||
return 20.0 * (totalRayleigh * RAYLEIGH_SCATTERING_COEFF * rayleighPhase
|
||||
+ totalMie * MIE_SCATTERING_COEFF * miePhase);
|
||||
}
|
||||
|
||||
void main() {
|
||||
float u = atan(TexCoords.z, TexCoords.x) / (2.0 * PI) + 0.5;
|
||||
float v = asin(TexCoords.y) / PI + 0.5;
|
||||
float cloudAlpha = texture(u_CloudTexture, vec2(u,v)).r;
|
||||
|
||||
vec3 skyColor = get_sky_color(normalize(TexCoords));
|
||||
vec3 cloudColor = mix(skyColor, vec3(1.0), smoothstep(0.5, 0.8, cloudAlpha));
|
||||
|
||||
float poleFade = 1.0 - abs(TexCoords.y);
|
||||
float finalAlpha = smoothstep(0.0, 0.2, poleFade) * smoothstep(0.5, 0.8, cloudAlpha);
|
||||
|
||||
FragColor = vec4(pow(cloudColor, vec3(1.0/2.2)), finalAlpha);
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
|
||||
out vec3 TexCoords;
|
||||
|
||||
uniform float u_Time;
|
||||
uniform float u_ScrollSpeed;
|
||||
uniform mat4 model;
|
||||
uniform mat4 view;
|
||||
uniform mat4 projection;
|
||||
|
||||
void main() {
|
||||
TexCoords = aPos;
|
||||
|
||||
/* Rotate texture coords around Y axis for scrolling. */
|
||||
float angle = u_Time * u_ScrollSpeed;
|
||||
mat3 rot = mat3(cos(angle), 0, sin(angle), 0, 1, 0, -sin(angle), 0, cos(angle));
|
||||
TexCoords = rot * TexCoords;
|
||||
|
||||
gl_Position = projection * view * model * vec4(aPos, 1.0);
|
||||
}
|
||||
8
assets/shaders/shape.frag
Normal file
8
assets/shaders/shape.frag
Normal file
@ -0,0 +1,8 @@
|
||||
#version 330 core
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec3 ourColor;
|
||||
|
||||
void main() {
|
||||
FragColor = vec4(ourColor, 1.0);
|
||||
}
|
||||
12
assets/shaders/shape.vert
Normal file
12
assets/shaders/shape.vert
Normal file
@ -0,0 +1,12 @@
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 aPos;
|
||||
layout (location = 1) in vec3 aColor;
|
||||
|
||||
uniform mat4 projection;
|
||||
|
||||
out vec3 ourColor;
|
||||
|
||||
void main() {
|
||||
gl_Position = projection * vec4(aPos.x, aPos.y, 0.0, 1.0);
|
||||
ourColor = aColor;
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
#version 330 core
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec3 FragPos;
|
||||
in vec3 Normal;
|
||||
in vec3 WorldPos;
|
||||
in float Detail;
|
||||
|
||||
uniform vec3 objectColor;
|
||||
uniform vec3 lightColor;
|
||||
uniform vec3 lightDir; /* Normalised direction vector for the light source. */
|
||||
uniform bool u_IsTerrain;
|
||||
|
||||
void main() {
|
||||
vec3 finalColor;
|
||||
vec3 norm = normalize(Normal);
|
||||
|
||||
/* Define colours for different terrain types. */
|
||||
if(u_IsTerrain) {
|
||||
vec3 grassColor1 = vec3(0.4, 0.6, 0.2);
|
||||
vec3 grassColor2 = vec3(0.3, 0.5, 0.15);
|
||||
vec3 rockColor = vec3(0.5, 0.5, 0.4);
|
||||
vec3 steepColor = vec3(0.35, 0.3, 0.25);
|
||||
|
||||
/* Create patchy grass colur using the detail noise. */
|
||||
float detailFactor = smoothstep(-0.2, 0.2, Detail);
|
||||
vec3 finalGrassColor = mix(grassColor1, grassColor2, detailFactor);
|
||||
|
||||
/* Blend from grass to rock between height of 2.0 and 4.0. */
|
||||
float heightFactor = smoothstep(2.0, 4.0, WorldPos.y);
|
||||
finalColor = mix(finalGrassColor, rockColor, heightFactor);
|
||||
|
||||
/* Flat surface has a normal.y of 1.0. A steep slope is closer to 0. */
|
||||
float slopeFactor = 1.0 - smoothstep(0.6, 0.8, norm.y);
|
||||
finalColor = mix(finalColor, steepColor, slopeFactor);
|
||||
|
||||
} else {
|
||||
finalColor = objectColor;
|
||||
}
|
||||
|
||||
/* Standard lighting calculations. */
|
||||
float ambientStrength = 0.4;
|
||||
vec3 ambient = ambientStrength * lightColor;
|
||||
|
||||
float diff = max(dot(norm, normalize(-lightDir)), 0.0);
|
||||
vec3 diffuse = diff * lightColor;
|
||||
|
||||
vec3 result = (ambient + diffuse) * finalColor;
|
||||
FragColor = vec4(result, 1.0);
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in vec3 aNormal;
|
||||
layout (location = 2) in float aDetail;
|
||||
|
||||
out vec3 FragPos;
|
||||
out vec3 Normal;
|
||||
out vec3 WorldPos;
|
||||
out float Detail;
|
||||
|
||||
uniform mat4 model;
|
||||
uniform mat4 view;
|
||||
uniform mat4 projection;
|
||||
|
||||
void main() {
|
||||
/* Pass position and normal vectors to the fragment shader in world space. */
|
||||
FragPos = vec3(model * vec4(aPos, 1.0));
|
||||
WorldPos = FragPos;
|
||||
Normal = mat3(transpose(inverse(model))) * aNormal;
|
||||
Detail = aDetail;
|
||||
|
||||
gl_Position = projection * view * vec4(FragPos, 1.0);
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
#version 330 core
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec3 v_WorldPos;
|
||||
|
||||
uniform vec3 u_SunPos;
|
||||
|
||||
const float PI = 3.14159265359;
|
||||
|
||||
/* Atmosphere parameters. */
|
||||
const float ATMOSPHERE_RADIUS = 6520275.0;
|
||||
const float PLANET_RADIUS = 6371000.0;
|
||||
const float RAYLEIGH_SCALE_HEIGHT = 8000.0;
|
||||
const float MIE_SCALE_HEIGHT = 1200.0;
|
||||
|
||||
/* Scattering coefficients. */
|
||||
const vec3 RAYLEIGH_SCATTERING_COEFF = vec3(0.0000058, 0.0000135, 0.0000331);
|
||||
const vec3 MIE_SCATTERING_COEFF = vec3(0.00002);
|
||||
|
||||
/* Mie scattering parameters. */
|
||||
const float MIE_G = 0.76;
|
||||
|
||||
/* Number of samples for ray marching. */
|
||||
const int SAMPLES = 16;
|
||||
const int LIGHT_SAMPLES = 8;
|
||||
|
||||
float ray_sphere_intersect(vec3 origin, vec3 dir, float radius) {
|
||||
float a = dot(dir, dir);
|
||||
float b = 2.0 * dot(origin, dir);
|
||||
float c = dot(origin, origin) - radius * radius;
|
||||
float d = b * b - 4.0 * a * c;
|
||||
if (d < 0.0) {
|
||||
return -1.0;
|
||||
}
|
||||
return (-b + sqrt(d)) / (2.0 * a);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 dir = normalize(v_WorldPos);
|
||||
vec3 eye = vec3(0.0, PLANET_RADIUS + 1.0, 0.0);
|
||||
|
||||
float dist = ray_sphere_intersect(eye, dir, ATMOSPHERE_RADIUS);
|
||||
|
||||
vec3 totalRayleigh = vec3(0.0);
|
||||
vec3 totalMie = vec3(0.0);
|
||||
|
||||
float stepSize = dist / float(SAMPLES);
|
||||
for(int i = 0; i < SAMPLES; i++) {
|
||||
vec3 p = eye + dir * (float(i) + 0.5) * stepSize;
|
||||
float h = length(p) - PLANET_RADIUS;
|
||||
|
||||
float rayleighDensity = exp(-h / RAYLEIGH_SCALE_HEIGHT);
|
||||
float mieDensity = exp(-h / MIE_SCALE_HEIGHT);
|
||||
|
||||
float lightDist = ray_sphere_intersect(p, normalize(u_SunPos), ATMOSPHERE_RADIUS);
|
||||
float lightStepSize = lightDist / float(LIGHT_SAMPLES);
|
||||
float opticalDepthLight = 0.0;
|
||||
for(int j = 0; j < LIGHT_SAMPLES; j++) {
|
||||
vec3 lp = p + normalize(u_SunPos) * (float(j) + 0.5) * lightStepSize;
|
||||
float lh = length(lp) - PLANET_RADIUS;
|
||||
opticalDepthLight += exp(-lh / RAYLEIGH_SCALE_HEIGHT) * lightStepSize;
|
||||
}
|
||||
|
||||
vec3 transmittance = exp(-(opticalDepthLight * RAYLEIGH_SCATTERING_COEFF
|
||||
+ opticalDepthLight * MIE_SCATTERING_COEFF));
|
||||
totalRayleigh += rayleighDensity * stepSize * transmittance;
|
||||
totalMie += mieDensity * stepSize * transmittance;
|
||||
}
|
||||
|
||||
float mu = dot(dir, normalize(u_SunPos));
|
||||
float rayleighPhase = 3.0 / (16.0 * PI) * (1.0 + mu * mu);
|
||||
float miePhase = 3.0 / (8.0 * PI) * ((1.0 - MIE_G * MIE_G)
|
||||
* (1.0 + mu * mu)) / ((2.0 + MIE_G * MIE_G)
|
||||
* pow(1.0 + MIE_G * MIE_G - 2.0 * MIE_G * mu, 1.5));
|
||||
|
||||
vec3 finalColor = 20.0 * (totalRayleigh * RAYLEIGH_SCATTERING_COEFF
|
||||
* rayleighPhase + totalMie * MIE_SCATTERING_COEFF * miePhase);
|
||||
|
||||
FragColor = vec4(pow(finalColor, vec3(1.0/2.2)), 1.0);
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 aPos;
|
||||
|
||||
uniform mat4 view;
|
||||
uniform mat4 projection;
|
||||
|
||||
out vec3 v_WorldPos;
|
||||
|
||||
void main() {
|
||||
v_WorldPos = aPos;
|
||||
vec4 pos = projection * view * vec4(aPos, 1.0);
|
||||
gl_Position = pos.xyww;
|
||||
}
|
||||
11
assets/shaders/text.frag
Normal file
11
assets/shaders/text.frag
Normal file
@ -0,0 +1,11 @@
|
||||
#version 330 core
|
||||
in vec2 TexCoords;
|
||||
in vec3 ourColor;
|
||||
out vec4 color;
|
||||
|
||||
uniform sampler2D text;
|
||||
|
||||
void main() {
|
||||
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
|
||||
color = vec4(ourColor, 1.0) * sampled;
|
||||
}
|
||||
15
assets/shaders/text.vert
Normal file
15
assets/shaders/text.vert
Normal file
@ -0,0 +1,15 @@
|
||||
#version 330 core
|
||||
layout (location = 0) in vec2 aPos;
|
||||
layout (location = 1) in vec2 aTexCoords;
|
||||
layout (location = 2) in vec3 aColor;
|
||||
|
||||
out vec2 TexCoords;
|
||||
out vec3 ourColor;
|
||||
|
||||
uniform mat4 projection;
|
||||
|
||||
void main() {
|
||||
gl_Position = projection * vec4(aPos, 0.0, 1.0);
|
||||
TexCoords = aTexCoords;
|
||||
ourColor = aColor;
|
||||
}
|
||||
25
client/CMakeLists.txt
Normal file
25
client/CMakeLists.txt
Normal file
@ -0,0 +1,25 @@
|
||||
file(GLOB_RECURSE CLIENT_SOURCES "src/*.cpp")
|
||||
# Explicitly list server sources needed for single-player to avoid main() conflict.
|
||||
set(SERVER_SOURCES
|
||||
"../server/src/network_manager.cpp"
|
||||
"../server/src/player.cpp"
|
||||
)
|
||||
|
||||
add_executable(bettolac
|
||||
${CLIENT_SOURCES}
|
||||
${SERVER_SOURCES}
|
||||
)
|
||||
|
||||
find_package(SDL3 REQUIRED)
|
||||
find_package(GLEW REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(Freetype REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
target_link_libraries(bettolac PRIVATE bettola SDL3::SDL3 ${GLEW_LIBRARIES}
|
||||
${OPENGL_LIBRARIES} ${FREETYPE_LIBRARIES} Threads::Threads)
|
||||
|
||||
target_include_directories(bettolac PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
# Add server include dir for single player mode.
|
||||
${CMAKE_SOURCE_DIR}/server/src
|
||||
${GLEW_INCLUDE_DIRS} ${OPENGL_INCLUDE_DIR} ${FREETYPE_INCLUDE_DIRS})
|
||||
101
client/src/client_network.cpp
Normal file
101
client/src/client_network.cpp
Normal file
@ -0,0 +1,101 @@
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include "net/tcp_connection.h"
|
||||
|
||||
#include "client_network.h"
|
||||
|
||||
ClientNetwork::ClientNetwork(void) : _io_context() {}
|
||||
|
||||
ClientNetwork::~ClientNetwork(void) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
bool ClientNetwork::connect(const std::string& host, uint16_t port) {
|
||||
try {
|
||||
/* Resolve hostname into list of endpoints. */
|
||||
asio::ip::tcp::resolver resolver(_io_context);
|
||||
asio::ip::tcp::resolver::results_type endpoints =
|
||||
resolver.resolve(host, std::to_string(port));
|
||||
|
||||
/* Create connection object. It will own a socket. */
|
||||
_connection = std::make_shared<net::TcpConnection>(_io_context,
|
||||
asio::ip::tcp::socket(_io_context));
|
||||
|
||||
/* Connect to server. */
|
||||
asio::connect(_connection->socket(), endpoints);
|
||||
|
||||
if(!_connection->socket().is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Define callbacks for the connection. */
|
||||
_connection->start(
|
||||
[this](const std::string& msg) {
|
||||
/* Push new messages to the thread safe queue. */
|
||||
this->_incoming_messages.push_back(msg);
|
||||
},
|
||||
[this]() {
|
||||
/* Reset our connection on server disconnect. */
|
||||
fprintf(stderr, "[BettolaClient] Server disconnected.\n");
|
||||
this->_connection.reset();
|
||||
});
|
||||
|
||||
/* Start io_context in background thread. */
|
||||
_context_thread = std::thread([this]() { _io_context.run(); });
|
||||
|
||||
} catch(const std::exception& e) {
|
||||
fprintf(stderr, "[BettolaClient] Connection exception: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClientNetwork::disconnect(void) {
|
||||
/* Stop the context, should cause the io_context.run() call in the
|
||||
* background thread to return.
|
||||
*/
|
||||
_io_context.stop();
|
||||
|
||||
/* Wait for the background thread to finish. */
|
||||
if(_context_thread.joinable()) {
|
||||
_context_thread.join();
|
||||
}
|
||||
|
||||
/* Should be safe to close the socket now that thread is stoped <questionmark> */
|
||||
if(is_connected()) {
|
||||
/* Close the socket. Causes outstanding async operations
|
||||
* in TcpConnection to compete with an error, which in return
|
||||
* triggers the on_disconnect callback.
|
||||
*/
|
||||
_connection->socket().close();
|
||||
}
|
||||
|
||||
/* Kill background thread. */
|
||||
_io_context.stop();
|
||||
if(_context_thread.joinable()) {
|
||||
_context_thread.join();
|
||||
}
|
||||
|
||||
/* Release connection object. */
|
||||
_connection.reset();
|
||||
}
|
||||
|
||||
bool ClientNetwork::is_connected(void) {
|
||||
return _connection && _connection->socket().is_open();
|
||||
}
|
||||
|
||||
void ClientNetwork::send(const std::string& message) {
|
||||
if(is_connected()) {
|
||||
_connection->send(message);
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientNetwork::poll_message(std::string& message) {
|
||||
if(_incoming_messages.empty()) {
|
||||
return false;
|
||||
}
|
||||
message = _incoming_messages.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
30
client/src/client_network.h
Normal file
30
client/src/client_network.h
Normal file
@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <asio.hpp>
|
||||
#include <memory.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "asio/io_context.hpp"
|
||||
#include "net/tcp_connection.h"
|
||||
#include "net/ts_queue.h"
|
||||
|
||||
class ClientNetwork {
|
||||
public:
|
||||
ClientNetwork(void);
|
||||
~ClientNetwork(void);
|
||||
|
||||
bool connect(const std::string& host, uint16_t port);
|
||||
void disconnect(void);
|
||||
bool is_connected(void);
|
||||
|
||||
void send(const std::string& message);
|
||||
|
||||
bool poll_message(std::string& message);
|
||||
|
||||
private:
|
||||
asio::io_context _io_context;
|
||||
std::thread _context_thread;
|
||||
std::shared_ptr<net::TcpConnection> _connection;
|
||||
TsQueue<std::string> _incoming_messages;
|
||||
};
|
||||
52
client/src/debug/debug_overlay.cpp
Normal file
52
client/src/debug/debug_overlay.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
#include <cstdio>
|
||||
|
||||
#include "debug_overlay.h"
|
||||
#include "ui/ui_renderer.h"
|
||||
#include "gfx/types.h"
|
||||
|
||||
DebugOverlay::DebugOverlay(void) :
|
||||
_fps(0.0f),
|
||||
_frame_time(0.0f),
|
||||
_draw_calls(0),
|
||||
_shape_vertices(0),
|
||||
_text_vertices(0),
|
||||
_update_timer(0.0f) {}
|
||||
|
||||
void DebugOverlay::update(float dt, int draw_calls, int shape_verts, int txt_verts) {
|
||||
_update_timer += dt;
|
||||
/* Only update a few times per sec to keep them readable. */
|
||||
if(_update_timer > 0.2f) {
|
||||
_update_timer = 0.0f;
|
||||
_frame_time = dt * 1000.0f;
|
||||
_fps = 1.0f / dt;
|
||||
_draw_calls = draw_calls;
|
||||
_shape_vertices = shape_verts;
|
||||
_text_vertices = txt_verts;
|
||||
}
|
||||
}
|
||||
|
||||
void DebugOverlay::render(UIRenderer* ui_renderer) {
|
||||
const Color text_color = { 0.0f, 1.0f, 0.0f }; /* Bright green. */
|
||||
const float line_height = 18.0f;
|
||||
const float padding = 5.0f;
|
||||
|
||||
char buffer[256];
|
||||
|
||||
/* Manages its own rendering batch. */
|
||||
ui_renderer->begin_shapes();
|
||||
ui_renderer->begin_text();
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "FPS: %.0f (%.2f ms)", _fps, _frame_time);
|
||||
ui_renderer->render_text(buffer, padding, padding+line_height*1, text_color);
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "Draw Calls: %d", _draw_calls);
|
||||
ui_renderer->render_text(buffer, padding, padding+line_height*2, text_color);
|
||||
|
||||
snprintf(buffer, sizeof(buffer), "Vertices: %d (Shape %d, Text: %d)",
|
||||
_shape_vertices + _text_vertices, _shape_vertices, _text_vertices);
|
||||
ui_renderer->render_text(buffer, padding, padding+line_height*3, text_color);
|
||||
|
||||
ui_renderer->flush_shapes();
|
||||
ui_renderer->flush_text();
|
||||
}
|
||||
|
||||
24
client/src/debug/debug_overlay.h
Normal file
24
client/src/debug/debug_overlay.h
Normal file
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
class UIRenderer;
|
||||
|
||||
/**
|
||||
* Renders real-time performance metrics on screen.
|
||||
*/
|
||||
class DebugOverlay {
|
||||
public:
|
||||
DebugOverlay(void);
|
||||
|
||||
void update(float dt, int draw_calls, int shape_verts, int txt_verts);
|
||||
void render(UIRenderer* ui_renderer);
|
||||
|
||||
private:
|
||||
float _fps;
|
||||
float _frame_time;
|
||||
int _draw_calls;
|
||||
int _shape_vertices;
|
||||
int _text_vertices;
|
||||
|
||||
/* Oh? You want to be able to read the stats?! */
|
||||
float _update_timer;
|
||||
};
|
||||
5
client/src/debug/debug_stats.cpp
Normal file
5
client/src/debug/debug_stats.cpp
Normal file
@ -0,0 +1,5 @@
|
||||
#include "debug_stats.h"
|
||||
|
||||
int DebugStats::draw_calls = 0;
|
||||
int DebugStats::shape_vertices = 0;
|
||||
int DebugStats::text_vertices = 0;
|
||||
14
client/src/debug/debug_stats.h
Normal file
14
client/src/debug/debug_stats.h
Normal file
@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
struct DebugStats {
|
||||
static int draw_calls;
|
||||
static int shape_vertices;
|
||||
static int text_vertices;
|
||||
|
||||
static void reset(void) {
|
||||
draw_calls = 0;
|
||||
shape_vertices = 0;
|
||||
text_vertices = 0;
|
||||
}
|
||||
};
|
||||
|
||||
371
client/src/game_state.cpp
Normal file
371
client/src/game_state.cpp
Normal file
@ -0,0 +1,371 @@
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
#include "gfx/types.h"
|
||||
#include "net/constants.h"
|
||||
#include "net/message_protocol.h"
|
||||
#include "client_network.h"
|
||||
#include "network_manager.h"
|
||||
#include "game_state.h"
|
||||
#include "terminal.h"
|
||||
#include "ui/desktop.h"
|
||||
#include "ui/ui_window.h"
|
||||
#include "ui/editor.h"
|
||||
#include "ui/login_screen.h"
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <ui/main_menu.h>
|
||||
#include <ui/boot_sequence.h>
|
||||
#include "debug/debug_overlay.h"
|
||||
|
||||
void GameState::_init_desktop(void) {
|
||||
_desktop = std::make_unique<Desktop>(_screen_width, _screen_height, this, _initial_session_id);
|
||||
|
||||
auto term = std::make_unique<Terminal>(this);
|
||||
term->set_session_id(_initial_session_id);
|
||||
|
||||
auto term_window = std::make_unique<UIWindow>("Terminal", 100, 100, 800, 500);
|
||||
term_window->set_session_id(_initial_session_id);
|
||||
UIWindow* window_ptr = term_window.get();
|
||||
term_window->set_content(std::move(term));
|
||||
_desktop->add_window(std::move(term_window));
|
||||
_desktop->register_session(_initial_session_id, window_ptr);
|
||||
}
|
||||
|
||||
void GameState::_run_server(void) {
|
||||
try {
|
||||
NetworkManager server("bettola_sp.db");
|
||||
server.start(SINGLE_PLAYER_PORT);
|
||||
/*
|
||||
* Server's start() method is non-blocking, but NetworkManager
|
||||
* object must be kept alive. We'll just loop forever and let the OS
|
||||
* clean up the thread when main exits ;)
|
||||
*/
|
||||
while(true) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(5));
|
||||
}
|
||||
} catch(const std::exception& e) {
|
||||
fprintf(stderr, "Single-player server thread exception: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
GameState::GameState(void) :
|
||||
_current_screen(Screen::MAIN_MENU),
|
||||
_screen_width(0),
|
||||
_screen_height(0),
|
||||
_is_single_player(false),
|
||||
_show_debug_overlay(false),
|
||||
_initial_session_id(0) {_debug_overlay = std::make_unique<DebugOverlay>();}
|
||||
|
||||
GameState::~GameState(void) = default;
|
||||
|
||||
void GameState::init(int screen_width, int screen_height) {
|
||||
_screen_width = screen_width;
|
||||
_screen_height = screen_height;
|
||||
/* Create and connect the network client. */
|
||||
_network = std::make_unique<ClientNetwork>();
|
||||
|
||||
_main_menu = std::make_unique<MainMenu>(screen_width, screen_height);
|
||||
}
|
||||
|
||||
void GameState::start_single_player_now(int screen_width, int screen_height) {
|
||||
_is_single_player = true;
|
||||
_screen_width = screen_width;
|
||||
_screen_height = screen_height;
|
||||
fprintf(stdout, "Starting in single-player mode...\n");
|
||||
std::thread server_thread(&GameState::_run_server, this);
|
||||
server_thread.detach();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
_network = std::make_unique<ClientNetwork>();
|
||||
if(!_network->connect("127.0.0.1", SINGLE_PLAYER_PORT)) {
|
||||
/* TODO: Handle connection failure. */
|
||||
}
|
||||
_current_screen = Screen::DESKTOP;
|
||||
_init_desktop();
|
||||
}
|
||||
|
||||
void GameState::handle_event(SDL_Event* event, int screen_width, int screen_height) {
|
||||
if(event->type == SDL_EVENT_KEY_DOWN && event->key.key == SDLK_D &&
|
||||
(event->key.mod & SDL_KMOD_CTRL)) {
|
||||
_show_debug_overlay = !_show_debug_overlay;
|
||||
return; /* Consume the event. */
|
||||
}
|
||||
switch(_current_screen) {
|
||||
case Screen::MAIN_MENU:
|
||||
if(_main_menu) {
|
||||
_main_menu->handle_event(event);
|
||||
}
|
||||
break;
|
||||
case Screen::LOGIN:
|
||||
if(_login_screen) {
|
||||
_login_screen->handle_event(event);
|
||||
}
|
||||
break;
|
||||
case Screen::BOOTING:
|
||||
/* TODO: */
|
||||
break;
|
||||
case Screen::DESKTOP:
|
||||
if(_desktop) {
|
||||
_desktop->handle_event(event, screen_width, screen_height);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GameState::update(float dt, int draw_calls, int shape_verts, int text_verts) {
|
||||
_debug_overlay->update(dt, draw_calls, shape_verts, text_verts);
|
||||
switch(_current_screen) {
|
||||
case Screen::MAIN_MENU: {
|
||||
if(!_main_menu) break;
|
||||
Screen next_screen = _main_menu->update(dt);
|
||||
if(next_screen != Screen::MAIN_MENU) {
|
||||
const MenuButton* clicked_button = _main_menu->get_clicked_button();
|
||||
if(clicked_button) {
|
||||
_is_single_player = clicked_button->is_single_player;
|
||||
}
|
||||
_current_screen = next_screen;
|
||||
_main_menu.reset(); /* Free mem. */
|
||||
|
||||
if(_current_screen == Screen::LOGIN) {
|
||||
/* Connect to server. */
|
||||
if(_is_single_player) {
|
||||
fprintf(stdout, "Starting in single-player mode...\n");
|
||||
std::thread server_thread(&GameState::_run_server, this);
|
||||
server_thread.detach();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
if(!_network->connect("127.0.0.1", SINGLE_PLAYER_PORT)) {
|
||||
/* TODO: Handle connection failure. */
|
||||
}
|
||||
} else {
|
||||
if(!_network->connect("127.0.0.1", MULTIPLAYER_PORT)) {
|
||||
/* TODO: Handle connection failure. */
|
||||
}
|
||||
}
|
||||
_login_screen = std::make_unique<LoginScreen>(_screen_width, _screen_height);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Screen::LOGIN: {
|
||||
if(_login_screen && _login_screen->is_login_attempted()) {
|
||||
std::string username = _login_screen->get_username();
|
||||
std::string password = _login_screen->get_password();
|
||||
|
||||
if(_login_screen->is_new_account_mode()) {
|
||||
std::string hostname = _login_screen->get_hostname();
|
||||
_network->send(net_protocol::build_message(net_protocol::Opcode::C2S_CREATE_ACCOUNT,
|
||||
{username, password, hostname}));
|
||||
} else {
|
||||
_network->send(net_protocol::build_message(net_protocol::Opcode::C2S_LOGIN,
|
||||
{username, password}));
|
||||
}
|
||||
_login_screen->clear_login_attempt(); /* Try to spam my server now b.tch! */
|
||||
}
|
||||
|
||||
/* Check for server response to our login attempt. */
|
||||
std::string server_msg;
|
||||
while(_network->poll_message(server_msg)) {
|
||||
net_protocol::Opcode opcode;
|
||||
std::vector<std::string> args;
|
||||
net_protocol::parse_message(server_msg, opcode, args);
|
||||
|
||||
switch(opcode) {
|
||||
case net_protocol::Opcode::S2C_LOGIN_SUCCESS:
|
||||
case net_protocol::Opcode::S2C_CREATE_ACCOUNT_SUCCESS:
|
||||
/* Don't switch screen yet, wait for the session ID. */
|
||||
break;
|
||||
case net_protocol::Opcode::S2C_SESSION_CREATED:
|
||||
if(!args.empty()) {
|
||||
_initial_session_id = std::stoul(args[0]);
|
||||
}
|
||||
_current_screen = Screen::BOOTING;
|
||||
_login_screen.reset(); /* Free mem. */
|
||||
_boot_sequence = std::make_unique<BootSequence>();
|
||||
break;
|
||||
case net_protocol::Opcode::S2C_LOGIN_FAIL:
|
||||
_login_screen->set_error_message(args.empty() ? "Login failed." : args[0]);
|
||||
break;
|
||||
case net_protocol::Opcode::S2C_CREATE_ACCOUNT_FAIL:
|
||||
_login_screen->set_error_message(args.empty() ? "Account creation failed." :
|
||||
args[0]);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Recieved unexpected opcode %d during login.\n",
|
||||
static_cast<int>(opcode));
|
||||
break;
|
||||
}
|
||||
/* If we successfully changed screen, stop processing messages for this frame. */
|
||||
if(_current_screen == Screen::BOOTING) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Screen::BOOTING: {
|
||||
if(!_boot_sequence) break; /* Shouldn't happen. */
|
||||
if(_boot_sequence->is_finished()) {
|
||||
_current_screen = Screen::DESKTOP;
|
||||
_init_desktop();
|
||||
_boot_sequence.reset(); /* Free mem. */
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Screen::DESKTOP: {
|
||||
std::string server_msg;
|
||||
while(_network->poll_message(server_msg)) {
|
||||
net_protocol::Opcode opcode;
|
||||
std::vector<std::string> args;
|
||||
net_protocol::parse_message(server_msg, opcode, args);
|
||||
|
||||
switch(opcode) {
|
||||
case net_protocol::Opcode::S2C_SESSION_CREATED:
|
||||
if(!args.empty()) {
|
||||
uint32_t session_id = std::stoul(args[0]);
|
||||
UIWindow* new_term = _desktop->get_window_awaiting_session_id();
|
||||
if(new_term) {
|
||||
Terminal* term = dynamic_cast<Terminal*>(new_term->get_content());
|
||||
if(term) {
|
||||
new_term->set_session_id(session_id);
|
||||
term->set_session_id(session_id);
|
||||
_desktop->register_session(session_id, new_term);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case net_protocol::Opcode::S2C_FILE_DATA:
|
||||
if(args.size() == 3) {
|
||||
uint32_t session_id = std::stoul(args[0]);
|
||||
auto editor = std::make_unique<Editor>(args[1]);
|
||||
editor->set_buffer_content(args[2]);
|
||||
auto editor_window = std::make_unique<UIWindow>(args[1].c_str(),
|
||||
200, 200, 600, 400);
|
||||
editor_window->set_session_id(session_id);
|
||||
editor_window->set_content(std::move(editor));
|
||||
_desktop->add_window(std::move(editor_window));
|
||||
}
|
||||
break;
|
||||
case net_protocol::Opcode::S2C_DISCONNECT: {
|
||||
if(!args.empty()) {
|
||||
uint32_t session_id = std::stoul(args[0]);
|
||||
UIWindow* window = _desktop->get_window_by_session_id(session_id);
|
||||
if(window) {
|
||||
Terminal* terminal = dynamic_cast<Terminal*>(window->get_content());
|
||||
if(terminal) {
|
||||
terminal->add_history("Connection closed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case net_protocol::Opcode::S2C_CLOSE_WINDOW: {
|
||||
if(!args.empty()) {
|
||||
uint32_t session_id = std::stoul(args[0]);
|
||||
UIWindow* window = _desktop->get_window_by_session_id(session_id);
|
||||
if(window) {
|
||||
window->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case net_protocol::Opcode::S2C_COMMAND_RESPONSE: {
|
||||
if(args.size() == 2) {
|
||||
uint32_t session_id = std::stoul(args[0]);
|
||||
UIWindow* window = _desktop->get_window_by_session_id(session_id);
|
||||
if(window) {
|
||||
Terminal* terminal = dynamic_cast<Terminal*>(window->get_content());
|
||||
if(terminal) {
|
||||
/* Server sends "output\nprompt", split them. */
|
||||
size_t last_newline = args[1].find_last_of('\n');
|
||||
if(last_newline != std::string::npos) {
|
||||
std::string prompt = args[1].substr(last_newline+1);
|
||||
terminal->set_prompt(prompt);
|
||||
|
||||
std::string output = args[1].substr(0, last_newline);
|
||||
if(!output.empty()) {
|
||||
/* Split multiline output and push each line to history. */
|
||||
std::stringstream ss(output);
|
||||
std::string line;
|
||||
while(std::getline(ss, line, '\n')) {
|
||||
terminal->add_history(line);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
terminal->add_history(args[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(_desktop) {
|
||||
_desktop->update(dt, _screen_width, _screen_height);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameState::render(const RenderContext& context) {
|
||||
switch(_current_screen) {
|
||||
case Screen::MAIN_MENU:
|
||||
if(_main_menu) {
|
||||
_main_menu->render(context.ui_renderer);
|
||||
}
|
||||
break;
|
||||
case Screen::LOGIN:
|
||||
if(_login_screen) {
|
||||
_login_screen->render(context);
|
||||
}
|
||||
break;
|
||||
case Screen::BOOTING:
|
||||
if(_boot_sequence) {
|
||||
_boot_sequence->render(context.ui_renderer);
|
||||
}
|
||||
break;
|
||||
case Screen::DESKTOP:
|
||||
if(_desktop) {
|
||||
_desktop->render(context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if(_show_debug_overlay) {
|
||||
_debug_overlay->render(context.ui_renderer);
|
||||
}
|
||||
}
|
||||
|
||||
void GameState::send_network_command(uint32_t session_id, const std::string& command) {
|
||||
if(_network && _network->is_connected()) {
|
||||
_network->send(net_protocol::build_message(net_protocol::Opcode::C2S_COMMAND,
|
||||
{std::to_string(session_id), command}));
|
||||
}
|
||||
}
|
||||
|
||||
void GameState::send_file_write_request(uint32_t session_id, const std::string& path,
|
||||
const std::string& content) {
|
||||
_network->send(net_protocol::build_message(net_protocol::Opcode::C2S_WRITE_FILE,
|
||||
{std::to_string(session_id), path, content}));
|
||||
}
|
||||
|
||||
void GameState::send_file_read_request(uint32_t session_id, const std::string& path) {
|
||||
_network->send(net_protocol::build_message(net_protocol::Opcode::C2S_READ_FILE,
|
||||
{std::to_string(session_id), path}));
|
||||
}
|
||||
|
||||
void GameState::send_build_file_request(uint32_t session_id, const std::string& path,
|
||||
const std::string& content) {
|
||||
_network->send(net_protocol::build_message(net_protocol::Opcode::C2S_BUILD_FILE,
|
||||
{std::to_string(session_id), path, content}));
|
||||
}
|
||||
|
||||
void GameState::send_create_session_request(void) {
|
||||
if(_network && _network->is_connected()) {
|
||||
_network->send(net_protocol::build_message(net_protocol::Opcode::C2S_CREATE_SESSION));
|
||||
}
|
||||
}
|
||||
58
client/src/game_state.h
Normal file
58
client/src/game_state.h
Normal file
@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "gfx/types.h"
|
||||
|
||||
class DebugOverlay;
|
||||
class ClientNetwork;
|
||||
class Desktop;
|
||||
class LoginScreen;
|
||||
class BootSequence;
|
||||
class MainMenu;
|
||||
class ShapeRenderer;
|
||||
class TextRenderer;
|
||||
union SDL_Event;
|
||||
|
||||
enum class Screen {
|
||||
LOGIN,
|
||||
MAIN_MENU,
|
||||
BOOTING,
|
||||
DESKTOP
|
||||
};
|
||||
|
||||
class GameState {
|
||||
public:
|
||||
GameState(void);
|
||||
~GameState(void);
|
||||
|
||||
void init(int screen_width, int screen_height);
|
||||
void start_single_player_now(int screen_width, int screen_height);
|
||||
void handle_event(SDL_Event* event, int screen_width, int screen_height);
|
||||
void update(float dt, int draw_calls, int shape_verts, int text_verts);
|
||||
void render(const RenderContext& context);
|
||||
|
||||
/* Public network interface for UI components. */
|
||||
void send_network_command(uint32_t session_id, const std::string& command);
|
||||
void send_file_write_request(uint32_t session_id, const std::string& path, const std::string& content);
|
||||
void send_build_file_request(uint32_t session_id, const std::string& path, const std::string& content);
|
||||
void send_file_read_request(uint32_t session_id, const std::string& path);
|
||||
void send_create_session_request(void);
|
||||
|
||||
private:
|
||||
std::unique_ptr<ClientNetwork> _network;
|
||||
std::unique_ptr<Desktop> _desktop;
|
||||
std::unique_ptr<BootSequence> _boot_sequence;
|
||||
std::unique_ptr<LoginScreen> _login_screen;
|
||||
std::unique_ptr<MainMenu> _main_menu;
|
||||
std::unique_ptr<DebugOverlay> _debug_overlay;
|
||||
bool _show_debug_overlay;
|
||||
Screen _current_screen;
|
||||
int _screen_width;
|
||||
int _screen_height;
|
||||
bool _is_single_player;
|
||||
uint32_t _initial_session_id;
|
||||
|
||||
void _init_desktop(void);
|
||||
void _run_server(void);
|
||||
};
|
||||
86
client/src/gfx/shader.cpp
Normal file
86
client/src/gfx/shader.cpp
Normal file
@ -0,0 +1,86 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include <GL/glew.h>
|
||||
#include "shader.h"
|
||||
|
||||
std::string read_file(const char* path) {
|
||||
std::ifstream file(path);
|
||||
if(!file.is_open()) {
|
||||
printf("Failed to open file: %s\n", path);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
Shader::Shader(const char* vert_path, const char* frag_path) {
|
||||
std::string vert_string = read_file(vert_path);
|
||||
std::string frag_string = read_file(frag_path);
|
||||
if(vert_string.empty() || frag_string.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* vert_source = vert_string.c_str();
|
||||
const char* frag_source = frag_string.c_str();
|
||||
|
||||
unsigned int vert, frag;
|
||||
vert = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vert, 1, &vert_source, NULL);
|
||||
glCompileShader(vert);
|
||||
_check_compile_errors(vert, "VERTEX");
|
||||
|
||||
frag = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(frag, 1, &frag_source, NULL);
|
||||
glCompileShader(frag);
|
||||
_check_compile_errors(frag, "FRAGMENT");
|
||||
|
||||
id = glCreateProgram();
|
||||
glAttachShader(id, vert);
|
||||
glAttachShader(id, frag);
|
||||
glLinkProgram(id);
|
||||
_check_compile_errors(id, "PROGRAM");
|
||||
|
||||
glDeleteShader(vert);
|
||||
glDeleteShader(frag);
|
||||
}
|
||||
|
||||
void Shader::use(void) {
|
||||
glUseProgram(id);
|
||||
}
|
||||
|
||||
void Shader::set_mat4(const char* name, const float* value) {
|
||||
glUniformMatrix4fv(glGetUniformLocation(id, name), 1, GL_FALSE, value);
|
||||
}
|
||||
|
||||
void Shader::set_vec3(const char* name, float v1, float v2, float v3) {
|
||||
glUniform3f(glGetUniformLocation(id, name), v1, v2, v3);
|
||||
}
|
||||
|
||||
void Shader::set_int(const char* name, int value) {
|
||||
glUniform1i(glGetUniformLocation(id, name), value);
|
||||
}
|
||||
|
||||
void Shader::_check_compile_errors(unsigned int shader_id, const char* type) {
|
||||
int success;
|
||||
char info_log[1024];
|
||||
if(strcmp(type, "PROGRAM") != 0) {
|
||||
glGetShaderiv(shader_id, GL_COMPILE_STATUS, &success);
|
||||
if(!success) {
|
||||
glGetShaderInfoLog(shader_id, 1024, NULL, info_log);
|
||||
printf("Shader compilation error (%s):\n%s\n", type, info_log);
|
||||
}
|
||||
} else {
|
||||
glGetProgramiv(shader_id, GL_LINK_STATUS, &success);
|
||||
if(!success) {
|
||||
glGetProgramInfoLog(shader_id, 1024, NULL, info_log);
|
||||
printf("Shader linking error (%s):\n%s\n", type, info_log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
client/src/gfx/shader.h
Normal file
16
client/src/gfx/shader.h
Normal file
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
class Shader {
|
||||
public:
|
||||
unsigned int id;
|
||||
|
||||
Shader(const char* vertex_path, const char* fragment_path);
|
||||
|
||||
void use(void);
|
||||
void set_mat4(const char* name, const float* value);
|
||||
void set_vec3(const char* name, float v1, float v2, float v3);
|
||||
void set_int(const char* name, int value);
|
||||
|
||||
private:
|
||||
void _check_compile_errors(unsigned int shader, const char* type);
|
||||
};
|
||||
82
client/src/gfx/shape_renderer.cpp
Normal file
82
client/src/gfx/shape_renderer.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
#include <GL/glew.h>
|
||||
|
||||
#include "shape_renderer.h"
|
||||
#include "debug/debug_stats.h"
|
||||
#include "math/math.h"
|
||||
|
||||
ShapeRenderer::ShapeRenderer(unsigned int screen_width, unsigned int screen_height) {
|
||||
math::ortho_proj(_projection, 0.0f, (float)screen_width, 0.0f, (float)screen_height, -1.0f, 1.0f);
|
||||
|
||||
/* Load shader. */
|
||||
_shape_shader = new Shader("assets/shaders/shape.vert",
|
||||
"assets/shaders/shape.frag");
|
||||
_shape_shader->use();
|
||||
_shape_shader->set_mat4("projection", _projection);
|
||||
|
||||
/* Configure VAO/VBO for batch rendering. */
|
||||
glGenVertexArrays(1, &_vao);
|
||||
glGenBuffers(1, &_vbo);
|
||||
glBindVertexArray(_vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(ShapeVertex) * MAX_SHAPE_VERTICES, nullptr, GL_DYNAMIC_DRAW);
|
||||
|
||||
/* Position attribute. */
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(ShapeVertex), (void*)0);
|
||||
/* Colour attribute. */
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(ShapeVertex), (void*)offsetof(ShapeVertex, r));
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
_vertices.reserve(MAX_SHAPE_VERTICES);
|
||||
}
|
||||
|
||||
ShapeRenderer::~ShapeRenderer(void) {
|
||||
delete _shape_shader;
|
||||
}
|
||||
|
||||
void ShapeRenderer::begin(void) {
|
||||
_vertices.clear();
|
||||
}
|
||||
|
||||
void ShapeRenderer::flush(void) {
|
||||
if(_vertices.empty()) return;
|
||||
|
||||
_shape_shader->use();
|
||||
glBindVertexArray(_vao);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, _vertices.size() * sizeof(ShapeVertex), _vertices.data());
|
||||
|
||||
DebugStats::draw_calls++;
|
||||
DebugStats::shape_vertices += _vertices.size();
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, _vertices.size());
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void ShapeRenderer::draw_rect(int x, int y, int width, int height, const Color& color) {
|
||||
float x_f = (float)x;
|
||||
float y_f = (float)y;
|
||||
float w_f = (float)width;
|
||||
float h_f = (float)height;
|
||||
|
||||
_vertices.push_back({x_f, y_f+h_f, color.r, color.g, color.b});
|
||||
_vertices.push_back({x_f, y_f, color.r, color.g, color.b});
|
||||
_vertices.push_back({x_f+w_f, y_f, color.r, color.g, color.b});
|
||||
|
||||
_vertices.push_back({x_f, y_f+h_f, color.r, color.g, color.b});
|
||||
_vertices.push_back({x_f+w_f, y_f, color.r, color.g, color.b});
|
||||
_vertices.push_back({x_f+w_f, y_f+h_f, color.r, color.g, color.b});
|
||||
}
|
||||
|
||||
void ShapeRenderer::draw_triangle(int x1, int y1, int x2, int y2, int x3,
|
||||
int y3, const Color& color) {
|
||||
_vertices.push_back({(float)x1, (float)y1, color.r, color.g, color.b});
|
||||
_vertices.push_back({(float)x2, (float)y2, color.r, color.g, color.b});
|
||||
_vertices.push_back({(float)x3, (float)y3, color.r, color.g, color.b});
|
||||
}
|
||||
33
client/src/gfx/shape_renderer.h
Normal file
33
client/src/gfx/shape_renderer.h
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "shader.h"
|
||||
#include "types.h"
|
||||
|
||||
struct ShapeVertex {
|
||||
float x, y, r, g, b;
|
||||
};
|
||||
|
||||
const int MAX_SHAPES_PER_BATCH = 10000;
|
||||
const int VERTICES_PER_RECT = 6;
|
||||
const int MAX_SHAPE_VERTICES = MAX_SHAPES_PER_BATCH * VERTICES_PER_RECT;
|
||||
|
||||
class ShapeRenderer {
|
||||
public:
|
||||
ShapeRenderer(unsigned int screen_width, unsigned int screen_height);
|
||||
~ShapeRenderer(void);
|
||||
|
||||
void begin(void);
|
||||
void flush(void);
|
||||
|
||||
void draw_rect(int x, int y, int width, int height, const Color& color);
|
||||
void draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3,
|
||||
const Color& color);
|
||||
|
||||
private:
|
||||
Shader* _shape_shader;
|
||||
unsigned int _vao, _vbo;
|
||||
std::vector<ShapeVertex> _vertices;
|
||||
float _projection[16];
|
||||
};
|
||||
186
client/src/gfx/txt_renderer.cpp
Normal file
186
client/src/gfx/txt_renderer.cpp
Normal file
@ -0,0 +1,186 @@
|
||||
#include <cstdio>
|
||||
#include "gfx/types.h"
|
||||
#include <GL/glew.h>
|
||||
#include <SDL3/SDL_render.h>
|
||||
#include <freetype/freetype.h>
|
||||
|
||||
#include "txt_renderer.h"
|
||||
#include "debug/debug_stats.h"
|
||||
#include "math/math.h"
|
||||
|
||||
TextRenderer::TextRenderer(unsigned int screen_width, unsigned int screen_height) {
|
||||
/* Create projection matrix. */
|
||||
math::ortho_proj(_projecton, 0.0f, (float)screen_width, 0.0f, (float)screen_height, -1.0f, 1.0f);
|
||||
|
||||
/* Load shader. */
|
||||
_txt_shader = new Shader("assets/shaders/text.vert",
|
||||
"assets/shaders/text.frag");
|
||||
_txt_shader->use();
|
||||
_txt_shader->set_mat4("projection", _projecton);
|
||||
|
||||
/* Configure VAO/VBO for batch rendering. */
|
||||
glGenVertexArrays(1, &_vao);
|
||||
glGenBuffers(1, &_vbo);
|
||||
glBindVertexArray(_vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
|
||||
/* Pre-allocate buffer memory. */
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(TextVertex) * MAX_VERTICES, nullptr, GL_DYNAMIC_DRAW);
|
||||
|
||||
/* Position attribute. */
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void*)0);
|
||||
/* Texture coord attribute. */
|
||||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void*)offsetof(TextVertex, s));
|
||||
/* Colour attribute. */
|
||||
glEnableVertexAttribArray(2);
|
||||
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void*)offsetof(TextVertex, r));
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
/* Pre-allocate vector capacity. */
|
||||
_vertices.reserve(MAX_VERTICES);
|
||||
}
|
||||
|
||||
TextRenderer::~TextRenderer(void) {
|
||||
delete _txt_shader;
|
||||
glDeleteTextures(1, &_atlas_texture_id);
|
||||
}
|
||||
|
||||
void TextRenderer::load_font(const char* font_path, unsigned int font_size) {
|
||||
FT_Library ft;
|
||||
if(FT_Init_FreeType(&ft)) {
|
||||
printf("Could not init FreeType Library\n");
|
||||
return;
|
||||
}
|
||||
|
||||
FT_Face face;
|
||||
if(FT_New_Face(ft, font_path, 0, &face)) {
|
||||
printf("Failed to load font: %s\n", font_path);
|
||||
return;
|
||||
}
|
||||
|
||||
FT_Set_Pixel_Sizes(face, 0, font_size);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
/* Calculate atlas dimensions. */
|
||||
unsigned int atlas_width = 0;
|
||||
unsigned int atlas_height = 0;
|
||||
unsigned int max_height = 0;
|
||||
|
||||
for(unsigned char c = 0; c < 128; c++) {
|
||||
if(FT_Load_Char(face, c, FT_LOAD_RENDER)) {
|
||||
printf("Failed to load Glyph for char %c\n", c);
|
||||
continue;
|
||||
}
|
||||
|
||||
atlas_width += face->glyph->bitmap.width;
|
||||
max_height = std::max(max_height, face->glyph->bitmap.rows);
|
||||
}
|
||||
|
||||
atlas_height = max_height;
|
||||
|
||||
/* Create the atlas texture. */
|
||||
glGenTextures(1, &_atlas_texture_id);
|
||||
glBindTexture(GL_TEXTURE_2D, _atlas_texture_id);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, atlas_width, atlas_height, 0, GL_RED,
|
||||
GL_UNSIGNED_BYTE, 0);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
/* Fill the atlas texture with data. */
|
||||
int x_offset = 0;
|
||||
for(unsigned char c = 0; c < 128; c++) {
|
||||
if(FT_Load_Char(face, c, FT_LOAD_RENDER)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Copy glyph bitmap to atlas. */
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D, 0, x_offset, 0,
|
||||
face->glyph->bitmap.width, face->glyph->bitmap.rows,
|
||||
GL_RED, GL_UNSIGNED_BYTE, face->glyph->bitmap.buffer
|
||||
);
|
||||
|
||||
/* Store character metrics and texture coords. */
|
||||
_chars[c] = {
|
||||
{(int)face->glyph->bitmap.width, (int)face->glyph->bitmap.rows },
|
||||
{face->glyph->bitmap_left, face->glyph->bitmap_top},
|
||||
(unsigned int)face->glyph->advance.x,
|
||||
(float)x_offset / atlas_width,
|
||||
0.0f,
|
||||
(float)face->glyph->bitmap.width / atlas_width,
|
||||
(float)face->glyph->bitmap.rows / atlas_height
|
||||
};
|
||||
x_offset += face->glyph->bitmap.width;
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
FT_Done_Face(face);
|
||||
FT_Done_FreeType(ft);
|
||||
}
|
||||
|
||||
void TextRenderer::begin(void) {
|
||||
_vertices.clear();
|
||||
}
|
||||
|
||||
void TextRenderer::flush(void) {
|
||||
if(_vertices.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_txt_shader->use();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, _atlas_texture_id);
|
||||
glBindVertexArray(_vao);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, _vertices.size() * sizeof(TextVertex), _vertices.data());
|
||||
|
||||
DebugStats::draw_calls++;
|
||||
DebugStats::text_vertices += _vertices.size();
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, _vertices.size());
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
void TextRenderer::render_text(const char* text, float x, float y, const Color& color) {
|
||||
for(const char* p = text; *p; p++) {
|
||||
unsigned char c = *p;
|
||||
character ch = _chars[c];
|
||||
|
||||
float xpos = x + ch.bearing[0];
|
||||
float ypos = y - (ch.size[1] - ch.bearing[1]);
|
||||
float w = ch.size[0];
|
||||
float h = ch.size[1];
|
||||
|
||||
_vertices.push_back({xpos, ypos+h, ch.tx, ch.ty, color.r, color.g, color.b});
|
||||
_vertices.push_back({xpos, ypos, ch.tx, ch.ty+ch.th, color.r, color.g, color.b});
|
||||
_vertices.push_back({xpos+w, ypos, ch.tx+ch.tw, ch.ty+ch.th, color.r, color.g, color.b});
|
||||
|
||||
_vertices.push_back({xpos, ypos+h, ch.tx, ch.ty, color.r, color.g, color.b});
|
||||
_vertices.push_back({xpos+w, ypos, ch.tx+ch.tw, ch.ty+ch.th, color.r, color.g, color.b});
|
||||
_vertices.push_back({xpos+w, ypos+h, ch.tx+ch.tw, ch.ty, color.r, color.g, color.b});
|
||||
|
||||
x += (ch.advance >> 6);
|
||||
}
|
||||
}
|
||||
|
||||
float TextRenderer::get_text_width(const char* text, float scale) {
|
||||
float width = 0.0f;
|
||||
for(const char* p = text; *p; p++) {
|
||||
unsigned char c = *p;
|
||||
if(c < 128) {
|
||||
width += (_chars[c].advance >> 6) * scale;
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
47
client/src/gfx/txt_renderer.h
Normal file
47
client/src/gfx/txt_renderer.h
Normal file
@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <ft2build.h>
|
||||
#include <vector>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
#include "shader.h"
|
||||
#include "types.h"
|
||||
|
||||
struct TextVertex {
|
||||
float x, y, s, t, r, g, b;
|
||||
};
|
||||
|
||||
/* State of a single charactrer glyph. */
|
||||
struct character {
|
||||
int size[2];
|
||||
int bearing[2];
|
||||
unsigned int advance;
|
||||
float tx, ty; /* x and y offset of glyph in texture atlas. */
|
||||
float tw, th; /* width and height of glyph in texture atlas. */
|
||||
};
|
||||
|
||||
const int MAX_GLYPHS_PER_BATCH = 10000;
|
||||
const int VERTICES_PER_GLYPH = 6;
|
||||
const int MAX_VERTICES = MAX_GLYPHS_PER_BATCH * VERTICES_PER_GLYPH;
|
||||
|
||||
class TextRenderer {
|
||||
public:
|
||||
TextRenderer(unsigned int screen_width, unsigned int screen_height);
|
||||
~TextRenderer(void);
|
||||
|
||||
void load_font(const char* font_path, unsigned int font_size);
|
||||
void render_text(const char* text, float x, float y, const Color& color);
|
||||
float get_text_width(const char* text, float scale);
|
||||
|
||||
void begin(void);
|
||||
void flush(void);
|
||||
|
||||
|
||||
private:
|
||||
Shader* _txt_shader;
|
||||
unsigned int _vao, _vbo;
|
||||
unsigned int _atlas_texture_id;
|
||||
character _chars[128];
|
||||
std::vector<TextVertex> _vertices;
|
||||
float _projecton[16];
|
||||
};
|
||||
17
client/src/gfx/types.h
Normal file
17
client/src/gfx/types.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
class UIRenderer;
|
||||
|
||||
struct Color {
|
||||
float r, g, b;
|
||||
};
|
||||
|
||||
struct Rect {
|
||||
int x, y, w, h;
|
||||
};
|
||||
|
||||
struct RenderContext {
|
||||
UIRenderer* ui_renderer;
|
||||
int screen_height;
|
||||
bool show_cursor;
|
||||
};
|
||||
164
client/src/main.cpp
Normal file
164
client/src/main.cpp
Normal file
@ -0,0 +1,164 @@
|
||||
#include <memory>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <GL/glew.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_keyboard.h>
|
||||
#include <SDL3/SDL_timer.h>
|
||||
|
||||
#include "db/db.h"
|
||||
|
||||
#include "gfx/shape_renderer.h"
|
||||
#include "gfx/txt_renderer.h"
|
||||
#include "game_state.h"
|
||||
#include "ui/ui_renderer.h"
|
||||
#include "gfx/types.h"
|
||||
#include "ui/cursor_manager.h"
|
||||
#include "debug/debug_stats.h"
|
||||
|
||||
const int SCREEN_WIDTH = 1280;
|
||||
const int SCREEN_HEIGHT = 720;
|
||||
int main(int argc, char** argv) {
|
||||
/* Init SDL. */
|
||||
if(!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
printf("SDL could not initialise! SDL_ERROR: %s\n", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Set OpenGL attributes. */
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
|
||||
/* Create a window. */
|
||||
SDL_Window* window = SDL_CreateWindow(
|
||||
"Bettola Client",
|
||||
SCREEN_WIDTH,
|
||||
SCREEN_HEIGHT,
|
||||
SDL_WINDOW_OPENGL
|
||||
);
|
||||
|
||||
if(window == NULL) {
|
||||
printf("Unable to create window! SDL_ERROR: %s\n", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Create OpenGL context. */
|
||||
SDL_GLContext context = SDL_GL_CreateContext(window);
|
||||
if(context == NULL) {
|
||||
printf("OpenGL context could not be created! SDL_ERROR: %s\n", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Initialise GLEW. */
|
||||
glewExperimental = GL_TRUE;
|
||||
GLenum glewError = glewInit();
|
||||
if (glewError != GLEW_OK) {
|
||||
/* NOTE:
|
||||
* Dear future self, or other intrepid developer:
|
||||
* In some environments (like the Fedora VM I tested),
|
||||
* glewInit() returns an error here even when the context is valid.
|
||||
* This is a known quirk. We print the error but continue anyway,
|
||||
* because it's not fatal. Don't "fix" this by making it exit. Thank you.
|
||||
* It's GLEW, not you.
|
||||
*/
|
||||
fprintf(stderr, "Warning: glewInit failed with error code %u: %s. Attempting to continue anyway.\n",
|
||||
glewError, glewGetErrorString(glewError));
|
||||
}
|
||||
|
||||
/* Configure OpenGL. */
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
/* Listen for text input. */
|
||||
SDL_StartTextInput(window);
|
||||
|
||||
/* Init cursor manager. */
|
||||
CursorManager::init();
|
||||
|
||||
/* Init text renderer. */
|
||||
TextRenderer* txt_render_instance = new TextRenderer(SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
txt_render_instance->load_font("assets/fonts/hack/Hack-Regular.ttf", 14);
|
||||
|
||||
/* Init shape renderer. */
|
||||
ShapeRenderer* shape_renderer_instance = new ShapeRenderer(SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
|
||||
/* Init UI renderer. */
|
||||
UIRenderer* ui_renderer_instance = new UIRenderer(shape_renderer_instance,
|
||||
txt_render_instance, SCREEN_HEIGHT);
|
||||
|
||||
auto game_state = std::make_unique<GameState>();
|
||||
if(argc > 1 && std::string(argv[1]) == "-sp") {
|
||||
game_state->start_single_player_now(SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
} else {
|
||||
game_state->init(SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
}
|
||||
|
||||
/* timer for cursor blink. */
|
||||
Uint32 last_blink_time = 0;
|
||||
bool show_cursor = true;
|
||||
|
||||
/* Timestep. */
|
||||
Uint64 last_frame_time = SDL_GetPerformanceCounter();
|
||||
float dt = 0.0f;
|
||||
|
||||
bool running = true;
|
||||
while(running) {
|
||||
/* Reset per-frame stats. */
|
||||
DebugStats::reset();
|
||||
|
||||
/* Event handling. */
|
||||
SDL_Event event;
|
||||
while(SDL_PollEvent(&event)) {
|
||||
if(event.type == SDL_EVENT_QUIT) { running = false; }
|
||||
game_state->handle_event(&event, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
}
|
||||
|
||||
/* Calculate delta time. */
|
||||
Uint64 current_frame_time = SDL_GetPerformanceCounter();
|
||||
dt = (current_frame_time - last_frame_time) / (float)SDL_GetPerformanceFrequency();
|
||||
last_frame_time = current_frame_time;
|
||||
|
||||
/* Clamp dt to avoid large jumps. */
|
||||
if(dt > 0.1f) dt = 0.1f;
|
||||
|
||||
Uint32 current_time = SDL_GetTicks();
|
||||
if(current_time - last_blink_time > 500) { /* Every 500ms. */
|
||||
show_cursor = !show_cursor;
|
||||
last_blink_time = current_time;
|
||||
}
|
||||
|
||||
/* Clear screen. */
|
||||
glClearColor(0.04f, 0.05f, 0.06, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
RenderContext context = {
|
||||
ui_renderer_instance,
|
||||
SCREEN_HEIGHT,
|
||||
show_cursor
|
||||
};
|
||||
game_state->render(context);
|
||||
|
||||
/* Update game state after rendering so stats are for the frame just rendered.
|
||||
* I know this is unusual, but given the text based nature of the game
|
||||
* we won't see a negative impact, and this is better than a large refactor ;)
|
||||
*/
|
||||
game_state->update(dt, DebugStats::draw_calls, DebugStats::shape_vertices,
|
||||
DebugStats::text_vertices);
|
||||
|
||||
/* It's really odd to call it SwapWindow now, rather than SwapBuffer. */
|
||||
SDL_GL_SwapWindow(window);
|
||||
}
|
||||
|
||||
/* Cleanup. */
|
||||
game_state.reset();
|
||||
delete ui_renderer_instance;
|
||||
delete shape_renderer_instance;
|
||||
delete txt_render_instance;
|
||||
SDL_GL_DestroyContext(context);
|
||||
CursorManager::quit();
|
||||
SDL_DestroyWindow(window);
|
||||
SDL_Quit();
|
||||
|
||||
return 0;
|
||||
}
|
||||
218
client/src/terminal.cpp
Normal file
218
client/src/terminal.cpp
Normal file
@ -0,0 +1,218 @@
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <GL/glew.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
#include "terminal.h"
|
||||
#include "game_state.h"
|
||||
#include "gfx/types.h"
|
||||
#include "ui/editor.h"
|
||||
#include "ui/window_action.h"
|
||||
|
||||
Terminal::Terminal(GameState* game_state)
|
||||
: _game_state(game_state), _should_close(false), _command_history_index(0),
|
||||
_scroll_offset(0), _prompt(""), _pending_action({ActionType::NONE}),
|
||||
_session_id(0) {
|
||||
_input_view = std::make_unique<TextView>(&_input_buffer, false, false, false);
|
||||
}
|
||||
|
||||
Terminal::~Terminal(void) {}
|
||||
|
||||
void Terminal::update(int content_width, int content_height) {
|
||||
|
||||
}
|
||||
|
||||
void Terminal::add_history(const std::string& line) {
|
||||
std::string line_with_spaces;
|
||||
for(char ch : line) {
|
||||
if(ch == '\t') {
|
||||
line_with_spaces += " ";
|
||||
} else {
|
||||
line_with_spaces += ch;
|
||||
}
|
||||
}
|
||||
_history.push_back(line_with_spaces);
|
||||
|
||||
if(line == "__CLOSE_CONNECTION__") {
|
||||
_should_close = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Terminal::set_prompt(const std::string& prompt) {
|
||||
_prompt = prompt;
|
||||
}
|
||||
|
||||
bool Terminal::should_close(void) {
|
||||
return _should_close;
|
||||
}
|
||||
|
||||
WindowAction Terminal::get_pending_action(void) {
|
||||
WindowAction action = _pending_action;
|
||||
_pending_action.type = ActionType::NONE;
|
||||
return action;
|
||||
}
|
||||
|
||||
void Terminal::set_session_id(uint32_t id) {
|
||||
_session_id = id;
|
||||
}
|
||||
|
||||
uint32_t Terminal::get_session_id(void) const {
|
||||
return _session_id;
|
||||
}
|
||||
|
||||
void Terminal::_on_ret_press(int content_height) {
|
||||
std::string command = _input_buffer.get_line(0);
|
||||
if(!command.empty()) {
|
||||
_command_history.push_back(command);
|
||||
}
|
||||
_command_history_index = _command_history.size();
|
||||
|
||||
_input_buffer.clear();
|
||||
|
||||
if(command == "clear") {
|
||||
_history.push_back(_prompt + "> " + command);
|
||||
_history.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Client-side command handling. */
|
||||
std::stringstream ss(command);
|
||||
std::string cmd_name;
|
||||
ss >> cmd_name;
|
||||
if(cmd_name == "edit") {
|
||||
_history.push_back(_prompt + "> " + command);
|
||||
ss >> _pending_action.payload1; /* filename. */
|
||||
_pending_action.type = ActionType::READ_FILE;
|
||||
return;
|
||||
}
|
||||
|
||||
_history.push_back(_prompt + "> " + command);
|
||||
_game_state->send_network_command(_session_id, command);
|
||||
|
||||
/* After processing a command, always snap to the bottom. */
|
||||
float line_height = 20.0f;
|
||||
int visible_lines = content_height / line_height;
|
||||
int max_scroll = (_history.size()+1) - visible_lines;
|
||||
if(max_scroll < 0) max_scroll = 0;
|
||||
_scroll_offset = max_scroll;
|
||||
}
|
||||
|
||||
void Terminal::handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y,
|
||||
int content_width, int content_height) {
|
||||
/* Pass input to TextView; if true, RET was pressed. */
|
||||
if(event->type == SDL_EVENT_KEY_DOWN) {
|
||||
switch(event->key.key) {
|
||||
case SDLK_UP:
|
||||
if(!_command_history.empty()) {
|
||||
_command_history_index = std::max(0, _command_history_index-1);
|
||||
_input_buffer.set_text(_command_history[_command_history_index]);
|
||||
}
|
||||
break;
|
||||
case SDLK_DOWN:
|
||||
if(!_command_history.empty()) {
|
||||
_command_history_index =
|
||||
std::min((int)_command_history.size(), _command_history_index+1);
|
||||
if(_command_history_index < (int)_command_history.size()) {
|
||||
_input_buffer.set_text(_command_history[_command_history_index]);
|
||||
} else {
|
||||
_input_buffer.clear();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
float line_height = 20.0f;
|
||||
int visible_lines = content_height / line_height;
|
||||
int max_scroll = (_history.size()+1) - visible_lines;
|
||||
if(max_scroll < 0) max_scroll = 0;
|
||||
|
||||
if(_input_view->handle_event(event, content_height)) { _on_ret_press(content_height); }
|
||||
else {
|
||||
_scroll_offset = max_scroll; /* Always snap to bottom on key press. */
|
||||
}
|
||||
}
|
||||
} else if(event->type == SDL_EVENT_TEXT_INPUT) {
|
||||
if(_input_view->handle_event(event, content_height)) { _on_ret_press(content_height); }
|
||||
else {
|
||||
float line_height = 20.0f;
|
||||
int visible_lines = content_height / line_height;
|
||||
int max_scroll = (_history.size()+1) - visible_lines;
|
||||
if(max_scroll < 0) max_scroll = 0;
|
||||
_scroll_offset = max_scroll; /* Always snap to bottom on text input. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Terminal::scroll(int amount, int win_content_height) {
|
||||
/* amount > 0 = scroll up. amount < 0 = scroll down. */
|
||||
_scroll_offset += amount;
|
||||
|
||||
/* Lower bound: Don't scroll below the top of the history. */
|
||||
if(_scroll_offset < 0) {
|
||||
_scroll_offset = 0;
|
||||
}
|
||||
/* Upper bound: Don't scroll past the last command. */
|
||||
float line_height = 20.0f;
|
||||
int visible_lines = win_content_height / line_height;
|
||||
int max_scroll = (_history.size()+1) - visible_lines;
|
||||
if(max_scroll < 0) max_scroll = 0;
|
||||
if(_scroll_offset > max_scroll) {
|
||||
_scroll_offset = max_scroll;
|
||||
}
|
||||
}
|
||||
|
||||
void Terminal::render(const RenderContext& context, int x, int y_screen, int y_gl,
|
||||
int width, int height) {
|
||||
const Color white = { 1.0f, 1.0f, 1.0f };
|
||||
const Color green = { 0.2f, 1.0f, 0.2f };
|
||||
float line_height = 20.0f;
|
||||
float padding = 5.0f;
|
||||
|
||||
/*
|
||||
* Auto-scroll to bottom if the user has not manually scrolled up.
|
||||
* Ensures input line is always visible after submitting a command.
|
||||
*/
|
||||
int visible_lines = (height-padding) / line_height;
|
||||
int max_scroll_offset = (_history.size() + 1) - visible_lines;
|
||||
if(max_scroll_offset < 0) max_scroll_offset = 0;
|
||||
if(_scroll_offset >= max_scroll_offset - 1) _scroll_offset = max_scroll_offset;
|
||||
|
||||
context.ui_renderer->begin_text();
|
||||
|
||||
/* Enable scissor test to clip rendering to the window content area. */
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(x, y_gl, width, height);
|
||||
|
||||
/* Draw History. */
|
||||
for(size_t i = _scroll_offset; i < _history.size(); ++i) {
|
||||
float line_y_pos = y_screen + padding + ((i - _scroll_offset) * line_height);
|
||||
/* Culling: If line is already below the view, stop. */
|
||||
if(line_y_pos > y_screen + height) { break; }
|
||||
context.ui_renderer->render_text(_history[i].c_str(), x+padding, line_y_pos+18, white);
|
||||
}
|
||||
|
||||
/* Draw current input line. */
|
||||
float prompt_line_y = (y_screen + padding) + ((_history.size() - _scroll_offset) * line_height);
|
||||
float prompt_baseline_y = prompt_line_y + 18;
|
||||
|
||||
/* Render prompt string. */
|
||||
std::string prompt_str = _prompt + "> ";
|
||||
context.ui_renderer->render_text(prompt_str.c_str(), x+padding, prompt_baseline_y, green);
|
||||
|
||||
/* Render text view for the input right after prompt. */
|
||||
float input_x_pos = x + padding + (prompt_str.length() * 8.5f); /* Estimate width */
|
||||
float input_width = width - (input_x_pos-x);
|
||||
SyntaxTheme theme; /* Terminal doesn't need highlighting, just a default theme. */
|
||||
_input_view->render_text_content(context.ui_renderer, theme, input_x_pos, prompt_line_y, input_width, line_height);
|
||||
|
||||
context.ui_renderer->flush_text();
|
||||
|
||||
if(context.show_cursor) {
|
||||
context.ui_renderer->begin_shapes();
|
||||
_input_view->render_cursor(context.ui_renderer, theme, input_x_pos, prompt_line_y, input_width, line_height);
|
||||
context.ui_renderer->flush_shapes();
|
||||
}
|
||||
/* Disable scissor test. */
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
47
client/src/terminal.h
Normal file
47
client/src/terminal.h
Normal file
@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "gfx/types.h"
|
||||
#include "ui/text_buffer.h"
|
||||
#include "ui/text_view.h"
|
||||
#include "ui/i_window_content.h"
|
||||
#include "ui/window_action.h"
|
||||
|
||||
class GameState;
|
||||
|
||||
class Terminal : public IWindowContent {
|
||||
public:
|
||||
Terminal(GameState* game_state);
|
||||
~Terminal(void);
|
||||
|
||||
void update(int content_width, int content_height) override;
|
||||
void handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y,
|
||||
int content_width, int content_height) override;
|
||||
void render(const RenderContext& context, int x, int y_screen, int y_gl,
|
||||
int width, int height) override;
|
||||
void scroll(int amount, int content_height) override;
|
||||
void add_history(const std::string& line);
|
||||
void set_prompt(const std::string& prompt);
|
||||
bool should_close(void) override;
|
||||
WindowAction get_pending_action(void) override;
|
||||
void set_session_id(uint32_t id);
|
||||
uint32_t get_session_id(void) const;
|
||||
|
||||
private:
|
||||
void _on_ret_press(int content_height);
|
||||
|
||||
bool _should_close;
|
||||
std::vector<std::string> _command_history;
|
||||
int _command_history_index;
|
||||
std::vector<std::string> _history;
|
||||
int _scroll_offset;
|
||||
std::string _prompt;
|
||||
GameState* _game_state;
|
||||
TextBuffer _input_buffer;
|
||||
WindowAction _pending_action;
|
||||
uint32_t _session_id;
|
||||
std::unique_ptr<TextView> _input_view;
|
||||
};
|
||||
58
client/src/ui/boot_sequence.cpp
Normal file
58
client/src/ui/boot_sequence.cpp
Normal file
@ -0,0 +1,58 @@
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
#include "ui/ui_renderer.h"
|
||||
|
||||
#include "boot_sequence.h"
|
||||
#include <SDL3/SDL_timer.h>
|
||||
|
||||
BootSequence::BootSequence(void) {
|
||||
/* Load boot messages. */
|
||||
std::ifstream boot_file("assets/boot_messages.txt");
|
||||
if(!boot_file.is_open()) {
|
||||
printf("ERROR: Failed to open assets/boot_messages.txt\n");
|
||||
_messages.push_back("ERROR: boot_messages.txt not found.");
|
||||
} else {
|
||||
std::string line;
|
||||
while(std::getline(boot_file, line)) {
|
||||
if(!line.empty()) {
|
||||
_messages.push_back(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Init timings. */
|
||||
_start_time = SDL_GetTicks();
|
||||
_line_interval_ms = 150; /* 150ms between each line. */
|
||||
/* Total duration is time for all lines plus a 2-second pause at the end. */
|
||||
_total_duration_ms = (_messages.size() * _line_interval_ms) + 2000;
|
||||
}
|
||||
|
||||
BootSequence::~BootSequence(void) {}
|
||||
|
||||
bool BootSequence::is_finished(void) {
|
||||
return (SDL_GetTicks() - _start_time) >= _total_duration_ms;
|
||||
}
|
||||
|
||||
void BootSequence::render(UIRenderer* ui_renderer) {
|
||||
ui_renderer->begin_text();
|
||||
|
||||
const Color text_color = { 0.9f, 0.9f, 0.9f }; /* grey/white */
|
||||
const float line_height = 18.0f;
|
||||
const float padding = 15.0f;
|
||||
|
||||
Uint32 elapsed_time = SDL_GetTicks() - _start_time;
|
||||
int lines_to_show = elapsed_time / _line_interval_ms;
|
||||
|
||||
if(lines_to_show > _messages.size()) {
|
||||
lines_to_show = _messages.size();
|
||||
}
|
||||
|
||||
for(int i = 0; i < lines_to_show; ++i) {
|
||||
float y_pos = padding + (i*line_height);
|
||||
ui_renderer->render_text(_messages[i].c_str(), padding, y_pos, text_color);
|
||||
}
|
||||
|
||||
ui_renderer->flush_text();
|
||||
}
|
||||
21
client/src/ui/boot_sequence.h
Normal file
21
client/src/ui/boot_sequence.h
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "ui/ui_renderer.h"
|
||||
|
||||
class BootSequence {
|
||||
public:
|
||||
BootSequence(void);
|
||||
~BootSequence(void);
|
||||
|
||||
bool is_finished(void);
|
||||
void render(UIRenderer* ui_renderer);
|
||||
private:
|
||||
std::vector<std::string> _messages;
|
||||
Uint32 _start_time;
|
||||
Uint32 _line_interval_ms; /* Time for each line to appear. */
|
||||
Uint32 _total_duration_ms; /* Total time for the whole sequence. */
|
||||
};
|
||||
39
client/src/ui/cursor_manager.cpp
Normal file
39
client/src/ui/cursor_manager.cpp
Normal file
@ -0,0 +1,39 @@
|
||||
#include <cstdio>
|
||||
|
||||
#include "cursor_manager.h"
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
|
||||
SDL_Cursor* CursorManager::_arrow_cursor = nullptr;
|
||||
SDL_Cursor* CursorManager::_resize_cursor = nullptr;
|
||||
|
||||
void CursorManager::init(void) {
|
||||
_arrow_cursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT);
|
||||
_resize_cursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NWSE_RESIZE);
|
||||
|
||||
if(!_arrow_cursor || !_resize_cursor) {
|
||||
printf("Failed to create system cursors! SDL_Error: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
void CursorManager::set_cursor(CursorType type) {
|
||||
SDL_Cursor* cursor_to_set = _arrow_cursor;
|
||||
switch(type) {
|
||||
case CursorType::ARROW:
|
||||
cursor_to_set = _arrow_cursor;
|
||||
break;
|
||||
case CursorType::RESIZE_NWSE:
|
||||
cursor_to_set = _resize_cursor;
|
||||
break;
|
||||
}
|
||||
|
||||
if(SDL_GetCursor() != cursor_to_set) {
|
||||
SDL_SetCursor(cursor_to_set);
|
||||
}
|
||||
}
|
||||
|
||||
void CursorManager::quit(void) {
|
||||
SDL_DestroyCursor(_arrow_cursor);
|
||||
SDL_DestroyCursor(_resize_cursor);
|
||||
}
|
||||
|
||||
19
client/src/ui/cursor_manager.h
Normal file
19
client/src/ui/cursor_manager.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
|
||||
enum class CursorType {
|
||||
ARROW,
|
||||
RESIZE_NWSE /* Diagonal resize arrow. */
|
||||
};
|
||||
|
||||
class CursorManager {
|
||||
public:
|
||||
static void init(void);
|
||||
static void set_cursor(CursorType type);
|
||||
static void quit(void);
|
||||
|
||||
private:
|
||||
static SDL_Cursor* _arrow_cursor;
|
||||
static SDL_Cursor* _resize_cursor;
|
||||
};
|
||||
324
client/src/ui/desktop.cpp
Normal file
324
client/src/ui/desktop.cpp
Normal file
@ -0,0 +1,324 @@
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <ui/cursor_manager.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
#include "gfx/types.h"
|
||||
#include "game_state.h"
|
||||
#include "desktop.h"
|
||||
#include "terminal.h"
|
||||
#include "ui/i_window_content.h"
|
||||
#include "ui/launcher.h"
|
||||
#include "ui/taskbar.h"
|
||||
#include "ui/ui_window.h"
|
||||
#include "ui/editor.h"
|
||||
#include "ui/window_action.h"
|
||||
|
||||
static const std::string& get_random_snippet(const std::vector<std::string>& snippets) {
|
||||
if(snippets.empty()) {
|
||||
static const std::string err = "ERR";
|
||||
return err;
|
||||
}
|
||||
return snippets[rand() % snippets.size()];
|
||||
}
|
||||
|
||||
Desktop::Desktop(int screen_width, int screen_height, GameState* game_state,
|
||||
uint32_t initial_session_id) {
|
||||
_taskbar = std::make_unique<Taskbar>(screen_width, screen_height);
|
||||
_initial_session_id = initial_session_id;
|
||||
_game_state= game_state;
|
||||
_focused_window = nullptr;
|
||||
_window_awaiting_session_id = nullptr;
|
||||
_launcher_is_open = false;
|
||||
_launcher = std::make_unique<Launcher>(5, 0, 200); /* Tmp y-coord. */
|
||||
int launcher_y = screen_height - _taskbar->get_height() - _launcher->get_height();
|
||||
_launcher->set_y(launcher_y);
|
||||
|
||||
/* Load snippets for temp wallpaper. */
|
||||
std::ifstream snippet_file("assets/menu_background_snippets.txt");
|
||||
if(snippet_file.is_open()) {
|
||||
std::string line;
|
||||
while(std::getline(snippet_file, line)) {
|
||||
if(!line.empty()) { _snippets.push_back(line); }
|
||||
}
|
||||
}
|
||||
|
||||
/* Init animated background. */
|
||||
for(int i = 0; i < 100; ++i) {
|
||||
float y_pos = (float)(rand() % screen_height);
|
||||
_background_text.push_back(
|
||||
{get_random_snippet(_snippets), (float)(rand() % screen_width),
|
||||
y_pos,
|
||||
(float)(rand() % 30 + 10) / 100.0f,
|
||||
(int)y_pos
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop owns UIWindow, make sure we delete them. */
|
||||
Desktop::~Desktop(void) {}
|
||||
|
||||
void Desktop::add_window(std::unique_ptr<UIWindow> window) {
|
||||
UIWindow* window_ptr = window.get();
|
||||
_windows.push_back(std::move(window));
|
||||
_taskbar->add_window(window_ptr);
|
||||
_set_focused_window(window_ptr);
|
||||
}
|
||||
|
||||
void Desktop::_set_focused_window(UIWindow* window) {
|
||||
if(window == _focused_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Unfocus old window. */
|
||||
if(_focused_window) {
|
||||
_focused_window->set_focused(false);
|
||||
}
|
||||
|
||||
/* Set new focused window. */
|
||||
_focused_window = window;
|
||||
if(_focused_window) {
|
||||
_focused_window->set_focused(true);
|
||||
|
||||
/* Move newly focused window to end of vector so it's rendered on top. */
|
||||
auto it = std::find_if(_windows.begin(), _windows.end(),
|
||||
[window](const std::unique_ptr<UIWindow>& p) {
|
||||
return p.get() == window;
|
||||
});
|
||||
if(it != _windows.end()) {
|
||||
std::rotate(it, it+1, _windows.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Desktop::handle_event(SDL_Event* event, int screen_width, int screen_height) {
|
||||
if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
int mouse_x = event->button.x;
|
||||
int mouse_y = event->button.y;
|
||||
|
||||
/* If click is on taskbar, ignore it on the down-event. Let the up-event handle it. */
|
||||
if(_taskbar->is_point_inside(mouse_x, mouse_y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* When launcher open, check for outside click to close it. */
|
||||
if(_launcher_is_open && !_launcher->is_point_inside(mouse_x, mouse_y, screen_height)) {
|
||||
_launcher_is_open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/* If click not on focused window, find which window should receive focus. */
|
||||
bool click_on_window = false;
|
||||
for(int i = _windows.size()-1; i >= 0; --i) {
|
||||
if(_windows[i].get()->is_point_inside(mouse_x, mouse_y)) {
|
||||
/* Top most window. Focus it. */
|
||||
UIWindow* target = _windows[i].get();
|
||||
if(!target->is_minimized()) {
|
||||
_set_focused_window(target);
|
||||
/* Pass event down to newly focused window to handle dragging, etc. */
|
||||
target->handle_event(event, screen_width, screen_height, _taskbar->get_height());
|
||||
}
|
||||
click_on_window = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Click wasn't on a window. Unfocus. */
|
||||
if(!click_on_window) {
|
||||
_set_focused_window(nullptr);
|
||||
}
|
||||
} else if(event->type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
if(_taskbar->is_start_button_clicked(event, screen_height)) {
|
||||
_launcher_is_open = !_launcher_is_open;
|
||||
} else if(_launcher_is_open) {
|
||||
std::string app_to_launch = _launcher->handle_event(event, screen_height);
|
||||
if(app_to_launch == "Terminal") {
|
||||
auto term = std::make_unique<Terminal>(_game_state);
|
||||
auto term_window = std::make_unique<UIWindow>("Terminal", 150, 150, 800, 500);
|
||||
_window_awaiting_session_id = term_window.get();
|
||||
term_window->set_content(std::move(term));
|
||||
add_window(std::move(term_window));
|
||||
_launcher_is_open = false;
|
||||
_game_state->send_create_session_request();
|
||||
} else if(app_to_launch == "Editor") {
|
||||
auto editor = std::make_unique<Editor>();
|
||||
auto editor_window = std::make_unique<UIWindow>("Editor", 200, 200, 600, 400);
|
||||
editor_window->set_session_id(_initial_session_id);
|
||||
editor_window->set_content(std::move(editor));
|
||||
add_window(std::move(editor_window));
|
||||
_launcher_is_open = false;
|
||||
}
|
||||
} else {
|
||||
UIWindow* clicked_window = _taskbar->handle_event(event, screen_height);
|
||||
if(clicked_window) {
|
||||
if(clicked_window == _focused_window && !clicked_window->is_minimized()) {
|
||||
clicked_window->minimize();
|
||||
_set_focused_window(nullptr);
|
||||
} else {
|
||||
clicked_window->restore();
|
||||
_set_focused_window(clicked_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(event->type == SDL_EVENT_MOUSE_MOTION) {
|
||||
/* Update cursor if hovering over any resize handle. */
|
||||
for(int i = _windows.size() - 1; i >= 0; --i) {
|
||||
if(_windows[i]->is_mouse_over_resize_handle(event->motion.x,
|
||||
event->motion.y)) {
|
||||
return CursorManager::set_cursor(CursorType::RESIZE_NWSE);
|
||||
}
|
||||
}
|
||||
CursorManager::set_cursor(CursorType::ARROW);
|
||||
}
|
||||
|
||||
/* Pass all other non-down-click events to focused window. */
|
||||
if(_focused_window && event->type != SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
_focused_window->handle_event(event, screen_width, screen_height, _taskbar->get_height());
|
||||
}
|
||||
}
|
||||
|
||||
void Desktop::update(float dt, int screen_width, int screen_height) {
|
||||
/* Remove closed windows. */
|
||||
if(_focused_window) {
|
||||
IWindowContent* content = _focused_window->get_content();
|
||||
if(content) {
|
||||
WindowAction action = content->get_pending_action();
|
||||
uint32_t session_id = _focused_window->get_session_id();
|
||||
switch(action.type) {
|
||||
case ActionType::WRITE_FILE: {
|
||||
if(session_id != 0) {
|
||||
_game_state->send_file_write_request(session_id, action.payload1, action.payload2);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionType::READ_FILE: {
|
||||
if(session_id != 0) {
|
||||
_game_state->send_file_read_request(session_id, action.payload1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionType::BUILD_FILE: {
|
||||
if(session_id != 0) {
|
||||
_game_state->send_build_file_request(session_id, action.payload1, action.payload2);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(auto& window : _windows) {
|
||||
if(window) {
|
||||
window->update();
|
||||
}
|
||||
}
|
||||
_update_wallpaper(dt, screen_width, screen_height);
|
||||
_windows.erase(std::remove_if(_windows.begin(), _windows.end(),
|
||||
[this](const std::unique_ptr<UIWindow>& w) {
|
||||
if (w->should_close()) {
|
||||
/* Also remove from session map. */
|
||||
uint32_t session_to_remove = 0;
|
||||
for(auto const& [sid, win] : _session_windows) {
|
||||
if(win == w.get()) {
|
||||
session_to_remove = sid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(session_to_remove != 0)
|
||||
_session_windows.erase(session_to_remove);
|
||||
_taskbar->remove_window(w.get());
|
||||
if (w.get() == _focused_window) {
|
||||
_focused_window = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
_windows.end());
|
||||
}
|
||||
|
||||
void Desktop::register_session(uint32_t session_id, UIWindow* window) {
|
||||
_session_windows[session_id] = window;
|
||||
if(_window_awaiting_session_id == window) {
|
||||
_window_awaiting_session_id = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
UIWindow* Desktop::get_window_by_session_id(uint32_t session_id) {
|
||||
if(_session_windows.count(session_id)) {
|
||||
return _session_windows.at(session_id);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIWindow* Desktop::get_focused_window(void) {
|
||||
return _focused_window;
|
||||
}
|
||||
|
||||
UIWindow* Desktop::get_window_awaiting_session_id(void) {
|
||||
return _window_awaiting_session_id;
|
||||
}
|
||||
|
||||
void Desktop::render(const RenderContext& context) {
|
||||
/* Pass 1: Background. */
|
||||
context.ui_renderer->begin_text();
|
||||
_render_wallpaper(context.ui_renderer);
|
||||
context.ui_renderer->flush_text();
|
||||
|
||||
/* Pass 2: Windows Render in order, last window is top-most. */
|
||||
for(size_t i = 0; i < _windows.size(); ++i) {
|
||||
auto& win_i = _windows[i];
|
||||
if(win_i->is_minimized()) continue;
|
||||
|
||||
bool is_occluded = false;
|
||||
Rect rect_i = win_i->get_rect();
|
||||
|
||||
/* Check against all windows on top of this one. */
|
||||
for(size_t j = i+1; j < _windows.size(); ++j) {
|
||||
auto& win_j = _windows[j];
|
||||
if(win_j->is_minimized()) continue;
|
||||
|
||||
Rect rect_j = win_j->get_rect();
|
||||
if(rect_i.x >= rect_j.x && (rect_i.x + rect_i.w) <= (rect_j.x + rect_j.w) &&
|
||||
rect_i.y >= rect_j.y && (rect_i.y + rect_i.h) <= (rect_j.y + rect_j.h)) {
|
||||
is_occluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!is_occluded) {
|
||||
win_i->render(context);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pass 3: Top-level static UI (taskbar, Launcher, etc.). */
|
||||
context.ui_renderer->begin_text();
|
||||
if(_launcher_is_open) {
|
||||
_launcher->render(context.ui_renderer);
|
||||
}
|
||||
_taskbar->render(context.ui_renderer, _focused_window);
|
||||
context.ui_renderer->flush_text();
|
||||
}
|
||||
|
||||
void Desktop::_render_wallpaper(UIRenderer* ui_renderer) {
|
||||
const Color wallpaper_color = { 0.0f, 0.15f, 0.08f };
|
||||
for(auto& line : _background_text) {
|
||||
ui_renderer->render_text(line.text.c_str(), std::round(line.x),
|
||||
line.render_y, wallpaper_color);
|
||||
}
|
||||
}
|
||||
|
||||
void Desktop::_update_wallpaper(float dt, int screen_width, int screen_height) {
|
||||
for(auto& line : _background_text) {
|
||||
line.y_precise -= line.speed * dt * 100.0f;
|
||||
line.render_y = static_cast<int>(std::round(line.y_precise));
|
||||
if(line.render_y < 0) {
|
||||
line.y_precise = screen_height;
|
||||
line.x = (float)(rand() % screen_width);
|
||||
line.text = get_random_snippet(_snippets);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
client/src/ui/desktop.h
Normal file
57
client/src/ui/desktop.h
Normal file
@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "gfx/types.h"
|
||||
#include "ui/ui_window.h"
|
||||
#include "ui/taskbar.h"
|
||||
#include "ui/launcher.h"
|
||||
#include "ui/ui_renderer.h"
|
||||
|
||||
class GameState;
|
||||
|
||||
/* Animated background stuff. */
|
||||
struct ScrollingText {
|
||||
std::string text;
|
||||
float x;
|
||||
float y_precise; /* sub-pixel animation. */
|
||||
float speed;
|
||||
int render_y; /* For pixel-perfect rendering. */
|
||||
};
|
||||
|
||||
class Desktop {
|
||||
public:
|
||||
Desktop(int screen_width, int screen_height, GameState* game_state,
|
||||
uint32_t initial_session_id);
|
||||
~Desktop(void);
|
||||
|
||||
void add_window(std::unique_ptr<UIWindow> window);
|
||||
void handle_event(SDL_Event* event, int screen_width, int screen_height);
|
||||
void update(float dt, int screen_width, int screen_height);
|
||||
void render(const RenderContext& context);
|
||||
|
||||
void register_session(uint32_t session_id, UIWindow* window);
|
||||
UIWindow* get_window_by_session_id(uint32_t session_id);
|
||||
UIWindow* get_focused_window(void);
|
||||
UIWindow* get_window_awaiting_session_id(void);
|
||||
|
||||
private:
|
||||
void _set_focused_window(UIWindow* window);
|
||||
void _render_wallpaper(UIRenderer* ui_renderer);
|
||||
void _update_wallpaper(float dt, int screen_width, int screen_height);
|
||||
|
||||
std::vector<std::unique_ptr<UIWindow>> _windows;
|
||||
std::unique_ptr<Taskbar> _taskbar;
|
||||
std::unique_ptr<Launcher> _launcher;
|
||||
UIWindow* _focused_window;
|
||||
UIWindow* _window_awaiting_session_id;
|
||||
std::map<uint32_t, UIWindow*> _session_windows;
|
||||
GameState* _game_state;
|
||||
std::vector<ScrollingText> _background_text;
|
||||
std::vector<std::string> _snippets;
|
||||
bool _launcher_is_open;
|
||||
uint32_t _initial_session_id;
|
||||
};
|
||||
114
client/src/ui/editor.cpp
Normal file
114
client/src/ui/editor.cpp
Normal file
@ -0,0 +1,114 @@
|
||||
#include "editor.h"
|
||||
#include <memory>
|
||||
|
||||
#include "text_view.h"
|
||||
#include "gfx/types.h"
|
||||
#include "ui/window_action.h"
|
||||
|
||||
Editor::Editor(void)
|
||||
: _should_close(false), _pending_action({ActionType::NONE}),
|
||||
_filename("untitled.txt") {
|
||||
_view = std::make_unique<TextView>(&_buffer, true, true, true);
|
||||
_menu_bar = std::make_unique<MenuBar>(25);
|
||||
_menu_bar->add_menu("File");
|
||||
_menu_bar->add_menu_item("File", "Save", [this]() {
|
||||
_pending_action = {ActionType::WRITE_FILE, _filename, _buffer.get_text()};
|
||||
});
|
||||
_menu_bar->add_menu("Build");
|
||||
_menu_bar->add_menu_item("Build", "Run Build", [this]() {
|
||||
_pending_action = {ActionType::BUILD_FILE, _filename, _buffer.get_text()};
|
||||
});
|
||||
}
|
||||
|
||||
Editor::Editor(const std::string& filename)
|
||||
: _should_close(false), _pending_action({ActionType::NONE}),
|
||||
_filename(filename) {
|
||||
_view = std::make_unique<TextView>(&_buffer, true, true, true);
|
||||
_menu_bar = std::make_unique<MenuBar>(25);
|
||||
_menu_bar->add_menu("File");
|
||||
_menu_bar->add_menu_item("File", "Save", [this]() {
|
||||
_pending_action = {ActionType::WRITE_FILE, _filename, _buffer.get_text()};
|
||||
});
|
||||
_menu_bar->add_menu("Build");
|
||||
_menu_bar->add_menu_item("Build", "Run Build", [this]() {
|
||||
_pending_action = {ActionType::BUILD_FILE, _filename, _buffer.get_text()};
|
||||
});
|
||||
}
|
||||
|
||||
Editor::~Editor(void) {}
|
||||
|
||||
void Editor::update(int content_width, int content_height) {
|
||||
/* Nothing to do yet. */
|
||||
}
|
||||
|
||||
void Editor::handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y,
|
||||
int content_width, int content_height) {
|
||||
if(!event) return;
|
||||
|
||||
_menu_bar->handle_event(event, window_x, window_y);
|
||||
|
||||
/* We don't care about the return val here. RET is just newline. */
|
||||
if(event->type == SDL_EVENT_KEY_DOWN && event->key.key == SDLK_S &&
|
||||
(event->key.mod & SDL_KMOD_CTRL)) {
|
||||
/* C-S pressed, create a save action. */
|
||||
_pending_action = { ActionType::WRITE_FILE, _filename, _buffer.get_text() };
|
||||
} else {
|
||||
_view->handle_event(event, content_height);
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::render(const RenderContext& context, int x, int y_screen, int y_gl,
|
||||
int width, int height) {
|
||||
int menu_bar_height = _menu_bar->get_height();
|
||||
int content_y = y_screen + menu_bar_height;
|
||||
int content_height = height - menu_bar_height;
|
||||
|
||||
/* Pass 1: Main Bar Background.*/
|
||||
context.ui_renderer->begin_shapes();
|
||||
_menu_bar->render_bar_bg(context.ui_renderer, x, y_screen, width);
|
||||
context.ui_renderer->flush_shapes();
|
||||
|
||||
context.ui_renderer->enable_scissor(x, y_gl, width, content_height);
|
||||
|
||||
/* Pass 2: Main text view. */
|
||||
context.ui_renderer->begin_text();
|
||||
_view->render_text_content(context.ui_renderer, _theme, x, content_y, width, content_height);
|
||||
context.ui_renderer->flush_text();
|
||||
|
||||
/* Pass 3: Editor cursor. */
|
||||
if(context.show_cursor) {
|
||||
context.ui_renderer->begin_shapes();
|
||||
_view->render_cursor(context.ui_renderer, _theme, x, content_y, width, content_height);
|
||||
context.ui_renderer->flush_shapes();
|
||||
}
|
||||
|
||||
context.ui_renderer->disable_scissor();
|
||||
|
||||
/* Pass 4: Menu bar text and dropdown. */
|
||||
context.ui_renderer->begin_text();
|
||||
_menu_bar->render_bar_text(context.ui_renderer, x, y_screen, width);
|
||||
_menu_bar->render_dropdown(context.ui_renderer, x, y_screen, width);
|
||||
context.ui_renderer->flush_text();
|
||||
}
|
||||
|
||||
void Editor::scroll(int amount, int content_height) {
|
||||
_view->scroll(amount, content_height);
|
||||
}
|
||||
|
||||
bool Editor::should_close(void) {
|
||||
return _should_close;
|
||||
}
|
||||
|
||||
void Editor::set_buffer_content(const std::string& content) {
|
||||
_buffer.set_text(content);
|
||||
}
|
||||
|
||||
WindowAction Editor::get_pending_action(void) {
|
||||
WindowAction action = _pending_action;
|
||||
_pending_action.type = ActionType::NONE; /* Clear action. */
|
||||
return action;
|
||||
}
|
||||
|
||||
const std::string& Editor::get_filename(void) const {
|
||||
return _filename;
|
||||
}
|
||||
47
client/src/ui/editor.h
Normal file
47
client/src/ui/editor.h
Normal file
@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "gfx/types.h"
|
||||
#include "i_window_content.h"
|
||||
#include "ui/text_buffer.h"
|
||||
#include "ui/window_action.h"
|
||||
#include "ui/menu_bar.h"
|
||||
|
||||
class TextView;
|
||||
|
||||
struct SyntaxTheme {
|
||||
Color normal = { 1.0f, 1.0f, 1.0f };
|
||||
Color keyword = { 0.8f, 0.6f, 1.0f };
|
||||
Color string = { 1.0f, 0.8f, 0.6f };
|
||||
Color number = { 0.6f, 1.0f, 0.8f };
|
||||
Color comment = { 0.6f, 0.6f, 0.6f };
|
||||
Color line_num = { 0.5f, 0.6f, 0.7f };
|
||||
};
|
||||
|
||||
class Editor : public IWindowContent {
|
||||
public:
|
||||
Editor(void);
|
||||
Editor(const std::string& filename);
|
||||
~Editor(void) override;
|
||||
|
||||
void update(int content_width, int content_height) override;
|
||||
void handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y,
|
||||
int content_widht, int content_heigth) override;
|
||||
void render(const RenderContext& context, int x, int y_screen, int y_gl,
|
||||
int width, int height) override;
|
||||
void scroll(int amount, int content_height) override;
|
||||
bool should_close(void) override;
|
||||
void set_buffer_content(const std::string& content);
|
||||
WindowAction get_pending_action(void) override;
|
||||
const std::string& get_filename(void) const;
|
||||
|
||||
private:
|
||||
TextBuffer _buffer;
|
||||
std::unique_ptr<TextView> _view;
|
||||
std::unique_ptr<MenuBar> _menu_bar;
|
||||
bool _should_close;
|
||||
WindowAction _pending_action;
|
||||
SyntaxTheme _theme;
|
||||
std::string _filename;
|
||||
};
|
||||
19
client/src/ui/i_window_content.h
Normal file
19
client/src/ui/i_window_content.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
#include "window_action.h"
|
||||
#include "gfx/types.h"
|
||||
|
||||
class IWindowContent {
|
||||
public:
|
||||
virtual ~IWindowContent(void) = default;
|
||||
virtual void update(int content_width, int content_height) = 0;
|
||||
virtual void handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y,
|
||||
int content_width, int content_height) = 0;
|
||||
virtual void render(const RenderContext& context, int x, int y_screen, int y_gl,
|
||||
int width, int height) = 0;
|
||||
virtual void scroll(int amount, int content_height) = 0;
|
||||
virtual bool should_close(void) = 0;
|
||||
virtual WindowAction get_pending_action() = 0;
|
||||
};
|
||||
76
client/src/ui/launcher.cpp
Normal file
76
client/src/ui/launcher.cpp
Normal file
@ -0,0 +1,76 @@
|
||||
#include "launcher.h"
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
#include "ui/ui_renderer.h"
|
||||
|
||||
Launcher::Launcher(int x, int y, int width) : _x(x), _y(y), _width(width) {
|
||||
/* TODO: Hardcode the launcher apps for now. */
|
||||
_apps.push_back("Terminal");
|
||||
_apps.push_back("Editor");
|
||||
|
||||
int item_height = 30;
|
||||
_height = _apps.size() * item_height;
|
||||
}
|
||||
|
||||
Launcher::~Launcher(void) {}
|
||||
|
||||
bool Launcher::is_point_inside(int x, int y, int screen_height) const {
|
||||
return (x >= _x && x <= _x + _width && y>= _y && y <= _y + _height);
|
||||
}
|
||||
|
||||
void Launcher::render(UIRenderer* ui_renderer) {
|
||||
const Color bg_color = { 0.15f, 0.17f, 0.19f };
|
||||
const Color text_color = { 0.9f, 0.9f, 0.9f };
|
||||
const Color hover_color = { 0.3f, 0.32f, 0.34f };
|
||||
|
||||
ui_renderer->begin_shapes();
|
||||
ui_renderer->begin_text();
|
||||
|
||||
/* Note: y-coord is TOP of launcher menu. */
|
||||
ui_renderer->draw_rect(_x, _y, _width, _height, bg_color);
|
||||
|
||||
int item_height = 30;
|
||||
int item_y = _y;
|
||||
|
||||
float mouse_x, mouse_y;
|
||||
SDL_GetMouseState(&mouse_x, &mouse_y);
|
||||
|
||||
for(const auto& app_name : _apps) {
|
||||
/* Check for hover. */
|
||||
if(mouse_x >= _x && mouse_x <= _x + _width &&
|
||||
mouse_y >= item_y && mouse_y <= item_y + item_height) {
|
||||
ui_renderer->draw_rect(_x, item_y, _width, item_height, hover_color);
|
||||
}
|
||||
|
||||
ui_renderer->render_text(app_name.c_str(), _x+10, item_y+20, text_color);
|
||||
item_y += item_height;
|
||||
}
|
||||
|
||||
ui_renderer->flush_shapes();
|
||||
ui_renderer->flush_text();
|
||||
}
|
||||
|
||||
std::string Launcher::handle_event(SDL_Event* event, int screen_height) {
|
||||
if(event->type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
int mouse_x = event->button.x;
|
||||
int mouse_y = event->button.y;
|
||||
|
||||
int item_height = 30;
|
||||
int item_y = _y;
|
||||
for(const auto& app_name : _apps) {
|
||||
if(mouse_x >= _x && mouse_x <= _x + _width &&
|
||||
mouse_y >= item_y && mouse_y <= item_y + item_height) {
|
||||
return app_name; /* Return name of clicked app. */
|
||||
}
|
||||
item_y += item_height;
|
||||
}
|
||||
}
|
||||
return ""; /* Nothing clicked. */
|
||||
}
|
||||
|
||||
void Launcher::set_y(int y) {
|
||||
_y = y;
|
||||
}
|
||||
|
||||
int Launcher::get_height(void) const {
|
||||
return _height;
|
||||
}
|
||||
23
client/src/ui/launcher.h
Normal file
23
client/src/ui/launcher.h
Normal file
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
class UIRenderer;
|
||||
|
||||
class Launcher {
|
||||
public:
|
||||
Launcher(int x, int y, int width);
|
||||
~Launcher(void);
|
||||
|
||||
void render(UIRenderer* ui_renderer);
|
||||
std::string handle_event(SDL_Event* event, int screen_height);
|
||||
bool is_point_inside(int x, int y, int screen_height) const;
|
||||
void set_y(int y);
|
||||
int get_height(void) const;
|
||||
|
||||
private:
|
||||
int _x, _y, _width, _height;
|
||||
std::vector<std::string> _apps;
|
||||
};
|
||||
195
client/src/ui/login_screen.cpp
Normal file
195
client/src/ui/login_screen.cpp
Normal file
@ -0,0 +1,195 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gfx/types.h"
|
||||
#include "ui/ui_renderer.h"
|
||||
#include "ui/login_screen.h"
|
||||
|
||||
LoginScreen::LoginScreen(int screen_width, int screen_height) :
|
||||
_screen_width(screen_width),
|
||||
_screen_height(screen_height) {
|
||||
|
||||
_is_new_account = false; /* Default to login. */
|
||||
|
||||
/* Position tabs relative to the form. */
|
||||
const int form_center_x = _screen_width / 2 - 25;
|
||||
const int tabs_y = (_screen_height / 2 - 100) + 200;
|
||||
_login_tab_rect = { form_center_x-90, tabs_y, 80, 30 };
|
||||
_create_account_tab_rect = { form_center_x+10, tabs_y, 150, 30 };
|
||||
}
|
||||
|
||||
LoginScreen::~LoginScreen(void) {}
|
||||
|
||||
void LoginScreen::update(float dt) {
|
||||
/* TODO: */
|
||||
}
|
||||
|
||||
void LoginScreen::render(const RenderContext& context) const {
|
||||
UIRenderer* ui_renderer = context.ui_renderer;
|
||||
|
||||
/* Colours. */
|
||||
const Color bg_color = { 0.06f, 0.07f, 0.09f };
|
||||
const Color text_color = { 0.8f, 0.8f, 0.8f };
|
||||
const Color inactive_box_color = { 0.1f, 0.1f, 0.15f };
|
||||
const Color active_box_color = { 0.15f, 0.15f, 0.2f };
|
||||
const Color warning_color = { 0.7f, 0.3f, 0.3f };
|
||||
const Color active_tab_color = { 0.9f, 0.9f, 0.9f };
|
||||
const Color inactive_tab_color = { 0.4f, 0.5f, 0.6f };
|
||||
|
||||
/* Layout. */
|
||||
const int box_width = 300;
|
||||
const int box_height = 30;
|
||||
const int center_x = (_screen_width - box_width) / 2;
|
||||
const int start_y = _screen_height / 2 - 100;
|
||||
|
||||
const Rect username_rect = { center_x, start_y+40, box_width, box_height };
|
||||
const Rect password_rect = { center_x, start_y+100, box_width, box_height };
|
||||
const Rect hostname_rect = { center_x, start_y+160, box_width, box_height };
|
||||
|
||||
ui_renderer->begin_shapes();
|
||||
ui_renderer->begin_text();
|
||||
|
||||
/* Draw background. */
|
||||
ui_renderer->draw_rect(0, 0, _screen_width, _screen_height, bg_color);
|
||||
|
||||
/* Draw mode-switch tabs. */
|
||||
ui_renderer->render_text("[Login]", _login_tab_rect.x, _login_tab_rect.y + 20,
|
||||
_is_new_account ? inactive_tab_color : active_tab_color);
|
||||
ui_renderer->render_text("[Create Account]", _create_account_tab_rect.x,
|
||||
_create_account_tab_rect.y + 20,
|
||||
_is_new_account ? active_tab_color : inactive_tab_color);
|
||||
|
||||
/* Draw title. */
|
||||
const char* title = _is_new_account ? "Create Account" : "Login";
|
||||
float title_width = ui_renderer->get_text_renderer()->get_text_width(title, 1.0f);
|
||||
ui_renderer->render_text(title, (_screen_width-title_width)/2, start_y, text_color);
|
||||
|
||||
/* Draw error message if it exists. */
|
||||
if(!_error_message.empty()) {
|
||||
float error_width = ui_renderer->get_text_renderer()->get_text_width(_error_message.c_str(), 1.0f);
|
||||
ui_renderer->render_text(_error_message.c_str(), (_screen_width-error_width)/2,
|
||||
start_y+20, warning_color);
|
||||
}
|
||||
|
||||
/* Draw input boxes and labels. */
|
||||
/* Username */
|
||||
ui_renderer->render_text("Username", username_rect.x, username_rect.y-5, text_color);
|
||||
ui_renderer->draw_rect(username_rect.x, username_rect.y, username_rect.w, username_rect.h,
|
||||
_active_field == 0 ? active_box_color : inactive_box_color);
|
||||
ui_renderer->render_text(_username_input.c_str(), username_rect.x+10, username_rect.y+20, text_color);
|
||||
if(_active_field == 0 && context.show_cursor) {
|
||||
float cursor_x = username_rect.x + 10 +
|
||||
ui_renderer->get_text_renderer()->get_text_width(_username_input.c_str(), 1.0f);
|
||||
ui_renderer->draw_rect((int)cursor_x, username_rect.y+5, 2, 20, text_color);
|
||||
}
|
||||
|
||||
/* Password. */
|
||||
ui_renderer->render_text("Password", password_rect.x, password_rect.y-5, text_color);
|
||||
ui_renderer->draw_rect(password_rect.x, password_rect.y, password_rect.w, password_rect.h,
|
||||
_active_field == 1 ? active_box_color : inactive_box_color);
|
||||
ui_renderer->render_text(_password_input.c_str(), password_rect.x+10, password_rect.y+20, text_color);
|
||||
if(_active_field == 1 && context.show_cursor) {
|
||||
float cursor_x = password_rect.x + 10 +
|
||||
ui_renderer->get_text_renderer()->get_text_width(_password_input.c_str(), 1.0f);
|
||||
ui_renderer->draw_rect((int)cursor_x, password_rect.y+5, 2, 20, text_color);
|
||||
}
|
||||
|
||||
/* Hostname (only for new account). */
|
||||
if(_is_new_account) {
|
||||
ui_renderer->render_text("Hostname", hostname_rect.x, hostname_rect.y-5, text_color);
|
||||
ui_renderer->draw_rect(hostname_rect.x, hostname_rect.y, hostname_rect.w, hostname_rect.h,
|
||||
_active_field == 2 ? active_box_color : inactive_box_color);
|
||||
ui_renderer->render_text(_hostname_input.c_str(), hostname_rect.x+10, hostname_rect.y+20, text_color);
|
||||
if(_active_field == 2 && context.show_cursor) {
|
||||
float cursor_x = hostname_rect.x + 10 +
|
||||
ui_renderer->get_text_renderer()->get_text_width(_hostname_input.c_str(), 1.0f);
|
||||
ui_renderer->draw_rect((int)cursor_x, hostname_rect.y+5, 2, 20, text_color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Draw the security warning. */
|
||||
const char* warning_title = "Security Warning";
|
||||
float warning_title_width = ui_renderer->get_text_renderer()->get_text_width(warning_title, 1.0f);
|
||||
ui_renderer->render_text(warning_title, (_screen_width-warning_title_width)/2,
|
||||
_screen_height-120, warning_color);
|
||||
const std::vector<std::string> warning_lines = {
|
||||
"Passwords in Bettola are stored in plain text by design.",
|
||||
"Players, can, and will attempt to obtain your password.",
|
||||
"DO NOT use a password you have used for any other game or service.",
|
||||
"Use a unique, throwaway password for this game only!"
|
||||
};
|
||||
for(size_t i = 0; i < warning_lines.size(); ++i) {
|
||||
const std::string& line = warning_lines[i];
|
||||
float line_width = ui_renderer->get_text_renderer()->get_text_width(line.c_str(), 1.0f);
|
||||
ui_renderer->render_text(line.c_str(), (_screen_width-line_width)/2,
|
||||
_screen_height-100+(i*20), warning_color);
|
||||
}
|
||||
|
||||
ui_renderer->flush_shapes();
|
||||
ui_renderer->flush_text();
|
||||
}
|
||||
|
||||
void LoginScreen::set_error_message(const std::string& msg) {
|
||||
_error_message = msg;
|
||||
}
|
||||
|
||||
void LoginScreen::handle_event(const SDL_Event* event) {
|
||||
if(event->type == SDL_EVENT_TEXT_INPUT) {
|
||||
/* Append character to active input string. */
|
||||
switch(_active_field) {
|
||||
case 0: _username_input += event->text.text; break;
|
||||
case 1: _password_input += event->text.text; break;
|
||||
case 2: if(_is_new_account) { _hostname_input += event->text.text; } break;
|
||||
}
|
||||
} else if(event->type == SDL_EVENT_KEY_DOWN) {
|
||||
if(event->key.key == SDLK_BACKSPACE) {
|
||||
/* Handle backspace. */
|
||||
switch(_active_field) {
|
||||
case 0: if(!_username_input.empty()) { _username_input.pop_back(); } break;
|
||||
case 1: if(!_password_input.empty()) { _password_input.pop_back(); } break;
|
||||
case 2: if(_is_new_account && !_hostname_input.empty()) {
|
||||
_hostname_input.pop_back();
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if(event->key.key == SDLK_TAB) {
|
||||
/* Tab to switch fields. */
|
||||
int num_fields = _is_new_account ? 3 : 2;
|
||||
_active_field = (_active_field+1) % num_fields;
|
||||
} else if(event->key.key == SDLK_RETURN) {
|
||||
_login_attempted = true;
|
||||
}
|
||||
} else if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
/* Handle mouse clicks. */
|
||||
int mouse_x = event->button.x;
|
||||
int mouse_y = event->button.y;
|
||||
|
||||
/* Recalculate rects to check for clicks. */
|
||||
const int box_width = 300;
|
||||
const int start_y = _screen_height / 2 - 100;
|
||||
const int center_x = (_screen_width - box_width) / 2;
|
||||
|
||||
const Rect username_rect = { center_x, start_y+40, box_width, 30 };
|
||||
const Rect password_rect = { center_x, start_y+100, box_width, 30 };
|
||||
const Rect hostname_rect = { center_x, start_y+160, box_width, 30 };
|
||||
|
||||
auto is_inside = [&](const Rect& rect) {
|
||||
return mouse_x >= rect.x && mouse_x <= rect.x + rect.w &&
|
||||
mouse_y >= rect.y && mouse_y <= rect.y + rect.h;
|
||||
};
|
||||
|
||||
if(is_inside(_login_tab_rect)) {
|
||||
_is_new_account = false;
|
||||
_username_input.clear(); _password_input.clear(); _hostname_input.clear();
|
||||
} else if(is_inside(_create_account_tab_rect)) {
|
||||
_is_new_account = true;
|
||||
_username_input.clear(); _password_input.clear(); _hostname_input.clear();
|
||||
} else if(is_inside(username_rect)) {
|
||||
_active_field = 0;
|
||||
} else if(is_inside(password_rect)) {
|
||||
_active_field = 1;
|
||||
} else if(_is_new_account && is_inside(hostname_rect)) {
|
||||
_active_field = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
client/src/ui/login_screen.h
Normal file
43
client/src/ui/login_screen.h
Normal file
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string>
|
||||
|
||||
#include "gfx/types.h"
|
||||
|
||||
class LoginScreen {
|
||||
public:
|
||||
LoginScreen(int screen_width, int screen_height);
|
||||
~LoginScreen(void);
|
||||
|
||||
void update(float dt);
|
||||
void render(const RenderContext& context) const;
|
||||
void handle_event(const SDL_Event* event);
|
||||
|
||||
bool is_login_attempted(void) const { return _login_attempted; }
|
||||
std::string get_username(void) const { return _username_input; }
|
||||
std::string get_password(void) const { return _password_input; }
|
||||
std::string get_hostname(void) const { return _hostname_input; }
|
||||
bool is_new_account_mode(void) const { return _is_new_account; }
|
||||
void clear_login_attempt(void) { _login_attempted = false;}
|
||||
void set_error_message(const std::string& msg);
|
||||
|
||||
private:
|
||||
/* UI State. */
|
||||
std::string _username_input;
|
||||
std::string _password_input;
|
||||
std::string _hostname_input;
|
||||
std::string _error_message;
|
||||
|
||||
bool _login_attempted = false;
|
||||
bool _is_new_account = true;
|
||||
int _active_field = 0; /* 0: username, 1: password, 2: hostname. */
|
||||
|
||||
/* Screen dimensions. */
|
||||
int _screen_width;
|
||||
int _screen_height;
|
||||
|
||||
/* Clickable tabs for mode switching. */
|
||||
Rect _create_account_tab_rect;
|
||||
Rect _login_tab_rect;
|
||||
};
|
||||
148
client/src/ui/main_menu.cpp
Normal file
148
client/src/ui/main_menu.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include "ui/ui_renderer.h"
|
||||
|
||||
#include "main_menu.h"
|
||||
|
||||
/* Get a random snippet form loaded word list. */
|
||||
static const std::string& get_random_snippet(const std::vector<std::string>& snippets) {
|
||||
/* If empty, return a default to avoid a crash? */
|
||||
if(snippets.empty()) {
|
||||
static const std::string empty_str = "ERROR: snippets file not found";
|
||||
return empty_str;
|
||||
}
|
||||
int index = rand() % snippets.size();
|
||||
return snippets[index];
|
||||
}
|
||||
|
||||
MainMenu::MainMenu(int screen_width, int screen_height) :
|
||||
_screen_width(screen_width), _screen_height(screen_height),
|
||||
_next_screen(Screen::MAIN_MENU) {
|
||||
|
||||
/* Load snippets form file. */
|
||||
std::ifstream snippet_file("assets/menu_background_snippets.txt");
|
||||
if(!snippet_file.is_open()) {
|
||||
printf("ERROR: Failed to open assets/menu_background_snippets.txt\n");
|
||||
} else {
|
||||
std::string line;
|
||||
while(std::getline(snippet_file, line)) {
|
||||
if(!line.empty()) {
|
||||
_snippets.push_back(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialise buttons. */
|
||||
const int button_width = 200;
|
||||
const int button_height = 50;
|
||||
const int center_x = (screen_width/2) - (button_width/2);
|
||||
const int center_y = (screen_height/2);
|
||||
|
||||
_buttons.push_back({
|
||||
"Single-Player",
|
||||
{ center_x, center_y + 30, button_width, button_height },
|
||||
Screen::LOGIN, /* This will trigger the login screen. */
|
||||
true,
|
||||
false
|
||||
});
|
||||
_buttons.push_back({
|
||||
"Online",
|
||||
{ center_x, center_y - 30, button_width, button_height },
|
||||
Screen::LOGIN,
|
||||
false,
|
||||
false
|
||||
});
|
||||
|
||||
/* Initialise animated background. */
|
||||
srand(time(NULL));
|
||||
for(int i = 0; i < 100; ++i) {
|
||||
_background_text.push_back({
|
||||
get_random_snippet(_snippets),
|
||||
(float)(rand() % screen_width),
|
||||
(float)(rand() % screen_height),
|
||||
(float)(rand() % 50 + 20) / 100.0f /* Random speed. */
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MainMenu::~MainMenu(void) {}
|
||||
|
||||
void MainMenu::handle_event(SDL_Event* event) {
|
||||
if(event->type == SDL_EVENT_MOUSE_MOTION) {
|
||||
int mouse_x = event->motion.x;
|
||||
int mouse_y = event->motion.y;
|
||||
for(auto& button : _buttons) {
|
||||
button.is_hovered = (mouse_x >= button.rect.x
|
||||
&& mouse_x <= button.rect.x + button.rect.w
|
||||
&& mouse_y >= button.rect.y
|
||||
&& mouse_y <= button.rect.y + button.rect.h);
|
||||
}
|
||||
} else if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
for(const auto& button : _buttons) {
|
||||
if(button.is_hovered) {
|
||||
_next_screen = button.action;
|
||||
_clicked_button = &button;
|
||||
break; /* Once clicked button found, exit. */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Screen MainMenu::update(float dt) {
|
||||
_update_background(dt);
|
||||
return _next_screen;
|
||||
}
|
||||
|
||||
void MainMenu::render(UIRenderer* ui_renderer) {
|
||||
/* Pass 1: Background text. */
|
||||
ui_renderer->begin_text();
|
||||
_render_background(ui_renderer);
|
||||
ui_renderer->flush_text();
|
||||
|
||||
/* Pass 2: Buttons. */
|
||||
ui_renderer->begin_shapes();
|
||||
ui_renderer->begin_text();
|
||||
|
||||
/* Button colours. */
|
||||
const Color button_color = { 0.1f, 0.15f, 0.2f };
|
||||
const Color button_hover_color = { 0.2f, 0.25f, 0.3f };
|
||||
const Color text_color = { 0.8f, 0.8f, 0.8f };
|
||||
|
||||
for(const auto& button : _buttons) {
|
||||
/* Draw button background. */
|
||||
if(button.is_hovered) {
|
||||
ui_renderer->draw_rect(button.rect.x, button.rect.y, button.rect.w,
|
||||
button.rect.h, button_hover_color);
|
||||
} else {
|
||||
ui_renderer->draw_rect(button.rect.x, button.rect.y, button.rect.w,
|
||||
button.rect.h, button_color);
|
||||
}
|
||||
|
||||
/* Draw button text centered. */
|
||||
float text_width = ui_renderer->get_text_renderer()->get_text_width(button.label.c_str(), 1.0f);
|
||||
float text_x = button.rect.x + (button.rect.w - text_width) / 2.0f;
|
||||
ui_renderer->render_text(button.label.c_str(), text_x, button.rect.y + 32, text_color);
|
||||
}
|
||||
|
||||
ui_renderer->flush_shapes();
|
||||
ui_renderer->flush_text();
|
||||
}
|
||||
|
||||
void MainMenu::_update_background(float dt) {
|
||||
for(auto& line : _background_text) {
|
||||
line.y -= line.speed * dt * 100.0f;
|
||||
if(line.y < 0) {
|
||||
line.y = _screen_height;
|
||||
line.x = (float)(rand() % _screen_width);
|
||||
line.text = get_random_snippet(_snippets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainMenu::_render_background(UIRenderer* ui_renderer) {
|
||||
const Color background_text_color = { 0.0f, 0.35f, 0.15f }; /* Dark green. */
|
||||
for(const auto& line : _background_text) {
|
||||
ui_renderer->render_text(line.text.c_str(), line.x, line.y, background_text_color);
|
||||
}
|
||||
}
|
||||
52
client/src/ui/main_menu.h
Normal file
52
client/src/ui/main_menu.h
Normal file
@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "gfx/types.h"
|
||||
#include "ui/ui_renderer.h"
|
||||
#include "game_state.h"
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
struct MenuButton {
|
||||
std::string label;
|
||||
Rect rect;
|
||||
Screen action; /* Change state. */
|
||||
bool is_single_player;
|
||||
bool is_hovered = false;
|
||||
};
|
||||
|
||||
class MainMenu {
|
||||
public:
|
||||
MainMenu(int screen_width, int screen_height);
|
||||
~MainMenu(void);
|
||||
|
||||
void handle_event(SDL_Event* event);
|
||||
Screen update(float dt);
|
||||
void render(UIRenderer* ui_renderer);
|
||||
|
||||
const MenuButton* get_clicked_button(void) const { return _clicked_button; }
|
||||
|
||||
private:
|
||||
void _update_background(float dt);
|
||||
void _render_background(UIRenderer* ui_renderer);
|
||||
|
||||
/* For animated background. */
|
||||
struct ScrollingText {
|
||||
std::string text;
|
||||
float x;
|
||||
float y;
|
||||
float speed;
|
||||
};
|
||||
|
||||
std::vector<ScrollingText> _background_text;
|
||||
int _screen_height;
|
||||
int _screen_width;
|
||||
|
||||
/* Buttons. */
|
||||
std::vector<MenuButton> _buttons;
|
||||
std::vector<std::string> _snippets;
|
||||
Screen _next_screen;
|
||||
const MenuButton* _clicked_button = nullptr;
|
||||
};
|
||||
112
client/src/ui/menu_bar.cpp
Normal file
112
client/src/ui/menu_bar.cpp
Normal file
@ -0,0 +1,112 @@
|
||||
#include "menu_bar.h"
|
||||
#include "gfx/shape_renderer.h"
|
||||
#include "gfx/txt_renderer.h"
|
||||
#include "gfx/types.h"
|
||||
|
||||
MenuBar::MenuBar(int height)
|
||||
: _height(height) {}
|
||||
|
||||
MenuBar::~MenuBar(void) {}
|
||||
|
||||
void MenuBar::add_menu(const std::string& label) {
|
||||
_menus.push_back({label, {}, false});
|
||||
}
|
||||
|
||||
void MenuBar::add_menu_item(const std::string& menu_label, const std::string& item_label,
|
||||
std::function<void()> action) {
|
||||
for(auto& menu : _menus) {
|
||||
if(menu.label == menu_label) {
|
||||
menu.items.push_back({item_label, action});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int MenuBar::get_height(void) const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
void MenuBar::handle_event(SDL_Event* event, int window_x, int window_y) {
|
||||
if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
int mouse_x = event->button.x;
|
||||
int mouse_y = event->button.y;
|
||||
const int title_bar_height = 30;
|
||||
|
||||
int menu_bar_screen_y = window_y + title_bar_height;
|
||||
int current_menu_x = window_x;
|
||||
bool click_on_bar = false;
|
||||
|
||||
if(mouse_y >= menu_bar_screen_y && mouse_y <= menu_bar_screen_y + _height) {
|
||||
for(size_t i = 0; i < _menus.size(); ++i) {
|
||||
int menu_width = 60;
|
||||
if(mouse_x >= current_menu_x && mouse_x <= current_menu_x + menu_width) {
|
||||
_open_menu_index = (_open_menu_index == (int)i) ? -1: i;
|
||||
click_on_bar = true;
|
||||
break;
|
||||
}
|
||||
current_menu_x += menu_width;
|
||||
}
|
||||
}
|
||||
|
||||
if(click_on_bar) return;
|
||||
|
||||
if(_open_menu_index != -1) {
|
||||
Menu& open_menu = _menus[_open_menu_index];
|
||||
int dropdown_x = window_x + (_open_menu_index * 60);
|
||||
int dropdown_y = menu_bar_screen_y + _height;
|
||||
int item_height = 30;
|
||||
int dropdown_width = 150;
|
||||
|
||||
for(const auto& item: open_menu.items) {
|
||||
if(mouse_x >= dropdown_x && mouse_x <= dropdown_x + dropdown_width &&
|
||||
mouse_y >= dropdown_y && mouse_y <= dropdown_y + item_height) {
|
||||
item.action();
|
||||
_open_menu_index = -1; /* Close menu. */
|
||||
return;
|
||||
}
|
||||
dropdown_y += item_height;
|
||||
}
|
||||
}
|
||||
/* Close any open menu when clicked outside. */
|
||||
_open_menu_index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void MenuBar::render_bar_bg(UIRenderer* ui_renderer, int x, int y, int width) {
|
||||
const Color bg_color = { 0.15f, 0.17f, 0.19f };
|
||||
ui_renderer->draw_rect(x, y, width, _height, bg_color);
|
||||
}
|
||||
|
||||
void MenuBar::render_bar_text(UIRenderer *ui_renderer, int x, int y, int width) {
|
||||
const Color text_color = { 0.9f, 0.9f, 0.9f };
|
||||
|
||||
int menu_x = x;
|
||||
for(size_t i = 0; i < _menus.size(); ++i) {
|
||||
int menu_width = 60;
|
||||
ui_renderer->render_text(_menus[i].label.c_str(), menu_x+10, y+20, text_color);
|
||||
menu_x += menu_width;
|
||||
}
|
||||
}
|
||||
|
||||
void MenuBar::render_dropdown(UIRenderer* ui_renderer, int x, int y, int width ) {
|
||||
if(_open_menu_index == -1) return;
|
||||
|
||||
const Color bg_color = { 0.15f, 0.17f, 0.19f };
|
||||
const Color text_color = { 0.9f, 0.9f, 0.9f };
|
||||
|
||||
ui_renderer->begin_shapes();
|
||||
ui_renderer->begin_text();
|
||||
|
||||
int menu_x = x + (_open_menu_index*60);
|
||||
int item_y = y + _height;
|
||||
int item_height = 30;
|
||||
int dropdown_width = 150;
|
||||
for(const auto& item : _menus[_open_menu_index].items) {
|
||||
ui_renderer->draw_rect(menu_x, item_y, dropdown_width, item_height, bg_color);
|
||||
ui_renderer->render_text(item.label.c_str(), menu_x+10, item_y+20, text_color);
|
||||
item_y += item_height;
|
||||
}
|
||||
|
||||
ui_renderer->flush_shapes();
|
||||
ui_renderer->flush_text();
|
||||
}
|
||||
40
client/src/ui/menu_bar.h
Normal file
40
client/src/ui/menu_bar.h
Normal file
@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
#include "ui/ui_renderer.h"
|
||||
|
||||
struct MenuItem {
|
||||
std::string label;
|
||||
std::function<void()> action;
|
||||
};
|
||||
|
||||
struct Menu {
|
||||
std::string label;
|
||||
std::vector<MenuItem> items;
|
||||
bool is_open = false;
|
||||
};
|
||||
|
||||
class MenuBar {
|
||||
public:
|
||||
MenuBar(int height);
|
||||
~MenuBar(void);
|
||||
|
||||
void add_menu(const std::string& label);
|
||||
void add_menu_item(const std::string& menu_label, const std::string& item_label,
|
||||
std::function<void()> action);
|
||||
|
||||
void handle_event(SDL_Event* event, int window_x, int window_y);
|
||||
void render_bar_bg(UIRenderer* ui_renderer, int x, int y, int width);
|
||||
void render_bar_text(UIRenderer* ui_renderer, int x, int y, int width);
|
||||
void render_dropdown(UIRenderer* ui_renderer, int x, int y, int width);
|
||||
|
||||
int get_height(void) const;
|
||||
private:
|
||||
int _height;
|
||||
std::vector<Menu> _menus;
|
||||
int _open_menu_index = -1;
|
||||
};
|
||||
114
client/src/ui/taskbar.cpp
Normal file
114
client/src/ui/taskbar.cpp
Normal file
@ -0,0 +1,114 @@
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include "taskbar.h"
|
||||
#include "ui/ui_renderer.h"
|
||||
#include "ui/ui_window.h"
|
||||
|
||||
Taskbar::Taskbar(int screen_width, int screen_height) {
|
||||
_width = screen_width;
|
||||
_height = 32;
|
||||
_y_pos = screen_height - _height; /* Taskbar at bottom because boring? */
|
||||
_start_button_width = 60;
|
||||
}
|
||||
|
||||
Taskbar::~Taskbar(void) {}
|
||||
|
||||
void Taskbar::add_window(UIWindow* window) {
|
||||
_buttons.push_back({window, window->get_title()});
|
||||
}
|
||||
|
||||
void Taskbar::remove_window(UIWindow* window) {
|
||||
_buttons.erase(
|
||||
std::remove_if(_buttons.begin(), _buttons.end(),
|
||||
[window](const TaskbarButton& btn) {
|
||||
return btn.window == window;
|
||||
}),
|
||||
_buttons.end());
|
||||
}
|
||||
|
||||
void Taskbar::render(UIRenderer* ui_renderer, UIWindow* focused_window) {
|
||||
const Color taskbar_color = { 0.1f, 0.12f, 0.14f };
|
||||
const Color button_color = { 0.2f, 0.22f, 0.24f };
|
||||
const Color button_focused_color = { 0.3f, 0.32f, 0.34f };
|
||||
const Color button_text_color = { 0.9f, 0.9f, 0.9f };
|
||||
|
||||
ui_renderer->begin_shapes();
|
||||
ui_renderer->begin_text();
|
||||
|
||||
ui_renderer->draw_rect(0, _y_pos, _width, _height, taskbar_color);
|
||||
|
||||
/* Draw start button. */
|
||||
ui_renderer->draw_rect(5, _y_pos + 5, _start_button_width - 10,
|
||||
_height - 10, button_color);
|
||||
ui_renderer->render_text("[B]", 20, _y_pos + 20, button_text_color);
|
||||
|
||||
/* Draw app buttons. */
|
||||
int button_width = 150;
|
||||
int padding = 5;
|
||||
int x_offset = _start_button_width + padding;
|
||||
|
||||
for(const auto& button: _buttons) {
|
||||
bool is_focused = (button.window == focused_window);
|
||||
ui_renderer->draw_rect(x_offset, _y_pos + padding, button_width,
|
||||
_height - (padding * 2),
|
||||
is_focused ? button_focused_color : button_color);
|
||||
|
||||
/* TODO: Truncate text when too long. */
|
||||
ui_renderer->render_text(button.title.c_str(), x_offset + 10,
|
||||
_y_pos + 20, button_text_color);
|
||||
x_offset += button_width + padding;
|
||||
}
|
||||
/* Draw clock. */
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto in_time_t = std::chrono::system_clock::to_time_t(now);
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(std::localtime(&in_time_t), "%H:%M");
|
||||
std::string time_str = ss.str();
|
||||
|
||||
ui_renderer->render_text(time_str.c_str(), _width-50, _y_pos+20, button_text_color);
|
||||
|
||||
ui_renderer->flush_shapes();
|
||||
ui_renderer->flush_text();
|
||||
}
|
||||
|
||||
UIWindow* Taskbar::handle_event(SDL_Event* event, int screen_height) {
|
||||
if(event->type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
int mouse_x = event->button.x;
|
||||
int mouse_y = event->button.y;
|
||||
|
||||
int button_width = 150;
|
||||
int padding = 5;
|
||||
int x_offset = _start_button_width + padding;
|
||||
for(const auto& button : _buttons) {
|
||||
if(mouse_x >= x_offset && mouse_x <= x_offset + button_width &&
|
||||
mouse_y >= _y_pos && mouse_y <= _y_pos + _height) {
|
||||
return button.window; /* Return clicked window. */
|
||||
}
|
||||
x_offset += button_width + padding;
|
||||
}
|
||||
}
|
||||
return nullptr; /* No window button was clicked. */
|
||||
}
|
||||
|
||||
bool Taskbar::is_start_button_clicked(SDL_Event* event, int screen_height) {
|
||||
if(event->type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
int mouse_x = event->button.x;
|
||||
int mouse_y = event->button.y;
|
||||
return (mouse_x >= 0 && mouse_x <= _start_button_width && mouse_y >= _y_pos &&
|
||||
mouse_y <= _y_pos + _height);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int Taskbar::get_height(void) const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
bool Taskbar::is_point_inside(int x, int y) const {
|
||||
return (y >= _y_pos && y<= _y_pos + _height);
|
||||
}
|
||||
35
client/src/ui/taskbar.h
Normal file
35
client/src/ui/taskbar.h
Normal file
@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class UIWindow;
|
||||
class UIRenderer;
|
||||
|
||||
struct TaskbarButton {
|
||||
UIWindow* window;
|
||||
std::string title;
|
||||
};
|
||||
|
||||
class Taskbar {
|
||||
public:
|
||||
Taskbar(int screen_width, int screen_height);
|
||||
~Taskbar(void);
|
||||
|
||||
void add_window(UIWindow* window);
|
||||
void remove_window(UIWindow* window);
|
||||
|
||||
void render(UIRenderer* ui_renderer, UIWindow* focused_window);
|
||||
UIWindow* handle_event(SDL_Event* event, int screen_height);
|
||||
bool is_start_button_clicked(SDL_Event* event, int screen_height);
|
||||
bool is_point_inside(int x, int y) const;
|
||||
|
||||
int get_height(void) const;
|
||||
private:
|
||||
std::vector<TaskbarButton> _buttons;
|
||||
int _width;
|
||||
int _height;
|
||||
int _y_pos;
|
||||
int _start_button_width;
|
||||
};
|
||||
250
client/src/ui/text_view.cpp
Normal file
250
client/src/ui/text_view.cpp
Normal file
@ -0,0 +1,250 @@
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <string>
|
||||
|
||||
#include "text_view.h"
|
||||
#include "gfx/types.h"
|
||||
#include "ui/editor.h"
|
||||
#include "ui/text_buffer.h"
|
||||
#include "ui/ui_renderer.h"
|
||||
|
||||
TextView::TextView(TextBuffer* buffer, bool handle_ret, bool show_line_numbers,
|
||||
bool syntax_highlighting) :
|
||||
_buffer(buffer),
|
||||
_scroll_offset(0),
|
||||
_handle_ret(handle_ret),
|
||||
_show_line_numbers(show_line_numbers),
|
||||
_syntax_highlighting(syntax_highlighting) {
|
||||
|
||||
_lua_keywords = {
|
||||
"and", "break", "do", "else", "elseif", "end", "false", "for",
|
||||
"function", "if", "in", "local", "nil", "not", "or", "repeat",
|
||||
"return", "then", "true", "until", "while"
|
||||
};
|
||||
}
|
||||
|
||||
TextView::~TextView(void) = default;
|
||||
|
||||
bool TextView::handle_event(SDL_Event* event, int content_height) {
|
||||
if(!_buffer) return false;
|
||||
|
||||
if(event->type == SDL_EVENT_TEXT_INPUT) {
|
||||
_buffer->insert_char(event->text.text[0]);
|
||||
_ensure_cursor_visible(content_height);
|
||||
} else if(event->type == SDL_EVENT_KEY_DOWN) {
|
||||
switch(event->key.key) {
|
||||
case SDLK_BACKSPACE:
|
||||
_buffer->backspace();
|
||||
_ensure_cursor_visible(content_height);
|
||||
break;
|
||||
case SDLK_RETURN:
|
||||
/*
|
||||
* For editor, we want a real newline.
|
||||
* For terminal, we just want to submit. No newline.
|
||||
* We'll return true and let the parent component do the do'ing.
|
||||
*/
|
||||
if(_handle_ret) {
|
||||
_buffer->newline();
|
||||
}
|
||||
_ensure_cursor_visible(content_height);
|
||||
return true;
|
||||
break;
|
||||
case SDLK_LEFT:
|
||||
_buffer->move_cursor(-1,0);
|
||||
_ensure_cursor_visible(content_height);
|
||||
break;
|
||||
case SDLK_RIGHT:
|
||||
_buffer->move_cursor(1,0);
|
||||
_ensure_cursor_visible(content_height);
|
||||
break;
|
||||
case SDLK_UP:
|
||||
_buffer->move_cursor(0,-1);
|
||||
_ensure_cursor_visible(content_height);
|
||||
break;
|
||||
case SDLK_DOWN:
|
||||
_buffer->move_cursor(0,1);
|
||||
_ensure_cursor_visible(content_height);
|
||||
break;
|
||||
case SDLK_HOME:
|
||||
_buffer->move_cursor_home();
|
||||
_ensure_cursor_visible(content_height);
|
||||
break;
|
||||
case SDLK_END:
|
||||
_buffer->move_cursor_end();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TextView::scroll(int amount, int content_height) {
|
||||
_scroll_offset += amount;
|
||||
|
||||
if(_scroll_offset < 0) {
|
||||
_scroll_offset = 0;
|
||||
}
|
||||
float line_height = 20.0f;
|
||||
int visible_lines = static_cast<int>(content_height / line_height)-1;
|
||||
int max_scroll = _buffer->get_line_count() - visible_lines;
|
||||
/* Allow one extra scroll step if content is larger than visible area. */
|
||||
if(_buffer->get_line_count() > visible_lines) max_scroll += 1;
|
||||
if(max_scroll < 0) {
|
||||
max_scroll = 0;
|
||||
}
|
||||
if(max_scroll < 0) max_scroll = 0;
|
||||
if(_scroll_offset > max_scroll) {
|
||||
_scroll_offset = max_scroll;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Token> TextView::_tokenize_line(const std::string& line) {
|
||||
std::vector<Token> tokens;
|
||||
std::string current_token_text;
|
||||
TokenType current_token_type = TokenType::NORMAL;
|
||||
|
||||
for(size_t i = 0; i < line.length(); ++i) {
|
||||
char c = line[i];
|
||||
|
||||
/* Check for comments. */
|
||||
if(i + 1 < line.length() && c == '-' && line[i+1] == '-') {
|
||||
if(!current_token_text.empty()) {
|
||||
tokens.push_back({current_token_type, current_token_text});
|
||||
}
|
||||
tokens.push_back({TokenType::COMMENT, line.substr(i)});
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/* Check for strings. */
|
||||
if(c == '"') {
|
||||
if(!current_token_text.empty()) {
|
||||
tokens.push_back({current_token_type, current_token_text});
|
||||
}
|
||||
size_t end_quote = line.find('"', i+1);
|
||||
if(end_quote == std::string::npos) {
|
||||
tokens.push_back({TokenType::STRING, line.substr(i)});
|
||||
return tokens;
|
||||
}
|
||||
tokens.push_back({TokenType::STRING, line.substr(i, end_quote - i+1)});
|
||||
i = end_quote;
|
||||
current_token_text = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check for numbers. */
|
||||
if(isdigit(c) && (current_token_text.empty() || current_token_type == TokenType::NUMBER)) {
|
||||
current_token_type = TokenType::NUMBER;
|
||||
current_token_text += c;
|
||||
} else if(isalnum(c) || c == '_') { /* Keywords and identifiers. */
|
||||
if(current_token_type != TokenType::NORMAL) {
|
||||
tokens.push_back({current_token_type, current_token_text});
|
||||
current_token_text = "";
|
||||
}
|
||||
current_token_type = TokenType::NORMAL;
|
||||
current_token_text += c;
|
||||
} else { /* Delimiters. */
|
||||
if(!current_token_text.empty()) {
|
||||
if(_lua_keywords.count(current_token_text)) {
|
||||
tokens.push_back({TokenType::KEYWORD, current_token_text});
|
||||
} else {
|
||||
tokens.push_back({current_token_type, current_token_text});
|
||||
}
|
||||
current_token_text = "";
|
||||
}
|
||||
tokens.push_back({TokenType::NORMAL, std::string(1, c)});
|
||||
current_token_type = TokenType::NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
if(!current_token_text.empty()) {
|
||||
if(_lua_keywords.count(current_token_text)) {
|
||||
tokens.push_back({TokenType::KEYWORD, current_token_text});
|
||||
} else {
|
||||
tokens.push_back({current_token_type, current_token_text});
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
void TextView::render_text_content(UIRenderer* ui_renderer, const SyntaxTheme& theme,
|
||||
int x, int y, int width, int height) {
|
||||
if(!_buffer) return;
|
||||
|
||||
const float line_height = 20.0f; /* TODO: Get font metrics? */
|
||||
const float padding = 5.0f;
|
||||
const float gutter_width = _show_line_numbers ? 40.0f : 0.0f;
|
||||
|
||||
float current_y = y;
|
||||
|
||||
for(size_t i = _scroll_offset; i < _buffer->get_line_count(); ++i) {
|
||||
/*
|
||||
* Culling: If the top of the current line is already below the bottom of the
|
||||
* view, we can stop rendering completely.
|
||||
*/
|
||||
if(current_y >= y + height) {
|
||||
break;
|
||||
}
|
||||
|
||||
if(_show_line_numbers) {
|
||||
/* Render line number. */
|
||||
std::string line_num_str = std::to_string(i+1);
|
||||
float line_num_text_width =
|
||||
ui_renderer->get_text_renderer()->get_text_width(line_num_str.c_str(), 1.0f);
|
||||
ui_renderer->render_text(line_num_str.c_str(), x+padding+(gutter_width-line_num_text_width-10),
|
||||
current_y+18, theme.line_num);
|
||||
}
|
||||
|
||||
float current_x = x+padding + gutter_width;
|
||||
if(_syntax_highlighting) {
|
||||
std::vector<Token> tokens = _tokenize_line(_buffer->get_line(i));
|
||||
for(const auto& token : tokens) {
|
||||
Color color = theme.normal;
|
||||
switch(token.type) {
|
||||
case TokenType::KEYWORD: color = theme.keyword; break;
|
||||
case TokenType::STRING: color = theme.string; break;
|
||||
case TokenType::NUMBER: color = theme.number; break;
|
||||
case TokenType::COMMENT: color = theme.comment; break;
|
||||
default: break;
|
||||
}
|
||||
ui_renderer->render_text(token.text.c_str(), current_x, current_y+18, color);
|
||||
current_x += ui_renderer->get_text_renderer()->get_text_width(token.text.c_str(), 1.0f);
|
||||
}
|
||||
} else {
|
||||
ui_renderer->render_text(_buffer->get_line(i).c_str(), current_x, current_y+18, theme.normal);
|
||||
}
|
||||
current_y += line_height;
|
||||
}
|
||||
}
|
||||
|
||||
void TextView::render_cursor(UIRenderer* ui_renderer, const SyntaxTheme& theme,
|
||||
int x, int y, int width, int height) {
|
||||
const float line_height = 20.0f; /* TODO: Get font metrics? */
|
||||
const float padding = 5.0f;
|
||||
const float gutter_width = _show_line_numbers ? 40.0f : 0.0f;
|
||||
Point cursor_pos = _buffer->get_cursor_pos();
|
||||
std::string line_before_cursor = _buffer->get_line(cursor_pos.row).substr(0, cursor_pos.col);
|
||||
float text_width =
|
||||
ui_renderer->get_text_renderer()->get_text_width(line_before_cursor.c_str(), 1.0f);
|
||||
float cursor_x = x + padding + gutter_width + text_width;
|
||||
float cursor_y = y + (cursor_pos.row - _scroll_offset) * line_height;
|
||||
if(cursor_y >= y && cursor_y < y + height) {
|
||||
ui_renderer->draw_rect(cursor_x, cursor_y, 2, line_height, theme.normal);
|
||||
}
|
||||
}
|
||||
|
||||
void TextView::_ensure_cursor_visible(int content_height) {
|
||||
Point cursor_pos = _buffer->get_cursor_pos();
|
||||
float line_height = 20.0f;
|
||||
int visible_lines = static_cast<int>(content_height / line_height)-1;
|
||||
|
||||
/* If cursor above current scroll view, scroll up. */
|
||||
if(cursor_pos.row < _scroll_offset) {
|
||||
_scroll_offset = cursor_pos.row;
|
||||
} else if(cursor_pos.row >= _scroll_offset + visible_lines) {
|
||||
/* Below, scroll down. */
|
||||
_scroll_offset = cursor_pos.row - visible_lines+1;
|
||||
}
|
||||
|
||||
/* Re-clamp scroll offset to ensure it's within valid bounds. */
|
||||
scroll(0, content_height);
|
||||
}
|
||||
49
client/src/ui/text_view.h
Normal file
49
client/src/ui/text_view.h
Normal file
@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "ui/text_buffer.h"
|
||||
#include "ui/ui_renderer.h"
|
||||
#include "ui/editor.h" /* For SyntaxTheme */
|
||||
|
||||
class TextRenderer;
|
||||
|
||||
enum class TokenType {
|
||||
NORMAL,
|
||||
KEYWORD,
|
||||
STRING,
|
||||
NUMBER,
|
||||
COMMENT
|
||||
};
|
||||
|
||||
struct Token {
|
||||
TokenType type;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
class TextView {
|
||||
public:
|
||||
TextView(TextBuffer* buffer, bool handle_ret, bool show_line_numbers, bool syntax_highlighting);
|
||||
~TextView(void);
|
||||
|
||||
bool handle_event(SDL_Event* event, int content_height);
|
||||
void scroll(int amount, int content_height);
|
||||
void render_text_content(UIRenderer* ui_renderer, const SyntaxTheme& theme,
|
||||
int x, int y, int width, int height);
|
||||
void render_cursor(UIRenderer* ui_renderer, const SyntaxTheme& theme,
|
||||
int x, int y, int width, int height);
|
||||
|
||||
private:
|
||||
void _ensure_cursor_visible(int content_height);
|
||||
std::vector<Token> _tokenize_line(const std::string& line);
|
||||
|
||||
TextBuffer* _buffer;
|
||||
int _scroll_offset;
|
||||
bool _handle_ret;
|
||||
bool _show_line_numbers;
|
||||
bool _syntax_highlighting;
|
||||
std::unordered_set<std::string> _lua_keywords;
|
||||
};
|
||||
64
client/src/ui/ui_renderer.cpp
Normal file
64
client/src/ui/ui_renderer.cpp
Normal file
@ -0,0 +1,64 @@
|
||||
#include <GL/glew.h>
|
||||
|
||||
#include "ui_renderer.h"
|
||||
#include "gfx/shape_renderer.h"
|
||||
#include "gfx/txt_renderer.h"
|
||||
|
||||
UIRenderer::UIRenderer(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int screen_height)
|
||||
: _shape_renderer(shape_renderer), _txt_renderer(txt_renderer), _screen_height(screen_height) {}
|
||||
|
||||
void UIRenderer::draw_rect(int x, int y, int width, int height, const Color& color) {
|
||||
if(!_shape_renderer) return;
|
||||
/* Convert top-left screen coord to bottom-left GL coord. */
|
||||
int y_gl = _screen_height - y - height;
|
||||
_shape_renderer->draw_rect(x, y_gl, width, height, color);
|
||||
}
|
||||
|
||||
void UIRenderer::draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3, const Color& color) {
|
||||
if(!_shape_renderer) return;
|
||||
/* Convert top-left screen coord to bottom-left GL coord. */
|
||||
int y1_gl = _screen_height - y1;
|
||||
int y2_gl = _screen_height - y2;
|
||||
int y3_gl = _screen_height - y3;
|
||||
_shape_renderer->draw_triangle(x1, y1_gl, x2, y2_gl, x3, y3_gl, color);
|
||||
}
|
||||
|
||||
void UIRenderer::render_text(const char* text, int x, int y, const Color& color) {
|
||||
if(!_txt_renderer) return;
|
||||
/* Convert the screen-space baseline y-coord to GL-space baseline y-coord. */
|
||||
int y_gl = _screen_height - y;
|
||||
_txt_renderer->render_text(text, x, y_gl, color);
|
||||
}
|
||||
|
||||
void UIRenderer::begin_text(void) {
|
||||
if(!_txt_renderer) return;
|
||||
_txt_renderer->begin();
|
||||
}
|
||||
|
||||
void UIRenderer::flush_text(void) {
|
||||
if(!_txt_renderer) return;
|
||||
_txt_renderer->flush();
|
||||
}
|
||||
|
||||
void UIRenderer::begin_shapes(void) {
|
||||
if(!_shape_renderer) return;
|
||||
_shape_renderer->begin();
|
||||
}
|
||||
|
||||
void UIRenderer::flush_shapes(void) {
|
||||
if(!_shape_renderer) return;
|
||||
_shape_renderer->flush();
|
||||
}
|
||||
|
||||
TextRenderer* UIRenderer::get_text_renderer(void) {
|
||||
return _txt_renderer;
|
||||
}
|
||||
|
||||
void UIRenderer::enable_scissor(int x, int y, int width, int height) {
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(x, y, width, height);
|
||||
}
|
||||
|
||||
void UIRenderer::disable_scissor(void) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
36
client/src/ui/ui_renderer.h
Normal file
36
client/src/ui/ui_renderer.h
Normal file
@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "gfx/shape_renderer.h"
|
||||
#include "gfx/txt_renderer.h"
|
||||
#include "gfx/types.h"
|
||||
|
||||
/*
|
||||
* I'm so damn sick of working between two rendering systems!
|
||||
* Here! Have a wrapper around the low-level renderers to provide a f.cking
|
||||
* consistant top-left origin for all UI components.
|
||||
*/
|
||||
class UIRenderer {
|
||||
public:
|
||||
UIRenderer(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int screen_height);
|
||||
|
||||
void draw_rect(int x, int y, int width, int height, const Color& color);
|
||||
void draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3, const Color& color);
|
||||
void render_text(const char* text, int x, int y, const Color& color);
|
||||
|
||||
void begin_text(void);
|
||||
void flush_text(void);
|
||||
|
||||
void begin_shapes(void);
|
||||
void flush_shapes(void);
|
||||
|
||||
/* Expose underlying text renderer for things like width calculation. */
|
||||
TextRenderer* get_text_renderer(void);
|
||||
|
||||
void enable_scissor(int x, int y, int width, int height);
|
||||
void disable_scissor(void);
|
||||
|
||||
private:
|
||||
ShapeRenderer* _shape_renderer;
|
||||
TextRenderer* _txt_renderer;
|
||||
int _screen_height;
|
||||
};
|
||||
224
client/src/ui/ui_window.cpp
Normal file
224
client/src/ui/ui_window.cpp
Normal file
@ -0,0 +1,224 @@
|
||||
#include "ui_window.h"
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
#include <memory>
|
||||
#include "ui/i_window_content.h"
|
||||
#include "ui_renderer.h"
|
||||
|
||||
UIWindow::UIWindow(const char* title, int x, int y, int width, int height) :
|
||||
_title(title),
|
||||
_x(x),
|
||||
_y(y),
|
||||
_width(width),
|
||||
_height(height),
|
||||
_content(nullptr),
|
||||
_is_dragging(false),
|
||||
_is_hovered(false),
|
||||
_is_focused(false),
|
||||
_should_close(false),
|
||||
_state(WindowState::NORMAL),
|
||||
_is_resizing(false),
|
||||
_resize_margin(10) {
|
||||
/* Init title bar buttons. */
|
||||
_title_bar_buttons.push_back({WindowButtonAction::CLOSE, {0.8f, 0.2f, 0.2f}});
|
||||
_title_bar_buttons.push_back({WindowButtonAction::MAXIMIZE, {0.2f, 0.8f, 0.2f}});
|
||||
_title_bar_buttons.push_back({WindowButtonAction::MINIMIZE, {0.8f, 0.8f, 0.2f}});
|
||||
}
|
||||
|
||||
UIWindow::~UIWindow(void) {}
|
||||
|
||||
void UIWindow::minimize(void) {
|
||||
_state = WindowState::MINIMIZED;
|
||||
}
|
||||
|
||||
void UIWindow::restore(void) {
|
||||
_state = WindowState::NORMAL;
|
||||
}
|
||||
|
||||
bool UIWindow::is_minimized(void) const {
|
||||
return _state == WindowState::MINIMIZED;
|
||||
}
|
||||
|
||||
const std::string& UIWindow::get_title(void) const {
|
||||
return _title;
|
||||
}
|
||||
|
||||
Rect UIWindow::get_rect(void) const {
|
||||
return { _x, _y, _width, _height };
|
||||
}
|
||||
|
||||
bool UIWindow::is_mouse_over_resize_handle(int mouse_x, int mouse_y) const {
|
||||
return (mouse_x >= _x + _width - _resize_margin && mouse_x <= _x + _width &&
|
||||
mouse_y >= _y + _height - _resize_margin &&
|
||||
mouse_y <= _y + _height);
|
||||
}
|
||||
|
||||
bool UIWindow::should_close(void) const {
|
||||
return _should_close;
|
||||
}
|
||||
|
||||
void UIWindow::update(void) {
|
||||
if(_content) {
|
||||
if(_content->should_close()) { _should_close = true; }
|
||||
}
|
||||
}
|
||||
|
||||
void UIWindow::set_content(std::unique_ptr<IWindowContent> content) {
|
||||
_content = std::move(content);
|
||||
}
|
||||
|
||||
void UIWindow::set_focused(bool focused) {
|
||||
_is_focused = focused;
|
||||
}
|
||||
|
||||
IWindowContent* UIWindow::get_content(void) {
|
||||
return _content.get();
|
||||
}
|
||||
|
||||
bool UIWindow::is_point_inside(int x, int y) {
|
||||
return (x >= _x && x <= _x + _width &&
|
||||
y >= _y && y <= _y + _height);
|
||||
}
|
||||
|
||||
void UIWindow::render(const RenderContext& context) {
|
||||
int title_bar_height = 30;
|
||||
|
||||
context.ui_renderer->begin_shapes();
|
||||
context.ui_renderer->begin_text();
|
||||
|
||||
/* Define colours. */
|
||||
const Color frame_color = { 0.2f, 0.2f, 0.25f };
|
||||
const Color title_bar_color = { 0.15f, 0.15f, 0.2f };
|
||||
const Color focused_title_bar_color = { 0.3f, 0.3f, 0.4f };
|
||||
const Color title_text_color = { 0.9f, 0.9f, 0.9f };
|
||||
const Color resize_handle_color = { 0.9f, 0.9f, 0.9f };
|
||||
|
||||
/* Draw main window frame/background. */
|
||||
context.ui_renderer->draw_rect(_x, _y, _width, _height, frame_color);
|
||||
|
||||
/* Draw title bar. */
|
||||
if(_is_focused) {
|
||||
context.ui_renderer->draw_rect(_x, _y, _width, title_bar_height, focused_title_bar_color);
|
||||
} else {
|
||||
context.ui_renderer->draw_rect(_x, _y, _width, title_bar_height, title_bar_color);
|
||||
}
|
||||
|
||||
/* Draw title text. */
|
||||
context.ui_renderer->render_text(_title.c_str(), _x+5, _y+20, title_text_color);
|
||||
|
||||
/* Draw title bar buttons. */
|
||||
int button_size = 20;
|
||||
int button_margin = 5;
|
||||
int x_offset = _x + _width - button_size - button_margin;
|
||||
for(const auto& button : _title_bar_buttons) {
|
||||
context.ui_renderer->draw_rect(x_offset, _y+button_margin, button_size,
|
||||
button_size, button.color);
|
||||
x_offset -= (button_size + button_margin);
|
||||
}
|
||||
|
||||
/* Draw Resize handle. */
|
||||
int corner_x = _x + _width;
|
||||
int corner_y = _y + _height;
|
||||
context.ui_renderer->draw_triangle(corner_x, corner_y - 10, corner_x - 10,
|
||||
corner_y, corner_x, corner_y, resize_handle_color);
|
||||
|
||||
/* Flush the shapes and text for the window frame and title bar. */
|
||||
context.ui_renderer->flush_shapes();
|
||||
context.ui_renderer->flush_text();
|
||||
|
||||
if(_content) {
|
||||
int content_screen_y = _y + title_bar_height;
|
||||
int content_height = _height - title_bar_height;
|
||||
int content_width = _width;
|
||||
/* Got to pass GL y-coord for scissor box to work correctly. */
|
||||
int content_gl_y = context.screen_height - content_screen_y - content_height;
|
||||
_content->render(context, _x, content_screen_y, content_gl_y, content_width, content_height);
|
||||
}
|
||||
}
|
||||
|
||||
void UIWindow::handle_event(SDL_Event* event, int screen_width,
|
||||
int screen_height, int taskbar_height) {
|
||||
int title_bar_height = 30;
|
||||
|
||||
if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
int mouse_x = event->button.x;
|
||||
int mouse_y = event->button.y;
|
||||
|
||||
/* Check for title bar button clicks. */
|
||||
int button_size = 20;
|
||||
int button_margin = 5;
|
||||
int x_offset = _x + _width - button_size - button_margin;
|
||||
for(const auto& button : _title_bar_buttons) {
|
||||
if(mouse_x >= x_offset && mouse_x <= x_offset + button_size &&
|
||||
mouse_y >= _y + button_margin && mouse_y <= _y + button_margin + button_size) {
|
||||
switch(button.action) {
|
||||
case WindowButtonAction::CLOSE:
|
||||
_should_close = true;
|
||||
break;
|
||||
case WindowButtonAction::MAXIMIZE:
|
||||
if(_state == WindowState::MAXIMIZED) {
|
||||
_x = _pre_maximize_rect.x;
|
||||
_y = _pre_maximize_rect.y;
|
||||
_width = _pre_maximize_rect.w;
|
||||
_height = _pre_maximize_rect.h;
|
||||
_state = WindowState::NORMAL;
|
||||
} else {
|
||||
_pre_maximize_rect = { _x, _y, _width, _height };
|
||||
_x = 0; _y = 0;
|
||||
_width = screen_width; _height = screen_height - taskbar_height;
|
||||
_state = WindowState::MAXIMIZED;
|
||||
}
|
||||
break;
|
||||
case WindowButtonAction::MINIMIZE:
|
||||
minimize();
|
||||
break;
|
||||
}
|
||||
return; /* Button clicked, no need to process further. */
|
||||
}
|
||||
x_offset -= (button_size + button_margin);
|
||||
}
|
||||
|
||||
/* Check for resize handle click (bottom-right corner). */
|
||||
if(is_mouse_over_resize_handle(mouse_x, mouse_y) &&
|
||||
_state != WindowState::MAXIMIZED) {
|
||||
_is_resizing = true;
|
||||
} else if(mouse_x >= _x && mouse_x <= _x + _width &&
|
||||
mouse_y >= _y && mouse_y <= _y + title_bar_height &&
|
||||
_state != WindowState::MAXIMIZED) {
|
||||
/* Is click within title bar? */
|
||||
_is_dragging = true;
|
||||
_drag_offset_x = mouse_x - _x;
|
||||
_drag_offset_y = mouse_y - _y;
|
||||
}
|
||||
} else if(event->type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
_is_dragging = false;
|
||||
_is_resizing = false;
|
||||
} else if(event->type == SDL_EVENT_MOUSE_MOTION) {
|
||||
if(_is_dragging) {
|
||||
_x = event->motion.x - _drag_offset_x;
|
||||
_y = event->motion.y - _drag_offset_y;
|
||||
} else if(_is_resizing) {
|
||||
int new_width = event->motion.x - _x;
|
||||
int new_height = event->motion.y - _y;
|
||||
_width = (new_width > 100) ? new_width : 100; /* Min width. */
|
||||
_height = (new_height > 80) ? new_height : 80; /* Min height. */
|
||||
}
|
||||
|
||||
/* Check if mouse hovered over window. */
|
||||
_is_hovered = is_point_inside(event->motion.x, event->motion.y);
|
||||
} else if(event->type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
/* Only scroll window if mouse is hovering. */
|
||||
if(_is_hovered && _content) {
|
||||
/* SDL's wheel motion is negative scroll up on the y. */
|
||||
int content_height_gl = _height - title_bar_height;
|
||||
_content->scroll(-event->wheel.y, content_height_gl);
|
||||
}
|
||||
}
|
||||
if(_content) {
|
||||
int y_gl = screen_height - _y - _height;
|
||||
int content_height = _height - title_bar_height;
|
||||
int content_width = _width;
|
||||
_content->handle_input(event, _x, _y, y_gl, content_width, content_height);
|
||||
}
|
||||
}
|
||||
|
||||
71
client/src/ui/ui_window.h
Normal file
71
client/src/ui/ui_window.h
Normal file
@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gfx/types.h"
|
||||
#include "i_window_content.h"
|
||||
|
||||
enum class WindowState {
|
||||
NORMAL,
|
||||
MINIMIZED,
|
||||
MAXIMIZED
|
||||
};
|
||||
|
||||
enum class WindowButtonAction {
|
||||
CLOSE,
|
||||
MAXIMIZE,
|
||||
MINIMIZE
|
||||
};
|
||||
|
||||
struct TitleBarButton {
|
||||
WindowButtonAction action;
|
||||
Color color;
|
||||
};
|
||||
|
||||
class UIWindow {
|
||||
public:
|
||||
UIWindow(const char* title, int x, int y, int width, int height);
|
||||
~UIWindow(void);
|
||||
|
||||
void update(void);
|
||||
void render(const RenderContext& context);
|
||||
void handle_event(SDL_Event* event, int screen_width, int screen_height,
|
||||
int taskbar_height);
|
||||
void minimize(void);
|
||||
void restore(void);
|
||||
void close(void) { _should_close = true; }
|
||||
bool is_minimized(void) const;
|
||||
bool should_close(void) const;
|
||||
void set_focused(bool focused);
|
||||
bool is_point_inside(int x, int y);
|
||||
void set_content(std::unique_ptr<IWindowContent> content);
|
||||
IWindowContent* get_content(void);
|
||||
bool is_mouse_over_resize_handle(int mouse_x, int mouse_y) const;
|
||||
const std::string& get_title(void) const;
|
||||
Rect get_rect(void) const;
|
||||
void set_session_id(uint32_t id) { _session_id = id; }
|
||||
uint32_t get_session_id(void) const { return _session_id; }
|
||||
|
||||
private:
|
||||
friend class Taskbar; /* Allow taskbar to access private members. */
|
||||
int _x, _y, _width, _height;
|
||||
Rect _pre_maximize_rect;
|
||||
std::string _title;
|
||||
std::unique_ptr<IWindowContent> _content;
|
||||
bool _is_focused; /* Managed by desktop. */
|
||||
bool _is_hovered; /* Send scroll events even if not focused. */
|
||||
bool _should_close;
|
||||
|
||||
std::vector<TitleBarButton> _title_bar_buttons;
|
||||
WindowState _state;
|
||||
|
||||
bool _is_dragging;
|
||||
int _drag_offset_x, _drag_offset_y;
|
||||
|
||||
bool _is_resizing;
|
||||
int _resize_margin;
|
||||
|
||||
uint32_t _session_id;
|
||||
};
|
||||
17
client/src/ui/window_action.h
Normal file
17
client/src/ui/window_action.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
enum class ActionType {
|
||||
NONE,
|
||||
WRITE_FILE,
|
||||
READ_FILE,
|
||||
BUILD_FILE,
|
||||
CLOSE_WINDOW
|
||||
};
|
||||
|
||||
struct WindowAction {
|
||||
ActionType type = ActionType::NONE;
|
||||
std::string payload1; /* i.e., filename. */
|
||||
std::string payload2; /* i.e., content. */
|
||||
};
|
||||
16
common/CMakeLists.txt
Normal file
16
common/CMakeLists.txt
Normal file
@ -0,0 +1,16 @@
|
||||
find_package(Lua 5.4 REQUIRED)
|
||||
|
||||
file(GLOB_RECURSE BETTOLA_SOURCES "src/*.cpp")
|
||||
|
||||
add_library(bettola
|
||||
${BETTOLA_SOURCES}
|
||||
)
|
||||
|
||||
target_link_libraries(bettola PUBLIC ${LUA_LIBRARIES} sol2 sqlite)
|
||||
|
||||
target_include_directories(bettola PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${LUA_INCLUDE_DIR}
|
||||
${asio_SOURCE_DIR}/asio/include
|
||||
${sqlite_modern_cpp_SOURCE_DIR}/hdr
|
||||
)
|
||||
|
||||
6
common/src/bettola.cpp
Normal file
6
common/src/bettola.cpp
Normal file
@ -0,0 +1,6 @@
|
||||
#include <cstdio>
|
||||
#include "bettola.h"
|
||||
|
||||
void bettola_function(void) {
|
||||
printf("Hello from libbettola!\n");
|
||||
}
|
||||
3
common/src/bettola.h
Normal file
3
common/src/bettola.h
Normal file
@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void bettola_function(void);
|
||||
89
common/src/db/database_manager.cpp
Normal file
89
common/src/db/database_manager.cpp
Normal file
@ -0,0 +1,89 @@
|
||||
#include "database_manager.h"
|
||||
#include <memory>
|
||||
#include "vfs.h"
|
||||
|
||||
DatabaseManager::DatabaseManager(const std::string& db_path) :
|
||||
_db(db_path) {
|
||||
_player_repository = std::make_unique<PlayerRepository>(_db);
|
||||
_machine_repository = std::make_unique<MachineRepository>(_db);
|
||||
_service_repository = std::make_unique<ServiceRepository>(_db);
|
||||
_vfs_repository = std::make_unique<VFSRepository>(_db);
|
||||
|
||||
_db << "CREATE TABLE IF NOT EXISTS players("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"username TEXT NOT NULL UNIQUE,"
|
||||
"password TEXT NOT NULL,"
|
||||
"hostname TEXT NOT NULL,"
|
||||
"home_machine_id INTEGER"
|
||||
");";
|
||||
|
||||
_db << "CREATE TABLE IF NOT EXISTS machines ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"owner_id INTEGER,"
|
||||
"hostname TEXT NOT NULL,"
|
||||
"ip_address TEXT NOT NULL UNIQUE"
|
||||
");";
|
||||
|
||||
_db << "CREATE TABLE IF NOT EXISTS vfs_nodes ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"machine_id INTEGER NOT NULL,"
|
||||
"parent_id INTEGER,"
|
||||
"name TEXT NOT NULL,"
|
||||
"type INTEGER NOT NULL,"
|
||||
"content TEXT"
|
||||
");";
|
||||
_db << "CREATE TABLE IF NOT EXISTS services ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"machine_id INTEGER NOT NULL,"
|
||||
"port INTEGER NOT NULL,"
|
||||
"name TEXT NOT NULL"
|
||||
");";
|
||||
}
|
||||
|
||||
DatabaseManager::~DatabaseManager(void) {
|
||||
/* db is auto closed when _db goes out of scope. */
|
||||
}
|
||||
|
||||
bool DatabaseManager::create_player(const std::string& username, const std::string& password,
|
||||
const std::string& hostname, vfs_node* vfs_template) {
|
||||
long long player_id = 0;
|
||||
long long machine_id = 0;
|
||||
|
||||
try {
|
||||
_db << "BEGIN;";
|
||||
|
||||
player_id = _player_repository->create(username, password, hostname);
|
||||
|
||||
/* Create the home machine. */
|
||||
/* TODO: Implement real IP allication. */
|
||||
std::string ip_address = "192.168.1." + std::to_string(player_id);
|
||||
machine_id = _machine_repository->create(player_id, hostname, ip_address);
|
||||
|
||||
_player_repository->set_home_machine_id(player_id, machine_id);
|
||||
|
||||
/* Create the root dir for the new machine's VFS. */
|
||||
long long root_id = _vfs_repository->create_node(machine_id, nullptr, "/", DIR_NODE);
|
||||
|
||||
/* Create default subdirs. */
|
||||
_vfs_repository->create_node(machine_id, &root_id, "home", DIR_NODE);
|
||||
_vfs_repository->create_node(machine_id, &root_id, "etc", DIR_NODE);
|
||||
|
||||
/* Create /bin and get it's ID */
|
||||
long long bin_id = _vfs_repository->create_node(machine_id, &root_id, "bin", DIR_NODE);
|
||||
|
||||
/* Copy scripts from template into new machine's /bin */
|
||||
vfs_node* template_bin = vfs_template->children["bin"];
|
||||
for(auto const& [name, node] : template_bin->children) {
|
||||
_vfs_repository->create_node(machine_id, &bin_id, name, node->type, node->content);
|
||||
}
|
||||
|
||||
/* Add default SSH service. */
|
||||
_service_repository->create(machine_id, 22, "SSH");
|
||||
|
||||
_db << "COMMIT";
|
||||
} catch(const std::exception& e) {
|
||||
_db << "ROLLBACK;"; /* Ensure atomicity. */
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
34
common/src/db/database_manager.h
Normal file
34
common/src/db/database_manager.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
#include "service_repository.h"
|
||||
#include "player_repository.h"
|
||||
#include "machine_repository.h"
|
||||
#include "vfs_repository.h"
|
||||
#include "sqlite_modern_cpp.h"
|
||||
#include "vfs.h"
|
||||
|
||||
class DatabaseManager {
|
||||
public:
|
||||
DatabaseManager(const std::string& db_path);
|
||||
~DatabaseManager(void);
|
||||
|
||||
/* Return true on success, false if user already exists. */
|
||||
bool create_player(const std::string& username, const std::string& password,
|
||||
const std::string& hostname, vfs_node* vfs_template);
|
||||
|
||||
PlayerRepository& players(void) { return *_player_repository; }
|
||||
MachineRepository& machines(void) { return *_machine_repository; }
|
||||
ServiceRepository& services(void) { return *_service_repository; }
|
||||
VFSRepository& vfs(void) { return *_vfs_repository; }
|
||||
|
||||
sqlite::database _db;
|
||||
|
||||
private:
|
||||
std::unique_ptr<PlayerRepository> _player_repository;
|
||||
std::unique_ptr<MachineRepository> _machine_repository;
|
||||
std::unique_ptr<ServiceRepository> _service_repository;
|
||||
std::unique_ptr<VFSRepository> _vfs_repository;
|
||||
};
|
||||
3
common/src/db/db.h
Normal file
3
common/src/db/db.h
Normal file
@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <sqlite_modern_cpp.h>
|
||||
49
common/src/db/machine_repository.cpp
Normal file
49
common/src/db/machine_repository.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
#include "machine_repository.h"
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
MachineRepository::MachineRepository(sqlite::database& db) : _db(db) {}
|
||||
|
||||
long long MachineRepository::create(std::optional<long long> owner_id,
|
||||
const std::string& hostname,
|
||||
const std::string& ip_address) {
|
||||
if(owner_id.has_value()) {
|
||||
_db << "INSERT INTO machines (owner_id, hostname, ip_address) VALUES(?, ?, ?);"
|
||||
<< owner_id.value() << hostname << ip_address;
|
||||
} else {
|
||||
_db << "INSERT INTO machines (owner_id, hostname, ip_address) VALUES (NULL, ?, ?);"
|
||||
<< hostname << ip_address;
|
||||
}
|
||||
return _db.last_insert_rowid();
|
||||
}
|
||||
|
||||
int MachineRepository::get_npc_count(void) {
|
||||
int count = 0;
|
||||
_db << "SELECT count(*) FROM machines WHERE owner_id IS NULL;" >> count;
|
||||
return count;
|
||||
}
|
||||
|
||||
std::vector<MachineData> MachineRepository::get_all_npcs(void) {
|
||||
std::vector<MachineData> machines;
|
||||
_db << "SELECT id, ip_address FROM machines WHERE owner_id IS NULL;"
|
||||
>> [&](long long id, std::string ip_address) {
|
||||
machines.push_back({id, ip_address});
|
||||
};
|
||||
return machines;
|
||||
}
|
||||
|
||||
std::vector<MachineData> MachineRepository::get_all(void) {
|
||||
std::vector<MachineData> machines;
|
||||
_db << "SELECT id, ip_address FROM machines;"
|
||||
>> [&](long long id, std::string ip_address) {
|
||||
machines.push_back({id, ip_address});
|
||||
};
|
||||
return machines;
|
||||
}
|
||||
|
||||
std::string MachineRepository::get_hostname(long long machine_id) {
|
||||
std::string hostname;
|
||||
_db << "SELECT hostname FROM machines WHERE id = ?;"
|
||||
<< machine_id
|
||||
>> hostname;
|
||||
return hostname;
|
||||
}
|
||||
28
common/src/db/machine_repository.h
Normal file
28
common/src/db/machine_repository.h
Normal file
@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
/* Struct to hold machine data. */
|
||||
struct MachineData {
|
||||
long long id;
|
||||
std::string ip_address;
|
||||
};
|
||||
|
||||
class MachineRepository {
|
||||
public:
|
||||
MachineRepository(sqlite::database& db);
|
||||
|
||||
long long create(std::optional<long long> owner_id, const std::string& hostname,
|
||||
const std::string& ip_address);
|
||||
int get_npc_count(void);
|
||||
std::vector<MachineData> get_all_npcs(void);
|
||||
std::vector<MachineData> get_all(void);
|
||||
std::string get_hostname(long long machine_id);
|
||||
|
||||
private:
|
||||
sqlite::database& _db;
|
||||
};
|
||||
|
||||
34
common/src/db/player_repository.cpp
Normal file
34
common/src/db/player_repository.cpp
Normal file
@ -0,0 +1,34 @@
|
||||
#include "player_repository.h"
|
||||
|
||||
PlayerRepository::PlayerRepository(sqlite::database& db) : _db(db) {}
|
||||
|
||||
long long PlayerRepository::create(const std::string& username, const std::string& password,
|
||||
const std::string& hostname) {
|
||||
_db << "INSERT INTO players (username, password, hostname) VALUES (?, ?, ?);"
|
||||
<< username
|
||||
<< password
|
||||
<< hostname;
|
||||
return _db.last_insert_rowid();
|
||||
}
|
||||
|
||||
bool PlayerRepository::authenticate(const std::string& username, const std::string& password) {
|
||||
bool authed = false;
|
||||
_db << "SELECT id FROM players WHERE username = ? AND password = ?;"
|
||||
<< username
|
||||
<< password
|
||||
>> [&](long long id) {authed = true;};
|
||||
return authed;
|
||||
}
|
||||
|
||||
long long PlayerRepository::get_home_machine_id(const std::string& username) {
|
||||
long long machine_id = -1;
|
||||
_db << "SELECT home_machine_id FROM players WHERE username = ?;"
|
||||
<< username
|
||||
>> machine_id;
|
||||
return machine_id;
|
||||
}
|
||||
|
||||
void PlayerRepository::set_home_machine_id(long long player_id, long long machine_id) {
|
||||
_db << "UPDATE players SET home_machine_id = ? WHERE id = ?;"
|
||||
<< machine_id << player_id;
|
||||
}
|
||||
19
common/src/db/player_repository.h
Normal file
19
common/src/db/player_repository.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
class PlayerRepository {
|
||||
public:
|
||||
PlayerRepository(sqlite::database& db);
|
||||
|
||||
long long create(const std::string& username, const std::string& password,
|
||||
const std::string& hostname);
|
||||
bool authenticate(const std::string& username, const std::string& password);
|
||||
long long get_home_machine_id(const std::string& username);
|
||||
void set_home_machine_id(long long player_id, long long machine_id);
|
||||
|
||||
private:
|
||||
sqlite::database& _db;
|
||||
};
|
||||
19
common/src/db/service_repository.cpp
Normal file
19
common/src/db/service_repository.cpp
Normal file
@ -0,0 +1,19 @@
|
||||
#include "service_repository.h"
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
ServiceRepository::ServiceRepository(sqlite::database& db) : _db(db) {}
|
||||
|
||||
void ServiceRepository::create(long long machine_id, int port, const std::string& name) {
|
||||
_db << "INSERT INTO services (machine_id, port, name) VALUES (?, ?, ?);"
|
||||
<< machine_id << port << name;
|
||||
}
|
||||
|
||||
std::map<int, std::string> ServiceRepository::get_for_machine(long long machine_id) {
|
||||
std::map<int, std::string> services;
|
||||
_db << "SELECT port, name FROM services WHERE machine_id = ?;"
|
||||
<< machine_id
|
||||
>> [&](int port, std::string name) {
|
||||
services[port] = name;
|
||||
};
|
||||
return services;
|
||||
}
|
||||
17
common/src/db/service_repository.h
Normal file
17
common/src/db/service_repository.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
class ServiceRepository {
|
||||
public:
|
||||
ServiceRepository(sqlite::database& db);
|
||||
|
||||
void create(long long machine_id, int port, const std::string& name);
|
||||
std::map<int, std::string> get_for_machine(long long machine_id);
|
||||
|
||||
private:
|
||||
sqlite::database& _db;
|
||||
};
|
||||
35
common/src/db/vfs_repository.cpp
Normal file
35
common/src/db/vfs_repository.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
#include "vfs_repository.h"
|
||||
|
||||
VFSRepository::VFSRepository(sqlite::database& db) : _db(db) {}
|
||||
|
||||
long long VFSRepository::create_node(long long machine_id, long long* parent_id,
|
||||
const std::string& name, vfs_node_type type,
|
||||
const std::string& content) {
|
||||
if(parent_id) {
|
||||
_db << "INSERT INTO vfs_nodes (machine_id, parent_id, name, type, content) "
|
||||
"VALUES (?, ?, ?, ?, ?);"
|
||||
<< machine_id << *parent_id << name << type << content;
|
||||
} else {
|
||||
_db << "INSERT INTO vfs_nodes (machine_id, parent_id, name, type, content) "
|
||||
"VALUES (?, NULL, ?, ?, ?);"
|
||||
<< machine_id << name << type << content;
|
||||
}
|
||||
return _db.last_insert_rowid();
|
||||
}
|
||||
|
||||
std::vector<vfs_node*> VFSRepository::get_nodes_for_machine(long long machine_id) {
|
||||
std::vector<vfs_node*> nodes;
|
||||
_db << "SELECT id, parent_id, name, type, content FROM vfs_nodes WHERE machine_id = ?;"
|
||||
<< machine_id
|
||||
>> [&](long long id, long long parent_id, std::string name, int type, std::string content) {
|
||||
vfs_node* node = new vfs_node();
|
||||
node->id = id;
|
||||
node->parent_id = parent_id;
|
||||
node->name = name;
|
||||
node->type = (vfs_node_type) type;
|
||||
node->content = content;
|
||||
|
||||
nodes.push_back(node);
|
||||
};
|
||||
return nodes;
|
||||
}
|
||||
20
common/src/db/vfs_repository.h
Normal file
20
common/src/db/vfs_repository.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "vfs.h"
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
class VFSRepository {
|
||||
public:
|
||||
VFSRepository(sqlite::database& db);
|
||||
|
||||
long long create_node(long long machine_id, long long* parent_id,
|
||||
const std::string& name, vfs_node_type type,
|
||||
const std::string& content = "");
|
||||
std::vector<vfs_node*> get_nodes_for_machine(long long machine_id);
|
||||
|
||||
private:
|
||||
sqlite::database& _db;
|
||||
};
|
||||
11
common/src/i_network_bridge.h
Normal file
11
common/src/i_network_bridge.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
class Machine;
|
||||
|
||||
class INetworkBridge {
|
||||
public:
|
||||
virtual Machine* get_machine_by_ip(const std::string& ip) = 0;
|
||||
virtual void release_machine(long long machine_id) = 0;
|
||||
};
|
||||
306
common/src/lua_api.cpp
Normal file
306
common/src/lua_api.cpp
Normal file
@ -0,0 +1,306 @@
|
||||
#include <sol/call.hpp>
|
||||
#include <sol/types.hpp>
|
||||
#include <sstream>
|
||||
|
||||
#include "i_network_bridge.h"
|
||||
#include "lua_api.h"
|
||||
#include "session.h"
|
||||
#include "machine.h"
|
||||
#include "vfs.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
vfs_node* get_current_dir(Session& context) {
|
||||
return context.get_current_dir();
|
||||
}
|
||||
|
||||
std::string rm(Session& context, const std::string& filename) {
|
||||
vfs_node* current_dir = context.get_current_dir();
|
||||
auto it = current_dir->children.find(filename);
|
||||
|
||||
if(it == current_dir->children.end()) {
|
||||
return "rm: cannot remove '" + filename + "': No such file or directory.";
|
||||
}
|
||||
if(it->second->type == DIR_NODE) {
|
||||
return "rm: cannot remove '" + filename + "': Is a directory.";
|
||||
}
|
||||
|
||||
delete it->second; /* Free the memory for the node. */
|
||||
current_dir->children.erase(it); /* Remove from map. */
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string write_file(Session& context, const std::string& path,
|
||||
const std::string& content) {
|
||||
vfs_node* parent_dir = nullptr;
|
||||
std::string filename;
|
||||
|
||||
if(path[0] == '/') {
|
||||
Machine* session_machine = context.get_session_machine();
|
||||
vfs_node* root = session_machine->vfs_root;
|
||||
|
||||
size_t last_slash = path.find_last_of('/');
|
||||
if(last_slash == 0) {
|
||||
/* File in root. */
|
||||
parent_dir = root;
|
||||
filename = path.substr(1);
|
||||
} else {
|
||||
std::string parent_path = path.substr(0, last_slash);
|
||||
parent_dir = find_node_by_path(root, parent_path);
|
||||
filename = path.substr(last_slash+1);
|
||||
}
|
||||
} else {
|
||||
/* Relative path. */
|
||||
parent_dir = context.get_current_dir();
|
||||
filename = path;
|
||||
}
|
||||
|
||||
if(!parent_dir) {
|
||||
return "write: cannot create file '" + path + "': No such file or directory";
|
||||
}
|
||||
|
||||
if(parent_dir->type != DIR_NODE) {
|
||||
return "write: cannot create file in '" + get_full_path(parent_dir) + "': Not a directory";
|
||||
}
|
||||
|
||||
auto it = parent_dir->children.find(filename);
|
||||
if(it != parent_dir->children.end()) {
|
||||
if(it->second->type == DIR_NODE) {
|
||||
return "write: " + path + ": Is a directory";
|
||||
}
|
||||
it->second->content = content;
|
||||
} else {
|
||||
/* File does not exist, create it. */
|
||||
vfs_node* new_file = new_node(filename, FILE_NODE, parent_dir);
|
||||
new_file->content = content;
|
||||
parent_dir->children[filename] = new_file;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string create_executable(Session& context, const std::string& path,
|
||||
const std::string& content) {
|
||||
vfs_node* parent_dir = nullptr;
|
||||
std::string filename;
|
||||
|
||||
if(path[0] == '/') {
|
||||
Machine* session_machine = context.get_session_machine();
|
||||
vfs_node* root = session_machine->vfs_root;
|
||||
|
||||
size_t last_slash = path.find_last_of('/');
|
||||
if(last_slash == 0) {
|
||||
parent_dir = root;
|
||||
filename = path.substr(1);
|
||||
} else {
|
||||
std::string parent_path = path.substr(0, last_slash);
|
||||
parent_dir = find_node_by_path(root, parent_path);
|
||||
filename = path.substr(last_slash+1);
|
||||
}
|
||||
} else {
|
||||
parent_dir = context.get_current_dir();
|
||||
filename = path;
|
||||
}
|
||||
|
||||
if(!parent_dir) {
|
||||
return "create_executable: cannot create file '" + path + "': No such file or directory";
|
||||
}
|
||||
|
||||
if(parent_dir->type != DIR_NODE) {
|
||||
return "create_executable: cannot create file in '" + get_full_path(parent_dir)
|
||||
+ "': Not a directory";
|
||||
}
|
||||
|
||||
/* Overwrite if exists. */
|
||||
auto it = parent_dir->children.find(filename);
|
||||
if(it != parent_dir->children.end()) {
|
||||
delete it->second;
|
||||
parent_dir->children.erase(it);
|
||||
}
|
||||
vfs_node* new_exec = new_node(filename, EXEC_NODE, parent_dir);
|
||||
new_exec->content = util::xor_string(content);
|
||||
parent_dir->children[filename] = new_exec;
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string cd(Session& context, const std::string& path) {
|
||||
vfs_node* current_dir = context.get_current_dir();
|
||||
|
||||
if(path == "..") {
|
||||
if(current_dir->parent) {
|
||||
context.set_current_dir(current_dir->parent);
|
||||
}
|
||||
} else {
|
||||
auto it = current_dir->children.find(path);
|
||||
if(it != current_dir->children.end() && it->second->type == DIR_NODE) {
|
||||
context.set_current_dir(it->second);
|
||||
} else {
|
||||
return "cd: no such file or directory: " + path;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string ls(Session& context) {
|
||||
vfs_node* dir = context.get_current_dir();
|
||||
if(dir->type != DIR_NODE) {
|
||||
return "ls: not a directory";
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
for(auto const& [name, node] : dir->children) {
|
||||
ss << name;
|
||||
if(node->type == DIR_NODE) {
|
||||
ss << "/";
|
||||
}
|
||||
ss << " ";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string ssh(Session& context, const std::string& ip) {
|
||||
INetworkBridge* bridge = context.get_network_bridge();
|
||||
Machine* target_machine = bridge->get_machine_by_ip(ip);
|
||||
|
||||
if(target_machine) {
|
||||
context.set_session_machine(target_machine);
|
||||
return "Connected to " + ip;
|
||||
} else {
|
||||
return "ssh: Could not resolve hostname " + ip + ": Name or service not found";
|
||||
}
|
||||
}
|
||||
|
||||
std::string nmap(Session& context, const std::string& ip) {
|
||||
INetworkBridge* bridge = context.get_network_bridge();
|
||||
Machine* target_machine = bridge->get_machine_by_ip(ip);
|
||||
|
||||
if(!target_machine) {
|
||||
return "nmap: Could not resolve host: " + ip;
|
||||
}
|
||||
|
||||
/* Read services from the cached machine object, not the DB. */
|
||||
auto services = target_machine->services;
|
||||
|
||||
/* Release the machine from the cache if it's a remote machine. */
|
||||
if(target_machine != context.get_session_machine()) {
|
||||
bridge->release_machine(target_machine->id);
|
||||
}
|
||||
|
||||
if(services.empty()) {
|
||||
return "No open ports for " + ip;
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "Host: " << ip << "\n";
|
||||
ss << "PORT\tSTATE\tSERVICE\n";
|
||||
for(auto const& [port, service_name] : services) {
|
||||
ss << port << "/tcp\t" << "open\t" << service_name << "\n";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string disconnect(Session& context) {
|
||||
Machine* current_machine = context.get_session_machine();
|
||||
if(current_machine != context.get_home_machine()) {
|
||||
INetworkBridge* bridge = context.get_network_bridge();
|
||||
bridge->release_machine(current_machine->id);
|
||||
}
|
||||
context.set_session_machine(context.get_home_machine());
|
||||
return "Connection closed.";
|
||||
}
|
||||
|
||||
std::string close_terminal(Session& context) {
|
||||
return "__CLOSE_CONNECTION__";
|
||||
}
|
||||
|
||||
struct ScpPath {
|
||||
std::string host;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
ScpPath parse_scp_path(const std::string& arg) {
|
||||
size_t colon_pos = arg.find(':');
|
||||
if(colon_pos != std::string::npos) {
|
||||
return {arg.substr(0, colon_pos), arg.substr(colon_pos+1)};
|
||||
} else {
|
||||
return {"", arg};
|
||||
}
|
||||
}
|
||||
|
||||
std::string scp(Session& context, const std::string& source_arg,
|
||||
const std::string& dest_arg) {
|
||||
ScpPath source_path = parse_scp_path(source_arg);
|
||||
ScpPath dest_path = parse_scp_path(dest_arg);
|
||||
Machine* session_machine = context.get_session_machine();
|
||||
INetworkBridge* bridge = context.get_network_bridge();
|
||||
|
||||
Machine* source_machine = source_path.host.empty()
|
||||
? session_machine : bridge->get_machine_by_ip(source_path.host);
|
||||
|
||||
Machine* dest_machine = dest_path.host.empty()
|
||||
? session_machine : bridge->get_machine_by_ip(dest_path.host);
|
||||
|
||||
if(!source_machine) {
|
||||
return "scp: " + source_path.host + ": Name or service not known";
|
||||
}
|
||||
if(!dest_machine) {
|
||||
if(source_machine != session_machine) bridge->release_machine(source_machine->id);
|
||||
return "scp: " + dest_path.host + ": Name or service not known";
|
||||
}
|
||||
|
||||
/* Simplified to use only absolute paths for now. */
|
||||
vfs_node* source_node = find_node_by_path(source_machine->vfs_root, source_path.path);
|
||||
|
||||
if(!source_node || source_node->type != FILE_NODE) {
|
||||
if(source_machine != session_machine) bridge->release_machine(source_machine->id);
|
||||
if(dest_machine != session_machine) bridge->release_machine(dest_machine->id);
|
||||
return "scp: " + source_path.path + ": No such file or directory";
|
||||
}
|
||||
|
||||
vfs_node* dest_node = find_node_by_path(dest_machine->vfs_root, dest_path.path);
|
||||
vfs_node* dest_dir = nullptr;
|
||||
std::string dest_filename;
|
||||
|
||||
if(dest_node && dest_node->type == DIR_NODE) {
|
||||
dest_dir = dest_node;
|
||||
dest_filename = source_node->name;
|
||||
} else {
|
||||
size_t last_slash = dest_path.path.find_last_of('/');
|
||||
if(last_slash != std::string::npos) {
|
||||
std::string parent_path = dest_path.path.substr(0, last_slash);
|
||||
if(parent_path.empty()) parent_path = "/";
|
||||
dest_dir = find_node_by_path(dest_machine->vfs_root, parent_path);
|
||||
dest_filename = dest_path.path.substr(last_slash+1);
|
||||
} else {
|
||||
dest_dir = dest_machine->vfs_root;
|
||||
dest_filename = dest_path.path;
|
||||
}
|
||||
}
|
||||
|
||||
if(!dest_dir) {
|
||||
if(source_machine != session_machine) bridge->release_machine(source_machine->id);
|
||||
if(dest_machine != session_machine) bridge->release_machine(dest_machine->id);
|
||||
return "scp: " + dest_path.path + ": No such file or directory";
|
||||
}
|
||||
|
||||
if(dest_dir->children.count(dest_filename)) {
|
||||
vfs_node* existing_node = dest_dir->children[dest_filename];
|
||||
if(existing_node->type == DIR_NODE) {
|
||||
if(source_machine != session_machine) bridge->release_machine(source_machine->id);
|
||||
if(dest_machine != session_machine) bridge->release_machine(dest_machine->id);
|
||||
return "scp: " + dest_path.path + ": Is a directory";
|
||||
}
|
||||
existing_node->content = source_node->content;
|
||||
} else {
|
||||
vfs_node* new_file = new_node(dest_filename, FILE_NODE, dest_dir);
|
||||
new_file->content = source_node->content;
|
||||
dest_dir->children[dest_filename] = new_file;
|
||||
}
|
||||
|
||||
if(source_machine != session_machine) bridge->release_machine(source_machine->id);
|
||||
if(dest_machine != session_machine) bridge->release_machine(dest_machine->id);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
} /* namespace api */
|
||||
31
common/src/lua_api.h
Normal file
31
common/src/lua_api.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "vfs.h"
|
||||
|
||||
class Session;
|
||||
class NetworkManager;
|
||||
|
||||
namespace api {
|
||||
|
||||
/* FILESYSTEM ACTIONS. */
|
||||
vfs_node* get_current_dir(Session& context);
|
||||
std::string rm(Session& context, const std::string& filename);
|
||||
std::string write_file(Session& context, const std::string& path,
|
||||
const std::string& content);
|
||||
std::string create_executable(Session& context, const std::string& path,
|
||||
const std::string& content);
|
||||
std::string ls(Session& context);
|
||||
std::string cd(Session& context, const std::string& path);
|
||||
std::string scp(Session& context, const std::string& source,
|
||||
const std::string& destination);
|
||||
|
||||
/* NETWORK ACTIONS. */
|
||||
std::string ssh(Session& context, const std::string& ip);
|
||||
std::string nmap(Session& context, const std::string& ip);
|
||||
std::string disconnect(Session& context);
|
||||
|
||||
/* SYSTEM ACTIONS. */
|
||||
std::string close_terminal(Session& context);
|
||||
|
||||
} /* namespace api */
|
||||
81
common/src/lua_processor.cpp
Normal file
81
common/src/lua_processor.cpp
Normal file
@ -0,0 +1,81 @@
|
||||
#include <sol/forward.hpp>
|
||||
#include <sol/object.hpp>
|
||||
#include <sol/protected_function_result.hpp>
|
||||
#include <sol/raii.hpp>
|
||||
|
||||
#include "lua_processor.h"
|
||||
#include "lua_api.h"
|
||||
#include "session.h"
|
||||
#include "vfs.h"
|
||||
|
||||
LuaProcessor::LuaProcessor(Session& context) {
|
||||
_lua.open_libraries(sol::lib::base, sol::lib::string, sol::lib::table);
|
||||
|
||||
/* Remove some dangerous functions from the base lib. */
|
||||
_lua["dofile"] = sol::nil;
|
||||
_lua["loadfile"] = sol::nil;
|
||||
_lua["load"] = sol::nil;
|
||||
_lua["pcall"] = sol::nil;
|
||||
_lua["xpcall"] = sol::nil;
|
||||
_lua["collectgarbage"] = sol::nil;
|
||||
_lua["getmetatable"] = sol::nil;
|
||||
_lua["setmetatable"] = sol::nil;
|
||||
_lua["rawequal"] = sol::nil;
|
||||
_lua["rawget"] = sol::nil;
|
||||
_lua["rawset"] = sol::nil;
|
||||
_lua["rawlen"] = sol::nil;
|
||||
|
||||
/* Expose vfs_node struct members to Lua. */
|
||||
_lua.new_usertype<vfs_node>("vfs_node",
|
||||
"name", &vfs_node::name,
|
||||
"type", &vfs_node::type,
|
||||
"children", &vfs_node::children,
|
||||
"content", &vfs_node::content);
|
||||
|
||||
/* Expose CommandProcessor to Lua. DON'T ALLOW SCRIPTS TO CREATE IT THOUGH! */
|
||||
_lua.new_usertype<Session>("Session", sol::no_constructor);
|
||||
|
||||
/* Create the 'bettola' API table. */
|
||||
sol::table bettola_api = _lua.create_named_table("bettola");
|
||||
bettola_api.set_function("rm", &api::rm);
|
||||
bettola_api.set_function("ls", &api::ls);
|
||||
bettola_api.set_function("write_file", &api::write_file);
|
||||
bettola_api.set_function("create_executable", &api::create_executable);
|
||||
bettola_api.set_function("get_current_dir", &api::get_current_dir);
|
||||
bettola_api.set_function("cd", &api::cd);
|
||||
bettola_api.set_function("scp", &api::scp);
|
||||
bettola_api.set_function("close_terminal", &api::close_terminal);
|
||||
bettola_api.set_function("ssh", &api::ssh);
|
||||
bettola_api.set_function("nmap", &api::nmap);
|
||||
bettola_api.set_function("disconnect", &api::disconnect);
|
||||
}
|
||||
|
||||
LuaProcessor::~LuaProcessor(void) {}
|
||||
|
||||
sol::object LuaProcessor::execute(const std::string& script, Session& context,
|
||||
const std::vector<std::string>& args, bool is_remote) {
|
||||
try {
|
||||
/* Pass C++ objects/points into the Lua env. */
|
||||
_lua["is_remote_session"] = is_remote;
|
||||
_lua["context"] = &context;
|
||||
|
||||
/* Create and populate the 'arg' table for the script. */
|
||||
sol::table arg_table = _lua.create_table();
|
||||
for(size_t i = 0; i < args.size(); ++i) {
|
||||
arg_table[i+1] = args[i]; /* Lua arrays 1-indexed. */
|
||||
}
|
||||
_lua["arg"] = arg_table;
|
||||
|
||||
sol::protected_function_result result = _lua.safe_script(script);
|
||||
if(result.valid()) {
|
||||
return result;
|
||||
} else {
|
||||
sol::error err = result;
|
||||
return sol::make_object(_lua, err.what());
|
||||
}
|
||||
|
||||
} catch(const sol::error& e) {
|
||||
/* Return the error message as a string in a sol::object. */
|
||||
return sol::make_object(_lua, e.what());
|
||||
}
|
||||
}
|
||||
20
common/src/lua_processor.h
Normal file
20
common/src/lua_processor.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <sol/sol.hpp>
|
||||
#include <vector>
|
||||
#include <vfs.h>
|
||||
#include <string>
|
||||
|
||||
class Session;
|
||||
|
||||
class LuaProcessor {
|
||||
public:
|
||||
LuaProcessor(Session& context);
|
||||
~LuaProcessor(void);
|
||||
|
||||
/* Executes a string of lua code and returns result as a string. */
|
||||
sol::object execute(const std::string& script, Session& context,
|
||||
const std::vector<std::string>& args, bool is_remote);
|
||||
private:
|
||||
sol::state _lua;
|
||||
};
|
||||
6
common/src/machine.cpp
Normal file
6
common/src/machine.cpp
Normal file
@ -0,0 +1,6 @@
|
||||
#include "machine.h"
|
||||
#include "vfs.h"
|
||||
|
||||
Machine::~Machine(void) {
|
||||
delete_vfs_tree(vfs_root);
|
||||
}
|
||||
27
common/src/machine.h
Normal file
27
common/src/machine.h
Normal file
@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
|
||||
#include "vfs.h"
|
||||
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
Machine(uint32_t id, std::string hostname) :
|
||||
id(id),
|
||||
hostname(std::move(hostname)),
|
||||
vfs_root(nullptr),
|
||||
is_vfs_a_copy(false) {}
|
||||
|
||||
~Machine(void); /* Clean up VFS tree. */
|
||||
|
||||
uint32_t id;
|
||||
std::string hostname;
|
||||
vfs_node* vfs_root;
|
||||
std::map<int, std::string> services;
|
||||
bool is_vfs_a_copy; /* Flag for CoW mechanism. */
|
||||
|
||||
/* TODO: We'll add hardware and sh.t here. */
|
||||
};
|
||||
141
common/src/machine_manager.cpp
Normal file
141
common/src/machine_manager.cpp
Normal file
@ -0,0 +1,141 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <map>
|
||||
|
||||
#include "db/database_manager.h"
|
||||
#include "machine_manager.h"
|
||||
#include "machine.h"
|
||||
#include "vfs.h"
|
||||
#include "util.h"
|
||||
|
||||
vfs_node* copy_vfs_node(vfs_node* original, vfs_node* new_parent) {
|
||||
if(!original) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Create the new node and copy its properties. */
|
||||
vfs_node* new_copy = new_node(original->name, original->type, new_parent);
|
||||
new_copy->content = original->content;
|
||||
|
||||
/* Recursively copy all children. */
|
||||
for(auto const& [key, child_node] : original->children) {
|
||||
new_copy->children[key] = copy_vfs_node(child_node, new_copy);
|
||||
}
|
||||
|
||||
return new_copy;
|
||||
}
|
||||
|
||||
MachineManager::MachineManager(DatabaseManager* db_manager) :
|
||||
_db_manager(db_manager) {
|
||||
/* Create template VFS that holds shared, read-only directories. */
|
||||
_vfs_template_root = new_node("/", DIR_NODE, nullptr);
|
||||
vfs_node* bin = new_node("bin", DIR_NODE, _vfs_template_root);
|
||||
_vfs_template_root->children["bin"] = bin;
|
||||
|
||||
/* Load all scripts from assets/scripts/bin into the VFS. */
|
||||
const std::string path = "assets/scripts/bin";
|
||||
for(const auto & entry : std::filesystem::directory_iterator(path)) {
|
||||
if(entry.is_regular_file() && entry.path().extension() == ".lua") {
|
||||
std::ifstream t(entry.path());
|
||||
std::stringstream buffer;
|
||||
buffer << t.rdbuf();
|
||||
std::string filename_with_ext = entry.path().filename().string();
|
||||
std::string filename = filename_with_ext.substr(0, filename_with_ext.find_last_of('.'));
|
||||
vfs_node* script_node = new_node(filename, EXEC_NODE, bin);
|
||||
script_node->content = util::xor_string(buffer.str());
|
||||
bin->children[filename] = script_node;
|
||||
fprintf(stderr, "Loaded executable: /bin/%s\n", filename.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MachineManager::~MachineManager(void) {
|
||||
delete_vfs_tree(_vfs_template_root);
|
||||
}
|
||||
|
||||
Machine* MachineManager::create_machine(uint32_t id, const std::string& hostname,
|
||||
const std::string& system_type) {
|
||||
auto* new_machine = new Machine(id, hostname);
|
||||
|
||||
vfs_node* root = new_node("/", DIR_NODE, nullptr);
|
||||
|
||||
/* Create directories for this specific VFS. */
|
||||
vfs_node* home = new_node("home", DIR_NODE, root);
|
||||
vfs_node* user = new_node("user", DIR_NODE, home);
|
||||
home->children["user"] = user;
|
||||
vfs_node* readme = new_node("readme.txt", FILE_NODE, user);
|
||||
readme->content = "Welcome to your new virtual machine.";
|
||||
user->children["readme.txt"] = readme;
|
||||
|
||||
/* Link to the shared directories from the template. */
|
||||
root->children["bin"] = copy_vfs_node(_vfs_template_root->children["bin"], root);
|
||||
|
||||
/* Assign the VFS to the new machine. */
|
||||
new_machine->vfs_root = root;
|
||||
|
||||
if(system_type == "npc") {
|
||||
vfs_node* npc_file = new_node("npc_system.txt", FILE_NODE, root);
|
||||
npc_file->content = "This guy sucks nuts!";
|
||||
root->children["npc_system.txt"] = npc_file;
|
||||
|
||||
}
|
||||
|
||||
return new_machine;
|
||||
}
|
||||
|
||||
/* Recursively build the VFS tree from database nodes. */
|
||||
void build_tree(vfs_node* parent, const std::map<long long, vfs_node*>& nodes) {
|
||||
for(auto const& [id, node] : nodes) {
|
||||
/* Inefficient but safe. Would be better to group nodes by parent_id. */
|
||||
if(node->parent_id == parent->id) {
|
||||
parent->children[node->name] = node;
|
||||
node->parent = parent;
|
||||
if(node->type == DIR_NODE) {
|
||||
build_tree(node, nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Machine* MachineManager::load_machine(long long machine_id) {
|
||||
printf("DEBUG: load_machine called for machine_id: %lld\n", machine_id);
|
||||
|
||||
std::string hostname = _db_manager->machines().get_hostname(machine_id);
|
||||
|
||||
Machine* machine = new Machine(machine_id, hostname);
|
||||
|
||||
/* Load all VFS nodes for this machine from the database. */
|
||||
std::map<long long, vfs_node*> node_map;
|
||||
vfs_node* root = nullptr;
|
||||
|
||||
auto nodes = _db_manager->vfs().get_nodes_for_machine(machine_id);
|
||||
for(vfs_node* node : nodes) {
|
||||
node_map[node->id] = node;
|
||||
if(node->name == "/") {
|
||||
root = node;
|
||||
}
|
||||
}
|
||||
|
||||
machine->services = _db_manager->services().get_for_machine(machine_id);
|
||||
|
||||
if(root) {
|
||||
build_tree(root, node_map);
|
||||
machine->vfs_root = root;
|
||||
}
|
||||
return machine;
|
||||
}
|
||||
|
||||
long long MachineManager::get_machine_id_by_ip(const std::string& ip) {
|
||||
if(_ip_to_id_map.count(ip)) {
|
||||
return _ip_to_id_map[ip];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void MachineManager::init(void) {
|
||||
auto all_machines = _db_manager->machines().get_all();
|
||||
for(const auto& machine_data : all_machines) {
|
||||
_ip_to_id_map[machine_data.ip_address] = machine_data.id;
|
||||
}
|
||||
}
|
||||
36
common/src/machine_manager.h
Normal file
36
common/src/machine_manager.h
Normal file
@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "db/database_manager.h"
|
||||
#include "machine.h"
|
||||
#include "vfs.h"
|
||||
|
||||
/* Recursive copy function for our Copy-on-Write behaviour. */
|
||||
vfs_node* copy_vfs_node(vfs_node* original, vfs_node* new_parent);
|
||||
|
||||
class MachineManager {
|
||||
public:
|
||||
MachineManager(DatabaseManager* db_manager);
|
||||
~MachineManager(void); /* TODO: Implement recursive VFS deletion. */
|
||||
|
||||
Machine* create_machine(uint32_t id, const std::string& hostname,
|
||||
const std::string& system_type);
|
||||
Machine* load_machine(long long machine_id);
|
||||
void init(void);
|
||||
|
||||
vfs_node* get_vfs_template(void) { return _vfs_template_root; }
|
||||
|
||||
/* Machine lookup. */
|
||||
long long get_machine_id_by_ip(const std::string& ip);
|
||||
|
||||
private:
|
||||
DatabaseManager* _db_manager;
|
||||
vfs_node* _vfs_template_root;
|
||||
|
||||
/* In memory cache of all machines, loaded or not. */
|
||||
std::map<std::string, long long> _ip_to_id_map;
|
||||
std::map<long long, Machine*> _world_machines;
|
||||
};
|
||||
|
||||
29
common/src/math/math.h
Normal file
29
common/src/math/math.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
namespace math {
|
||||
|
||||
inline void ortho_proj(float* mat, float left, float right, float bottom, float top,
|
||||
float near, float far) {
|
||||
|
||||
mat[0] = 2.0f / (right - left);
|
||||
mat[4] = 0.0f;
|
||||
mat[8] = 0.0f;
|
||||
mat[12] = -(right + left) / (right - left);
|
||||
|
||||
mat[1] = 0.0f;
|
||||
mat[5] = 2.0f / (top - bottom);
|
||||
mat[9] = 0.0f;
|
||||
mat[13] = -(top + bottom) / (top - bottom);
|
||||
|
||||
mat[2] = 0.0f;
|
||||
mat[6] = 0.0f;
|
||||
mat[10] = -2.0f / (far - near);
|
||||
mat[14] = -(far + near) / (far - near);
|
||||
|
||||
mat[3] = 0.0f;
|
||||
mat[7] = 0.0f;
|
||||
mat[11] = 0.0f;
|
||||
mat[15] = 1.0f;
|
||||
}
|
||||
|
||||
} /* namespace math */
|
||||
5
common/src/net/constants.h
Normal file
5
common/src/net/constants.h
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
const uint16_t MULTIPLAYER_PORT = 1337;
|
||||
const uint16_t SINGLE_PLAYER_PORT = 1338;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user