Compare commits
	
		
			No commits in common. "master" and "archive-3d-survival" have entirely different histories.
		
	
	
		
			master
			...
			archive-3d
		
	
		
							
								
								
									
										21
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,20 +1,3 @@
 | 
			
		||||
# 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
 | 
			
		||||
/bin
 | 
			
		||||
.clangd
 | 
			
		||||
*.swp
 | 
			
		||||
*.db
 | 
			
		||||
assets/design_doc.org
 | 
			
		||||
 | 
			
		||||
@ -1,55 +1,30 @@
 | 
			
		||||
cmake_minimum_required(VERSION 3.16)
 | 
			
		||||
project(bettola CXX C)
 | 
			
		||||
 | 
			
		||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 | 
			
		||||
project(bettola VERSION 0.1)
 | 
			
		||||
 | 
			
		||||
# Let's use C++17?
 | 
			
		||||
set(CMAKE_CXX_STANDARD 17)
 | 
			
		||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
 | 
			
		||||
 | 
			
		||||
# === Sol2 ===
 | 
			
		||||
include(FetchContent)
 | 
			
		||||
FetchContent_Declare(
 | 
			
		||||
  sol2
 | 
			
		||||
  GIT_REPOSITORY https://github.com/ThePhD/sol2.git
 | 
			
		||||
  GIT_TAG v3.3.1
 | 
			
		||||
)
 | 
			
		||||
FetchContent_MakeAvailable(sol2)
 | 
			
		||||
# include directories..
 | 
			
		||||
include_directories(libbettola/include)
 | 
			
		||||
include_directories(src)
 | 
			
		||||
 | 
			
		||||
# === Asio ===
 | 
			
		||||
FetchContent_Declare(
 | 
			
		||||
  asio
 | 
			
		||||
  GIT_REPOSITORY https://github.com/chriskohlhoff/asio
 | 
			
		||||
  git_TAG asio-1-36-0
 | 
			
		||||
)
 | 
			
		||||
FetchContent_MakeAvailable(asio)
 | 
			
		||||
# Deps.
 | 
			
		||||
find_package(SDL3   REQUIRED)
 | 
			
		||||
find_package(GLEW   REQUIRED)
 | 
			
		||||
find_package(OpenGL REQUIRED)
 | 
			
		||||
 | 
			
		||||
# === 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)
 | 
			
		||||
# Build our shared lib.
 | 
			
		||||
add_subdirectory(libbettola)
 | 
			
		||||
 | 
			
		||||
# === 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)
 | 
			
		||||
# Will need to clean build each time you add a new file though -.-
 | 
			
		||||
file(GLOB_RECURSE SOURCES "src/*.cpp")
 | 
			
		||||
add_executable(bettola ${SOURCES})
 | 
			
		||||
 | 
			
		||||
add_subdirectory(common)
 | 
			
		||||
add_subdirectory(client)
 | 
			
		||||
add_subdirectory(server)
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										62
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								Makefile
									
									
									
									
									
								
							@ -1,38 +1,46 @@
 | 
			
		||||
# Wrapper Makefile.
 | 
			
		||||
SHELL:=/bin/bash
 | 
			
		||||
BUILD_DIR:=bin
 | 
			
		||||
PROJECT_NAME:=bettola
 | 
			
		||||
EXECUTABLE:=$(BUILD_DIR)/$(PROJECT_NAME)
 | 
			
		||||
 | 
			
		||||
# Build artifacts.
 | 
			
		||||
BUILD_DIR := bin
 | 
			
		||||
CXX_COMPILER:=clang++
 | 
			
		||||
CMAKE_MAKEFILE:=$(BUILD_DIR)/Makefile
 | 
			
		||||
 | 
			
		||||
# Client path.
 | 
			
		||||
CLIENT_EXE := $(BUILD_DIR)/client/bettolac
 | 
			
		||||
# Server path.
 | 
			
		||||
SERVER_EXE := $(BUILD_DIR)/server/bettolas
 | 
			
		||||
.PHONY: all build config run clean help
 | 
			
		||||
 | 
			
		||||
.PHONY: all build config runc sp runs clean
 | 
			
		||||
 | 
			
		||||
