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
|
||||||
/bin/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# Editor/IDE, whatever people use.
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.suo
|
|
||||||
*.user
|
|
||||||
*.clangd/
|
|
||||||
compile_commands.json
|
|
||||||
|
|
||||||
# For those messy OS'es that like to put files everywhere.
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Misc.
|
|
||||||
assets/design_doc.org
|
|
||||||
.clangd
|
.clangd
|
||||||
*.swp
|
assets/design_doc.org
|
||||||
*.db
|
|
||||||
|
|||||||
@ -1,55 +1,30 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
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 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
# === Sol2 ===
|
# include directories..
|
||||||
include(FetchContent)
|
include_directories(libbettola/include)
|
||||||
FetchContent_Declare(
|
include_directories(src)
|
||||||
sol2
|
|
||||||
GIT_REPOSITORY https://github.com/ThePhD/sol2.git
|
|
||||||
GIT_TAG v3.3.1
|
|
||||||
)
|
|
||||||
FetchContent_MakeAvailable(sol2)
|
|
||||||
|
|
||||||
# === Asio ===
|
# Deps.
|
||||||
FetchContent_Declare(
|
find_package(SDL3 REQUIRED)
|
||||||
asio
|
find_package(GLEW REQUIRED)
|
||||||
GIT_REPOSITORY https://github.com/chriskohlhoff/asio
|
find_package(OpenGL REQUIRED)
|
||||||
git_TAG asio-1-36-0
|
|
||||||
)
|
|
||||||
FetchContent_MakeAvailable(asio)
|
|
||||||
|
|
||||||
# === SQLite ===
|
# Build our shared lib.
|
||||||
# Supress the developer warning for using Populate with declared content.
|
add_subdirectory(libbettola)
|
||||||
# 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)
|
|
||||||
|
|
||||||
# === sqlite_modern_cpp (SQLite wrapper) ===
|
# Will need to clean build each time you add a new file though -.-
|
||||||
FetchContent_Declare(
|
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||||
sqlite_modern_cpp
|
add_executable(bettola ${SOURCES})
|
||||||
GIT_REPOSITORY https://github.com/SqliteModernCpp/sqlite_modern_cpp.git
|
|
||||||
GIT_TAG v3.2
|
|
||||||
)
|
|
||||||
FetchContent_MakeAvailable(sqlite_modern_cpp)
|
|
||||||
|
|
||||||
add_subdirectory(common)
|
target_link_libraries(bettola PRIVATE bettola_lib SDL3::SDL3 GLEW::glew OpenGL::GL)
|
||||||
add_subdirectory(client)
|
|
||||||
add_subdirectory(server)
|
# 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.
|
CXX_COMPILER:=clang++
|
||||||
BUILD_DIR := bin
|
CMAKE_MAKEFILE:=$(BUILD_DIR)/Makefile
|
||||||
|
|
||||||
# Client path.
|
.PHONY: all build config run clean help
|
||||||
CLIENT_EXE := $(BUILD_DIR)/client/bettolac
|
|
||||||
# Server path.
|
|
||||||
SERVER_EXE := $(BUILD_DIR)/server/bettolas
|
|
||||||
|
|
||||||
.PHONY: all build config runc sp runs clean
|
# Default.
|
||||||
|
|
||||||
# Default target when running 'make'.
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
build: config
|
# Build project, if not configured, then do that first.
|
||||||
@echo "=== Building Bettola. ==="
|
build: $(CMAKE_MAKEFILE)
|
||||||
@$(MAKE) -C $(BUILD_DIR)
|
@echo "==== Building Bettola ===="
|
||||||
|
@cmake --build $(BUILD_DIR)
|
||||||
|
|
||||||
|
# run 'config' target if the build directory or CMake cache is missing.
|
||||||
|
$(CMAKE_MAKEFILE):
|
||||||
|
$(MAKE) config
|
||||||
|
|
||||||
config:
|
config:
|
||||||
@echo "=== Configuring Project. ==="
|
@echo "==== Configuring Bettola with CMake ===="
|
||||||
@cmake -B $(BUILD_DIR) -S . -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
@cmake -S . -B$(BUILD_DIR) -DCMAKE_CXX_COMPILER=$(CXX_COMPILER) -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||||
|
|
||||||
runc: build
|
# Build and run.
|
||||||
@echo "=== Running Bettola Client. ==="
|
run: all
|
||||||
@$(CLIENT_EXE)
|
@echo "==== Running Bettola ===="
|
||||||
|
$(EXECUTABLE)
|
||||||
sp: build
|
|
||||||
@echo "=== Running Bettola Client (Single Player). ==="
|
|
||||||
@$(CLIENT_EXE) -sp
|
|
||||||
|
|
||||||
runs: build
|
|
||||||
@echo "=== Running Bettola Server. ==="
|
|
||||||
@$(SERVER_EXE)
|
|
||||||
|
|
||||||
|
# Remove build dir.
|
||||||
clean:
|
clean:
|
||||||
@echo "=== Cleaning Build Directory. ==="
|
@echo "==== Cleaning Bettola ===="
|
||||||
@rm -rf $(BUILD_DIR)
|
@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
|
* Dependencies
|
||||||
The following dependencies are required:
|
The following dependencies are requird:
|
||||||
- A C++ compiler (e.g., clang, g++)
|
- A C++ compiler (e.g., clang, g++)
|
||||||
- CMake (3.16 or newer)
|
- CMake (3.16 or newer)
|
||||||
- SDL3 development libraries
|
- SDL3 development libraries
|
||||||
- Asio networking library (managed by CMake)
|
|
||||||
- GLEW development libraries
|
- GLEW development libraries
|
||||||
- FreeType development libraries
|
|
||||||
- Lua 5.4 development libraries
|
|
||||||
- sol2 (header-only, managed by CMake)
|
|
||||||
|
|
||||||
** Installation (Debian)
|
** 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
|
#+BEGIN_SRC bash
|
||||||
sudo apt update
|
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
|
#+END_SRC
|
||||||
|
|
||||||
* Build Instructions
|
* Build Instructions
|
||||||
This project uses a top-level Makefile to simplify the CMake workflow. Build
|
This project uses a top-level Makefile to simplify the CMake workflow. All build artifacts will be placed in the =bin/= directory.
|
||||||
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:*
|
- *Build the project:*
|
||||||
|
The executable will be located in =bin/bettola=.
|
||||||
#+BEGIN_SRC bash
|
#+BEGIN_SRC bash
|
||||||
make
|
make
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
- *Build and run the client:*
|
- *Build and run the project:*
|
||||||
#+BEGIN_SRC bash
|
#+BEGIN_SRC bash
|
||||||
make runc
|
make run
|
||||||
#+END_SRC
|
|
||||||
|
|
||||||
- *Build and run the server:*
|
|
||||||
#+BEGIN_SRC bash
|
|
||||||
make runs
|
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
- *Clean the project:*
|
- *Clean the project:*
|
||||||
@ -46,53 +37,3 @@ Simply run the following commands from the root of the project directory:
|
|||||||
#+BEGIN_SRC bash
|
#+BEGIN_SRC bash
|
||||||
make clean
|
make clean
|
||||||
#+END_SRC
|
#+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,62 +0,0 @@
|
|||||||
-- /bin/ls - Lists files in a directory.
|
|
||||||
--
|
|
||||||
-- Iterate over the 'children' map exposed via C++.
|
|
||||||
|
|
||||||
local function format_permissions(perms)
|
|
||||||
local rwx = { "-", "-", "-", "-", "-", "-", "-", "-", "-" }
|
|
||||||
if(perms & 0x100) ~= 0 then rwx[1] = "r" end -- Owner read.
|
|
||||||
if(perms & 0x080) ~= 0 then rwx[2] = "w" end -- Owner write.
|
|
||||||
if(perms & 0x040) ~= 0 then rwx[3] = "x" end -- Owner execute.
|
|
||||||
if(perms & 0x020) ~= 0 then rwx[4] = "r" end -- Group read.
|
|
||||||
if(perms & 0x010) ~= 0 then rwx[5] = "w" end -- Group write.
|
|
||||||
if(perms & 0x008) ~= 0 then rwx[6] = "x" end -- Group execute.
|
|
||||||
if(perms & 0x004) ~= 0 then rwx[7] = "r" end -- Other read.
|
|
||||||
if(perms & 0x002) ~= 0 then rwx[8] = "w" end -- Other write.
|
|
||||||
if(perms & 0x001) ~= 0 then rwx[9] = "x" end -- Other execute.
|
|
||||||
return table.concat(rwx)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_file_size(node)
|
|
||||||
if node.type == 0 then -- FILE_NODE.
|
|
||||||
return #node.content
|
|
||||||
else
|
|
||||||
return 0 -- Dirs don't have content size in this context.
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ls_long_format(dir)
|
|
||||||
local output = {}
|
|
||||||
for name, node in pairs(dir.children) do
|
|
||||||
local line_type = (node.type == 1) and "d" or "-"
|
|
||||||
local perms = format_permissions(node.permissions)
|
|
||||||
local owner = node.owner_id
|
|
||||||
local group = node.group_id
|
|
||||||
local size = get_file_size(node)
|
|
||||||
table.insert(output, string.format("%s%s %d %d %5d %s", line_type, perms, owner, group, size, name))
|
|
||||||
end
|
|
||||||
table.sort(output)
|
|
||||||
return table.concat(output, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ls_short_format(dir)
|
|
||||||
local output = {}
|
|
||||||
for name, node in pairs(dir.children) do
|
|
||||||
local display_name = name
|
|
||||||
if node.type == 1 then -- DIR_NODE
|
|
||||||
display_name = display_name .. "/"
|
|
||||||
elseif node.type == 2 then --EXEC_NODE
|
|
||||||
display_name = display_name .. "*"
|
|
||||||
end
|
|
||||||
table.insert(output, display_name)
|
|
||||||
end
|
|
||||||
table.sort(output)
|
|
||||||
return table.concat(output, "\t") -- Tab separated short format.
|
|
||||||
end
|
|
||||||
|
|
||||||
local current_dir = bettola.get_current_dir(context);
|
|
||||||
|
|
||||||
if arg[1] == "-l" then
|
|
||||||
return ls_long_format(current_dir)
|
|
||||||
else
|
|
||||||
return ls_short_format(current_dir)
|
|
||||||
end
|
|
||||||
@ -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,97 +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,"
|
|
||||||
"owner_id INTEGER NOT NULL,"
|
|
||||||
"group_id INTEGER NOT NULL,"
|
|
||||||
"permissions INTEGER NOT NULL"
|
|
||||||
");";
|
|
||||||
_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,
|
|
||||||
"", player_id, player_id, 0755);
|
|
||||||
|
|
||||||
/* Create default subdirs. */
|
|
||||||
_vfs_repository->create_node(machine_id, &root_id, "home", DIR_NODE,
|
|
||||||
"", player_id, player_id, 0755);
|
|
||||||
_vfs_repository->create_node(machine_id, &root_id, "etc", DIR_NODE,
|
|
||||||
"", player_id, player_id, 0755);
|
|
||||||
|
|
||||||
/* Create /bin and get it's ID */
|
|
||||||
long long bin_id = _vfs_repository->create_node(machine_id, &root_id, "bin", DIR_NODE,
|
|
||||||
"", player_id, player_id, 0755);
|
|
||||||
|
|
||||||
/* 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,
|
|
||||||
player_id, player_id, 0755);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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,46 +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,
|
|
||||||
uint32_t owner_id, uint32_t group_id,
|
|
||||||
uint16_t permissions) {
|
|
||||||
if(parent_id) {
|
|
||||||
_db << "INSERT INTO vfs_nodes (machine_id, parent_id, name, type, content, "
|
|
||||||
"owner_id, group_id, permissions) "
|
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?);"
|
|
||||||
<< machine_id << *parent_id << name << type << content << owner_id
|
|
||||||
<< group_id << permissions;
|
|
||||||
} else {
|
|
||||||
_db << "INSERT INTO vfs_nodes (machine_id, parent_id, name, type, content, "
|
|
||||||
"owner_id, group_id, permissions) "
|
|
||||||
"VALUES (?, NULL, ?, ?, ?, ?, ?, ?);"
|
|
||||||
<< machine_id << name << type << content
|
|
||||||
<< owner_id << group_id << permissions;
|
|
||||||
}
|
|
||||||
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, owner_id, group_id, permissions "
|
|
||||||
"FROM vfs_nodes WHERE machine_id = ?;"
|
|
||||||
<< machine_id
|
|
||||||
>> [&](long long id, long long parent_id, std::string name, int type,
|
|
||||||
std::string content, uint32_t owner_id, uint32_t group_id, uint16_t permissions) {
|
|
||||||
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;
|
|
||||||
node->owner_id = owner_id;
|
|
||||||
node->group_id = group_id;
|
|
||||||
node->permissions = permissions;
|
|
||||||
|
|
||||||
nodes.push_back(node);
|
|
||||||
};
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#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 = "",
|
|
||||||
uint32_t owner_id = 0, uint32_t group_id = 0,
|
|
||||||
uint16_t permissions = 0755);
|
|
||||||
|
|
||||||
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,84 +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,
|
|
||||||
"owner_id", &vfs_node::owner_id,
|
|
||||||
"group_id", &vfs_node::group_id,
|
|
||||||
"permissions", &vfs_node::permissions);
|
|
||||||
|
|
||||||
/* 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,148 +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,
|
|
||||||
original->owner_id, original->group_id,
|
|
||||||
original->permissions);
|
|
||||||
|
|
||||||
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, 0, 0, 0755); /* System owned. */
|
|
||||||
_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, 0, 0, 0755); /* System owned. */
|
|
||||||
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, 0, 0, 0755); /* Root owned by system (0). */
|
|
||||||
|
|
||||||
/* Create directories for this specific VFS. */
|
|
||||||
vfs_node* home = new_node("home", DIR_NODE, root, id, id, 0755); /* Owned by player. */
|
|
||||||
vfs_node* user = new_node("user", DIR_NODE, home);
|
|
||||||
home->children["user"] = user;
|
|
||||||
vfs_node* readme = new_node("readme.txt", FILE_NODE, user, id, id, 0644); /* Owned by player. */
|
|
||||||
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);
|
|
||||||
/* Ensure copied bin directory has the correct owner/group/permissions. */
|
|
||||||
root->children["bin"]->owner_id = 0; /* System owned. */
|
|
||||||
root->children["bin"]->group_id = 0;
|
|
||||||
root->children["bin"]->permissions = 0755;
|
|
||||||
|
|
||||||
/* 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, 0, 0, 0644);
|
|
||||||
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