# Default target when running 'make'.
 | 
			
		||||
# Default.
 | 
			
		||||
all: build
 | 
			
		||||
 | 
			
		||||
build: config
 | 
			
		||||
	@echo "=== Building Bettola. ==="
 | 
			
		||||
	@$(MAKE) -C $(BUILD_DIR)
 | 
			
		||||
# 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
 | 
			
		||||
 | 
			
		||||
config:
 | 
			
		||||
	@echo "=== Configuring Project. ==="
 | 
			
		||||
	@cmake -B $(BUILD_DIR) -S . -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
 | 
			
		||||
	@echo "==== Configuring Bettola with CMake ===="
 | 
			
		||||
	@mkdir -p $(BUILD_DIR)
 | 
			
		||||
	@cmake -S . -B$(BUILD_DIR) -DCMAKE_CXX_COMPILER=$(CXX_COMPILER) -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
# Build and run.
 | 
			
		||||
run: all
 | 
			
		||||
	@echo "==== Running Bettola ===="
 | 
			
		||||
	$(EXECUTABLE)
 | 
			
		||||
 | 
			
		||||
# Remove build dir.
 | 
			
		||||
clean:
 | 
			
		||||
	@echo "=== Cleaning Build Directory. ==="
 | 
			
		||||
	@echo "==== Cleaning Bettola ===="
 | 
			
		||||
	@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,44 +1,35 @@
 | 
			
		||||
#+TITLE: Working Title: Bettola
 | 
			
		||||
#+TITLE: Bettola Game
 | 
			
		||||
 | 
			
		||||
A multiplayer hacking simulator with a graphical OS, built in C++ and OpenGL.
 | 
			
		||||
A 2D RPG game and engine created with C++, SDL3, and OpenGL.
 | 
			
		||||
 | 
			
		||||
* Dependencies
 | 
			
		||||
The following dependencies are required:
 | 
			
		||||
The following dependencies are requird:
 | 
			
		||||
 - 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 most required dependencies with the following command:
 | 
			
		||||
You can install all required dependencies with the following command:
 | 
			
		||||
#+BEGIN_SRC bash
 | 
			
		||||
sudo apt update
 | 
			
		||||
sudo apt install build-essential clang cmake libsdl3-dev libglew-dev libfreetype-dev liblua5.4-dev
 | 
			
		||||
sudo apt install build-essential clang cmake libsdl3-dev libglew-dev
 | 
			
		||||
#+END_SRC
 | 
			
		||||
 | 
			
		||||
* Build Instructions
 | 
			
		||||
This project uses a top-level Makefile to simplify the CMake workflow. Build
 | 
			
		||||
artifacts will be placed in the =bin/= directory.
 | 
			
		||||
This project uses a top-level Makefile to simplify the CMake workflow. All build artifacts will be placed in the =bin/= directory.
 | 
			
		||||
 | 
			
		||||
Simply run the following commands from the root of the project directory:
 | 
			
		||||
Run the following 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 client:*
 | 
			
		||||
 - *Build and run the project:*
 | 
			
		||||
   #+BEGIN_SRC bash
 | 
			
		||||
   make runc
 | 
			
		||||
   #+END_SRC
 | 
			
		||||
 | 
			
		||||
 - *Build and run the server:*
 | 
			
		||||
   #+BEGIN_SRC bash
 | 
			
		||||
   make runs
 | 
			
		||||
   make run
 | 
			
		||||
   #+END_SRC
 | 
			
		||||
 | 
			
		||||
 - *Clean the project:*
 | 
			
		||||
@ -46,53 +37,3 @@ Simply run the following commands 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.
 | 
			
		||||
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
[  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...
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@ -1,34 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
-- /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)
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
-- /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
 | 
			
		||||
@ -1,7 +0,0 @@
 | 
			
		||||
-- /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)
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
-- /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
 | 
			
		||||
 | 
			
		||||
@ -1,6 +0,0 @@
 | 
			
		||||
-- /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
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
-- /bin/ls - Lists files in a directory.
 | 
			
		||||
--
 | 
			
		||||
-- Iterate over the 'children' map exposed via C++.
 | 
			
		||||
 | 
			
		||||
return bettola.ls(context)
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
-- /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)
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
local file_to_remove = arg[1]
 | 
			
		||||
if not file_to_remove then return "rm: missing operand" end
 | 
			
		||||
return bettola.rm(context, file_to_remove)
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
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)
 | 
			
		||||
@ -1,4 +0,0 @@
 | 
			
		||||
-- /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)
 | 
			
		||||
							
								
								
									
										85
									
								
								assets/shaders/cloud.frag
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								assets/shaders/cloud.frag
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								assets/shaders/cloud.vert
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								assets/shaders/cloud.vert
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
#version 330 core
 | 
			
		||||
out vec4 FragColor;
 | 
			
		||||
 | 
			
		||||
in vec3 ourColor;
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  FragColor = vec4(ourColor, 1.0);
 | 
			
		||||
}
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								assets/shaders/simple.frag
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								assets/shaders/simple.frag
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								assets/shaders/simple.vert
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								assets/shaders/simple.vert
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								assets/shaders/sky.frag
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								assets/shaders/sky.frag
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								assets/shaders/sky.vert
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								assets/shaders/sky.vert
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
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})
 | 
			
		||||
@ -1,101 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,52 +0,0 @@
 | 
			
		||||
#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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
#include "debug_stats.h"
 | 
			
		||||
 | 
			
		||||
int DebugStats::draw_calls      = 0;
 | 
			
		||||
int DebugStats::shape_vertices  = 0;
 | 
			
		||||
int DebugStats::text_vertices   = 0;
 | 
			
		||||
@ -1,14 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,371 +0,0 @@
 | 
			
		||||
#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));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,58 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
};
 | 
			
		||||
@ -1,86 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,16 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
};
 | 
			
		||||
@ -1,82 +0,0 @@
 | 
			
		||||
#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});
 | 
			
		||||
}
 | 
			
		||||
@ -1,33 +0,0 @@
 | 
			
		||||
#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];
 | 
			
		||||
};
 | 
			
		||||
@ -1,186 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,47 +0,0 @@
 | 
			
		||||
#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];
 | 
			
		||||
};
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,164 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,218 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
@ -1,47 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,58 +0,0 @@
 | 
			
		||||
#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();
 | 
			
		||||
}
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
#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. */
 | 
			
		||||
};
 | 
			
		||||
@ -1,39 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,324 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,114 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,47 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,76 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,195 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,43 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,148 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,52 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,112 +0,0 @@
 | 
			
		||||
#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();
 | 
			
		||||
}
 | 
			
		||||
@ -1,40 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,114 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,250 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
@ -1,49 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,64 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
}
 | 
			
		||||
@ -1,36 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,224 +0,0 @@
 | 
			
		||||
#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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,71 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
#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. */
 | 
			
		||||
};
 | 
			
		||||
@ -1,16 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -1,6 +0,0 @@
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include "bettola.h"
 | 
			
		||||
 | 
			
		||||
void bettola_function(void) {
 | 
			
		||||
  printf("Hello from libbettola!\n");
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
void bettola_function(void);
 | 
			
		||||
@ -1,89 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <sqlite_modern_cpp.h>
 | 
			
		||||
@ -1,49 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,28 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
}
 | 
			
		||||
@ -1,20 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,306 +0,0 @@
 | 
			
		||||
#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 */
 | 
			
		||||
@ -1,31 +0,0 @@
 | 
			
		||||
#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 */
 | 
			
		||||
@ -1,81 +0,0 @@
 | 
			
		||||
#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());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,20 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +0,0 @@
 | 
			
		||||
#include "machine.h"
 | 
			
		||||
#include "vfs.h"
 | 
			
		||||
 | 
			
		||||
Machine::~Machine(void) {
 | 
			
		||||
  delete_vfs_tree(vfs_root);
 | 
			
		||||
}
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
#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. */
 | 
			
		||||
};
 | 
			
		||||
@ -1,141 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,36 +0,0 @@
 | 
			
		||||
#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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,29 +0,0 @@
 | 
			
		||||
#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 */
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
#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