diff --git a/.gitignore b/.gitignore index 61a2874..d6fec84 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ dist/freetype-test* scenes/show-meshes* scenes/show-scene* *.blend[0-9] +CMakeLists.txt +cmake-build-debug/ +.idea/ +.vscode/ +.DS_STORE diff --git a/ECS/Component.cpp b/ECS/Component.cpp new file mode 100644 index 0000000..83683d2 --- /dev/null +++ b/ECS/Component.cpp @@ -0,0 +1 @@ +#include "Component.hpp" diff --git a/ECS/Component.hpp b/ECS/Component.hpp new file mode 100644 index 0000000..7e28466 --- /dev/null +++ b/ECS/Component.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +template +struct Component { + static std::unordered_map &get_map() { + static std::unordered_map map; + return map; + }; +}; diff --git a/ECS/Components/EventHandler.cpp b/ECS/Components/EventHandler.cpp new file mode 100644 index 0000000..e7d79ed --- /dev/null +++ b/ECS/Components/EventHandler.cpp @@ -0,0 +1,9 @@ +#include +#include "EventHandler.hpp" + +EventHandler::EventHandler(const std::function &f) + : handler(f) {} + +bool EventHandler::handle_event(const SDL_Event &evt, const glm::uvec2 &move) { + return handler(evt, move); +} diff --git a/ECS/Components/EventHandler.hpp b/ECS/Components/EventHandler.hpp new file mode 100644 index 0000000..9f5d179 --- /dev/null +++ b/ECS/Components/EventHandler.hpp @@ -0,0 +1,23 @@ +/* + * Created by Nellie Tonev on 10/27/23. + * Author(s): Nellie Tonev, Russell Emerine + * + * Component that corresponds to PlayMode::handle_event + */ +#pragma once + +#include +#include + +#include +#include +#include "../Component.hpp" + +struct EventHandler : Component { + explicit EventHandler(const std::function &f); + + bool handle_event(SDL_Event const &evt, glm::uvec2 const &window_size); + +private: + const std::function &handler; +}; diff --git a/ECS/Entity.cpp b/ECS/Entity.cpp new file mode 100644 index 0000000..e51761c --- /dev/null +++ b/ECS/Entity.cpp @@ -0,0 +1,21 @@ +/* + * Created by Matei Budiu on 10/26/23. + * Author(s): Nellie Tonev + * + * Adapted from Entity code covered in class on 10/24/23. + */ + +#include "Entity.hpp" + +Entity::Entity() { + /* create unique id for entity lookup */ + static uint32_t unique_id = 0; + id = unique_id; + unique_id++; +} + +Entity::~Entity() { + for (auto &[_, f]: to_delete) { + f(); + } +} diff --git a/ECS/Entity.hpp b/ECS/Entity.hpp new file mode 100644 index 0000000..2d541ba --- /dev/null +++ b/ECS/Entity.hpp @@ -0,0 +1,73 @@ +/* + * Created by Matei Budiu on 10/26/23. + * Author(s): Matei Budiu, Russell Emerine, Nellie Tonev + */ +#pragma once + +#include +#include +#include +#include +#include + +struct Entity { + Entity(); + + /* + * Destruct the entity by removing all components that haven't yet been removed + */ + ~Entity(); + + /* + * Unique identity for querying and deleting entities, generated uniquely in the constructor + */ + uint32_t id; + + /* + * Associate a component with this entity, update map of entities with component T + */ + template + T &add_component(Args &&... args) { + to_delete[std::type_index(typeid(T))] = [this]() { + // I don't just call remove_component because that invalidates the iterator in ~Entity + std::unordered_map &entity_to_component = T::get_map(); + entity_to_component.erase(id); + }; + + std::unordered_map &entity_to_component = T::get_map(); + // this call to emplace looks weird but I'm pretty sure it's correct + return entity_to_component.emplace( + std::piecewise_construct, + std::forward_as_tuple(id), + std::forward_as_tuple(args...) + ).first->second; + } + + /* + * Look up a component associated with this entity + */ + template + T *get_component() { + std::unordered_map &entity_to_component = T::get_map(); + auto f = entity_to_component.find(id); + if (f == entity_to_component.end()) return nullptr; + else return &f->second; + } + + /* + * Remove a component associated with this entity (if the entity has the component) + */ + template + void remove_component() { + to_delete.erase(std::type_index(typeid(T))); + + std::unordered_map &entity_to_component = T::get_map(); + entity_to_component.erase(id); + } + +private: + /* + * Keep track of what components to delete upon destruction + */ + std::unordered_map> to_delete; +}; diff --git a/ECS/README.md b/ECS/README.md new file mode 100644 index 0000000..d29149d --- /dev/null +++ b/ECS/README.md @@ -0,0 +1,17 @@ +# ECS Documentation +## **Entity** +A container of components corresponding to objects and elements of the game. +Classes for game objects (e.g., Player) should include a function to create an entity +representing the data that the main game loop needs. + +## **Components** +Define a new component type for any functionality that is shared between game objects +of different classes. Keep track of component definitions and +any implementation notes below. + +### EventHandler +Add to any entities that handle user inputs (e.g., Player and Terminal). +After attaching the EventHandler component, make sure to update its handle_event() function appropriately. +Edit the main loop to check if the event is handled across any of the entities with this component. + +## **Systems** diff --git a/LitColorTextureProgram.cpp b/LitColorTextureProgram.cpp index 6bde8a7..a078c75 100644 --- a/LitColorTextureProgram.cpp +++ b/LitColorTextureProgram.cpp @@ -14,6 +14,7 @@ Load< LitColorTextureProgram > lit_color_texture_program(LoadTagEarly, []() -> L lit_color_texture_program_pipeline.OBJECT_TO_CLIP_mat4 = ret->OBJECT_TO_CLIP_mat4; lit_color_texture_program_pipeline.OBJECT_TO_LIGHT_mat4x3 = ret->OBJECT_TO_LIGHT_mat4x3; lit_color_texture_program_pipeline.NORMAL_TO_LIGHT_mat3 = ret->NORMAL_TO_LIGHT_mat3; + lit_color_texture_program_pipeline.draw_frame = ret->draw_frame; /* This will be used later if/when we build a light loop into the Scene: lit_color_texture_program_pipeline.LIGHT_TYPE_int = ret->LIGHT_TYPE_int; @@ -80,6 +81,7 @@ LitColorTextureProgram::LitColorTextureProgram() { "in vec4 color;\n" "in vec2 texCoord;\n" "out vec4 fragColor;\n" + "uniform bool wireframe;\n" "void main() {\n" " vec3 n = normalize(normal);\n" " vec3 e;\n" @@ -103,7 +105,12 @@ LitColorTextureProgram::LitColorTextureProgram() { " e = max(0.0, dot(n,-LIGHT_DIRECTION)) * LIGHT_ENERGY;\n" " }\n" " vec4 albedo = texture(TEX, texCoord) * color;\n" - " fragColor = vec4(e*albedo.rgb, albedo.a);\n" + " if(wireframe){\n" + " fragColor = vec4(0.0,0.0,0.0,1.0);\n" + " }\n" + " else{\n" + " fragColor = vec4(e*albedo.rgb, 1.0);\n" + " }\n" "}\n" ); //As you can see above, adjacent strings in C/C++ are concatenated. @@ -126,6 +133,8 @@ LitColorTextureProgram::LitColorTextureProgram() { LIGHT_ENERGY_vec3 = glGetUniformLocation(program, "LIGHT_ENERGY"); LIGHT_CUTOFF_float = glGetUniformLocation(program, "LIGHT_CUTOFF"); + draw_frame = glGetUniformLocation(program,"wireframe"); + GLuint TEX_sampler2D = glGetUniformLocation(program, "TEX"); diff --git a/LitColorTextureProgram.hpp b/LitColorTextureProgram.hpp index da87dcb..563c521 100644 --- a/LitColorTextureProgram.hpp +++ b/LitColorTextureProgram.hpp @@ -28,6 +28,9 @@ struct LitColorTextureProgram { GLuint LIGHT_DIRECTION_vec3 = -1U; GLuint LIGHT_ENERGY_vec3 = -1U; GLuint LIGHT_CUTOFF_float = -1U; + + //Wireframe drawing + GLuint draw_frame = -1U; //Textures: //TEXTURE0 - texture that is accessed by TexCoord diff --git a/Maekfile.js b/Maekfile.js index 3847378..874f6f0 100644 --- a/Maekfile.js +++ b/Maekfile.js @@ -24,100 +24,103 @@ const NEST_LIBS = `../nest-libs/${maek.OS}`; //set compile flags (these can also be overridden per-task using the "options" parameter): if (maek.OS === "windows") { - maek.options.CPPFlags.push( - `/O2`, //optimize - //include paths for nest libraries: - `/I${NEST_LIBS}/SDL2/include`, - `/I${NEST_LIBS}/glm/include`, - `/I${NEST_LIBS}/libpng/include`, - `/I${NEST_LIBS}/opusfile/include`, - `/I${NEST_LIBS}/libopus/include`, - `/I${NEST_LIBS}/libogg/include`, - `/I${NEST_LIBS}/harfbuzz/include`, - `/I${NEST_LIBS}/freetype/include`, - //#disable a few warnings: - `/wd4146`, //-1U is still unsigned - `/wd4297`, //unfortunately SDLmain is nothrow - `/wd4100`, //unreferenced formal parameter - `/wd4201`, //nameless struct/union - `/wd4611` //interaction between setjmp and C++ object destruction - ); - maek.options.LINKLibs.push( - `/LIBPATH:${NEST_LIBS}/SDL2/lib`, `SDL2main.lib`, `SDL2.lib`, `OpenGL32.lib`, `Shell32.lib`, - `/LIBPATH:${NEST_LIBS}/libpng/lib`, `libpng.lib`, - `/LIBPATH:${NEST_LIBS}/zlib/lib`, `zlib.lib`, - `/LIBPATH:${NEST_LIBS}/opusfile/lib`, `opusfile.lib`, - `/LIBPATH:${NEST_LIBS}/libopus/lib`, `opus.lib`, - `/LIBPATH:${NEST_LIBS}/libogg/lib`, `libogg.lib`, - `/LIBPATH:${NEST_LIBS}/harfbuzz/lib`, `harfbuzz.lib`, - `/LIBPATH:${NEST_LIBS}/freetype/lib`, `freetype.lib`, - `/MANIFEST:EMBED`, `/MANIFESTINPUT:set-utf8-code-page.manifest` - ); + maek.options.CPPFlags.push( + `/O2`, //optimize + //include paths for nest libraries: + `/I${NEST_LIBS}/SDL2/include`, + `/I${NEST_LIBS}/glm/include`, + `/I${NEST_LIBS}/libpng/include`, + `/I${NEST_LIBS}/opusfile/include`, + `/I${NEST_LIBS}/libopus/include`, + `/I${NEST_LIBS}/libogg/include`, + `/I${NEST_LIBS}/harfbuzz/include`, + `/I${NEST_LIBS}/freetype/include`, + //#disable a few warnings: + `/wd4146`, //-1U is still unsigned + `/wd4297`, //unforunately SDLmain is nothrow + `/wd4100`, //unreferenced formal parameter + `/wd4201`, //nameless struct/union + `/wd4611` //interaction between setjmp and C++ object destruction + ); + maek.options.LINKLibs.push( + `/LIBPATH:${NEST_LIBS}/SDL2/lib`, `SDL2main.lib`, `SDL2.lib`, `OpenGL32.lib`, `Shell32.lib`, + `/LIBPATH:${NEST_LIBS}/libpng/lib`, `libpng.lib`, + `/LIBPATH:${NEST_LIBS}/zlib/lib`, `zlib.lib`, + `/LIBPATH:${NEST_LIBS}/opusfile/lib`, `opusfile.lib`, + `/LIBPATH:${NEST_LIBS}/libopus/lib`, `opus.lib`, + `/LIBPATH:${NEST_LIBS}/libogg/lib`, `libogg.lib`, + `/LIBPATH:${NEST_LIBS}/harfbuzz/lib`, `harfbuzz.lib`, + `/LIBPATH:${NEST_LIBS}/freetype/lib`, `freetype.lib`, + `/MANIFEST:EMBED`, `/MANIFESTINPUT:set-utf8-code-page.manifest` + ); } else if (maek.OS === "linux") { - maek.options.CPPFlags.push( - `-O2`, //optimize - //include paths for nest libraries: - `-I${NEST_LIBS}/SDL2/include/SDL2`, `-D_THREAD_SAFE`, //the output of sdl-config --cflags - `-I${NEST_LIBS}/glm/include`, - `-I${NEST_LIBS}/libpng/include`, - `-I${NEST_LIBS}/opusfile/include`, - `-I${NEST_LIBS}/libopus/include`, - `-I${NEST_LIBS}/libogg/include`, - `-I${NEST_LIBS}/harfbuzz/include`, - `-I${NEST_LIBS}/freetype/include` - ); - maek.options.LINKLibs.push( - //linker flags for nest libraries: - `-L${NEST_LIBS}/SDL2/lib`, `-lSDL2`, `-lm`, `-ldl`, `-lasound`, `-lpthread`, `-lX11`, `-lXext`, `-lpthread`, `-lrt`, `-lGL`, //the output of sdl-config --static-libs - `-L${NEST_LIBS}/libpng/lib`, `-lpng`, - `-L${NEST_LIBS}/zlib/lib`, `-lz`, - `-L${NEST_LIBS}/opusfile/lib`, `-lopusfile`, - `-L${NEST_LIBS}/libopus/lib`, `-lopus`, - `-L${NEST_LIBS}/libogg/lib`, `-logg`, - `-L${NEST_LIBS}/harfbuzz/lib`, `-lharfbuzz`, - `-L${NEST_LIBS}/freetype/lib`, `-lfreetype` - ); + maek.options.CPPFlags.push( + `-O2`, //optimize + //include paths for nest libraries: + `-I${NEST_LIBS}/SDL2/include/SDL2`, `-D_THREAD_SAFE`, //the output of sdl-config --cflags + `-I${NEST_LIBS}/glm/include`, + `-I${NEST_LIBS}/libpng/include`, + `-I${NEST_LIBS}/opusfile/include`, + `-I${NEST_LIBS}/libopus/include`, + `-I${NEST_LIBS}/libogg/include`, + `-I${NEST_LIBS}/harfbuzz/include`, + `-I${NEST_LIBS}/freetype/include` + ); + maek.options.LINKLibs.push( + //linker flags for nest libraries: + `-L${NEST_LIBS}/SDL2/lib`, `-lSDL2`, `-lm`, `-ldl`, `-lasound`, `-lpthread`, `-lX11`, `-lXext`, `-lpthread`, `-lrt`, `-lGL`, //the output of sdl-config --static-libs + `-L${NEST_LIBS}/libpng/lib`, `-lpng`, + `-L${NEST_LIBS}/zlib/lib`, `-lz`, + `-L${NEST_LIBS}/opusfile/lib`, `-lopusfile`, + `-L${NEST_LIBS}/libopus/lib`, `-lopus`, + `-L${NEST_LIBS}/libogg/lib`, `-logg`, + `-L${NEST_LIBS}/harfbuzz/lib`, `-lharfbuzz`, + `-L${NEST_LIBS}/freetype/lib`, `-lfreetype` + ); } else if (maek.OS === "macos") { - maek.options.CPPFlags.push( - `-O2`, //optimize - //include paths for nest libraries: - `-I${NEST_LIBS}/SDL2/include/SDL2`, `-D_THREAD_SAFE`, //the output of sdl-config --cflags - `-I${NEST_LIBS}/glm/include`, `-Wno-deprecated-declarations`, //because of vsprintf in string_cast - `-I${NEST_LIBS}/libpng/include`, - `-I${NEST_LIBS}/opusfile/include`, - `-I${NEST_LIBS}/libopus/include`, - `-I${NEST_LIBS}/libogg/include`, - `-I${NEST_LIBS}/harfbuzz/include`, - `-I${NEST_LIBS}/freetype/include` - ); - maek.options.LINKLibs.push( - //linker flags for nest libraries: - `-L${NEST_LIBS}/SDL2/lib`, `-lSDL2`, `-lm`,`-liconv`, `-framework`, `CoreAudio`, `-framework`, `AudioToolbox`, `-weak_framework`, `CoreHaptics`, `-weak_framework`, `GameController`, `-framework`, `ForceFeedback`, `-lobjc`, `-framework`, `CoreVideo`, `-framework`, `Cocoa`, `-framework`, `Carbon`, `-framework`, `IOKit`, `-framework`, `OpenGL`, //the output of sdl-config --static-libs - `-L${NEST_LIBS}/libpng/lib`, `-lpng`, - `-L${NEST_LIBS}/zlib/lib`, `-lz`, - `-L${NEST_LIBS}/opusfile/lib`, `-lopusfile`, - `-L${NEST_LIBS}/libopus/lib`, `-lopus`, - `-L${NEST_LIBS}/libogg/lib`, `-logg`, - `-L${NEST_LIBS}/harfbuzz/lib`, `-lharfbuzz`, - `-L${NEST_LIBS}/freetype/lib`, `-lfreetype` - ); + maek.options.CPPFlags.push( + `-O2`, //optimize + //include paths for nest libraries: + `-I${NEST_LIBS}/SDL2/include/SDL2`, `-D_THREAD_SAFE`, //the output of sdl-config --cflags + `-I${NEST_LIBS}/glm/include`, '-Wno-deprecated-declarations', //because of vsprintf in string_cast + `-I${NEST_LIBS}/libpng/include`, + `-I${NEST_LIBS}/opusfile/include`, + `-I${NEST_LIBS}/libopus/include`, + `-I${NEST_LIBS}/libogg/include`, + `-I${NEST_LIBS}/harfbuzz/include`, + `-I${NEST_LIBS}/freetype/include` + ); + maek.options.LINKLibs.push( + //linker flags for nest libraries: + `-L${NEST_LIBS}/SDL2/lib`, `-lSDL2`, `-lm`, `-liconv`, `-framework`, `CoreAudio`, `-framework`, `AudioToolbox`, `-weak_framework`, `CoreHaptics`, `-weak_framework`, `GameController`, `-framework`, `ForceFeedback`, `-lobjc`, `-framework`, `CoreVideo`, `-framework`, `Cocoa`, `-framework`, `Carbon`, `-framework`, `IOKit`, `-framework`, `OpenGL`, //the output of sdl-config --static-libs + `-L${NEST_LIBS}/libpng/lib`, `-lpng`, + `-L${NEST_LIBS}/zlib/lib`, `-lz`, + `-L${NEST_LIBS}/opusfile/lib`, `-lopusfile`, + `-L${NEST_LIBS}/libopus/lib`, `-lopus`, + `-L${NEST_LIBS}/libogg/lib`, `-logg`, + `-L${NEST_LIBS}/harfbuzz/lib`, `-lharfbuzz`, + `-L${NEST_LIBS}/freetype/lib`, `-lfreetype` + ); } //use COPY to copy a file // 'COPY(from, to)' // from: file to copy from // to: file to copy to let copies = [ - maek.COPY(`${NEST_LIBS}/SDL2/dist/README-SDL.txt`, `dist/README-SDL.txt`), - maek.COPY(`${NEST_LIBS}/libpng/dist/README-libpng.txt`, `dist/README-libpng.txt`), - maek.COPY(`${NEST_LIBS}/glm/dist/README-glm.txt`, `dist/README-glm.txt`), - maek.COPY(`${NEST_LIBS}/libopus/dist/README-libopus.txt`, `dist/README-libopus.txt`), - maek.COPY(`${NEST_LIBS}/opusfile/dist/README-opusfile.txt`, `dist/README-opusfile.txt`), - maek.COPY(`${NEST_LIBS}/libogg/dist/README-libogg.txt`, `dist/README-libogg.txt`), - maek.COPY(`${NEST_LIBS}/harfbuzz/dist/README-harfbuzz.txt`, `dist/README-harfbuzz.txt`), - maek.COPY(`${NEST_LIBS}/freetype/dist/README-freetype.txt`, `dist/README-freetype.txt`) + maek.COPY(`${NEST_LIBS}/SDL2/dist/README-SDL.txt`, `dist/README-SDL.txt`), + maek.COPY(`${NEST_LIBS}/libpng/dist/README-libpng.txt`, `dist/README-libpng.txt`), + maek.COPY(`${NEST_LIBS}/glm/dist/README-glm.txt`, `dist/README-glm.txt`), + maek.COPY(`${NEST_LIBS}/libopus/dist/README-libopus.txt`, `dist/README-libopus.txt`), + maek.COPY(`${NEST_LIBS}/opusfile/dist/README-opusfile.txt`, `dist/README-opusfile.txt`), + maek.COPY(`${NEST_LIBS}/libogg/dist/README-libogg.txt`, `dist/README-libogg.txt`), + maek.COPY(`${NEST_LIBS}/harfbuzz/dist/README-harfbuzz.txt`, `dist/README-harfbuzz.txt`), + maek.COPY(`${NEST_LIBS}/freetype/dist/README-freetype.txt`, `dist/README-freetype.txt`), + maek.COPY(`fonts/UFL.txt`, `dist/UFL.txt`), + maek.COPY(`fonts/README.md`, `dist/README-fonts.txt`), + maek.COPY(`fonts/UbuntuMono.png`, `dist/UbuntuMono.png`) ]; if (maek.OS === 'windows') { - copies.push( maek.COPY(`${NEST_LIBS}/SDL2/dist/SDL2.dll`, `dist/SDL2.dll`) ); + copies.push(maek.COPY(`${NEST_LIBS}/SDL2/dist/SDL2.dll`, `dist/SDL2.dll`)); } //call rules on the maek object to specify tasks. @@ -128,754 +131,756 @@ if (maek.OS === 'windows') { // cppFile: name of c++ file to compile // objFileBase (optional): base name object file to produce (if not supplied, set to options.objDir + '/' + cppFile without the extension) //returns objFile: objFileBase + a platform-dependant suffix ('.o' or '.obj') -const client_names = [ - maek.CPP('client.cpp'), - maek.CPP('PlayMode.cpp'), - maek.CPP('LitColorTextureProgram.cpp'), - //maek.CPP('ColorTextureProgram.cpp'), //not used right now, but you might want it - maek.CPP('Sound.cpp'), - maek.CPP('load_wav.cpp'), - maek.CPP('load_opus.cpp') -]; - -const server_names = [ - maek.CPP('server.cpp') +const game_names = [ + maek.CPP('WalkMesh.cpp'), + maek.CPP('PlayMode.cpp'), + maek.CPP('main.cpp'), + maek.CPP('LitColorTextureProgram.cpp'), + //maek.CPP('ColorTextureProgram.cpp'), //not used right now, but you might want it + maek.CPP('Sound.cpp'), + maek.CPP('load_wav.cpp'), + maek.CPP('load_opus.cpp') ]; const common_names = [ - maek.CPP('Game.cpp'), - maek.CPP('data_path.cpp'), - maek.CPP('PathFont.cpp'), - maek.CPP('PathFont-font.cpp'), - maek.CPP('DrawLines.cpp'), - maek.CPP('ColorProgram.cpp'), - maek.CPP('Scene.cpp'), - maek.CPP('Mesh.cpp'), - maek.CPP('load_save_png.cpp'), - maek.CPP('gl_compile_program.cpp'), - maek.CPP('Mode.cpp'), - maek.CPP('GL.cpp'), - maek.CPP('Load.cpp'), - maek.CPP('Connection.cpp'), - maek.CPP('hex_dump.cpp') + maek.CPP('data_path.cpp'), + maek.CPP('PathFont.cpp'), + maek.CPP('PathFont-font.cpp'), + maek.CPP('DrawLines.cpp'), + maek.CPP('ColorProgram.cpp'), + maek.CPP('Scene.cpp'), + maek.CPP('Mesh.cpp'), + maek.CPP('load_save_png.cpp'), + maek.CPP('gl_compile_program.cpp'), + maek.CPP('Mode.cpp'), + maek.CPP('GL.cpp'), + maek.CPP('Load.cpp'), + maek.CPP('ECS/Entity.cpp'), + maek.CPP('ECS/Components/EventHandler.cpp'), + maek.CPP('spline.cpp'), + maek.CPP('Terminal.cpp'), + maek.CPP('MonospaceFont.cpp'), + maek.CPP('TexProgram.cpp') ]; const show_meshes_names = [ - maek.CPP('show-meshes.cpp'), - maek.CPP('ShowMeshesProgram.cpp'), - maek.CPP('ShowMeshesMode.cpp') + maek.CPP('show-meshes.cpp'), + maek.CPP('ShowMeshesProgram.cpp'), + maek.CPP('ShowMeshesMode.cpp') ]; const show_scene_names = [ - maek.CPP('show-scene.cpp'), - maek.CPP('ShowSceneProgram.cpp'), - maek.CPP('ShowSceneMode.cpp') + maek.CPP('show-scene.cpp'), + maek.CPP('ShowSceneProgram.cpp'), + maek.CPP('ShowSceneMode.cpp') ]; //the '[exeFile =] LINK(objFiles, exeFileBase, [, options])' links an array of objects into an executable: // objFiles: array of objects to link // exeFileBase: name of executable file to produce //returns exeFile: exeFileBase + a platform-dependant suffix (e.g., '.exe' on windows) -const client_exe = maek.LINK([...client_names, ...common_names], 'dist/client'); -const server_exe = maek.LINK([...server_names, ...common_names], 'dist/server'); +const game_exe = maek.LINK([...game_names, ...common_names], 'dist/game'); const show_meshes_exe = maek.LINK([...show_meshes_names, ...common_names], 'scenes/show-meshes'); const show_scene_exe = maek.LINK([...show_scene_names, ...common_names], 'scenes/show-scene'); //set the default target to the game (and copy the readme files): -maek.TARGETS = [client_exe, server_exe, show_meshes_exe, show_scene_exe, ...copies]; +maek.TARGETS = [game_exe, show_meshes_exe, show_scene_exe, ...copies]; //Note that tasks that produce ':abstract targets' are never cached. // This is similar to how .PHONY targets behave in make. - //====================================================================== //Now, onward to the code that makes all this work: function init_maek() { - //---------------------------------- - //some setup - - //standard libraries: - const path = require('path').posix; //NOTE: expect posix-style paths even on windows - const fsPromises = require('fs').promises; - const fs = require('fs'); - const os = require('os'); - const performance = require('perf_hooks').performance; - const child_process = require('child_process'); - - //make it so that all paths/commands are relative to this file: - // (regardless of where you run it from) - console.log(`Building in ${__dirname}.`); - process.chdir(__dirname); - - //make it slightly more idiomatic to export: - const maek = module.exports; - - //----------------------------------------- - //Constants: - - //cache file location: - maek.CACHE_FILE = 'maek-cache.json'; - - //current OS: (with slightly nicer naming than os.platform() - maek.OS = (() => { - const platform = os.platform(); - if (platform === 'win32') return 'windows'; - else if (platform === 'darwin') return 'macos'; - else if (platform === 'linux') return 'linux'; - else { - console.error(`ERROR: Unrecognized platform ${os.platform()}.`); - process.exit(1); - } - })(); - - //----------------------------------------- - //Command line defaults: - - //maximum number of jobs to run: (change with -jN) - maek.JOBS = os.cpus().length + 1; - - //targets to build by default: (change by passing target names) - maek.TARGETS = []; - - //print extra info: (set by passing -v) - maek.VERBOSE = false; - - //quit on first failure: (set by passing -q) - maek.QUIT_EAGERLY = false; - - //----------------------------------------- - //options: set to change maek rule behavior - - const DEFAULT_OPTIONS = { - objPrefix: 'objs/', //prefix for object file paths (if not explicitly specified) - objSuffix: (maek.OS === 'windows' ? '.obj' : '.o'), //suffix for object files - exeSuffix: (maek.OS === 'windows' ? '.exe' : ''), //suffix for executable files - depends: [], //extra dependencies; generally only set locally - CPP: [], //the c++ compiler and any flags to start with (set below, per-OS) - CPPFlags: [], //extra flags for c++ compiler - LINK: [], //the linker and any flags to start with (set below, per-OS) - LINKLibs: [], //extra -L and -l flags for linker - } - - if (maek.OS === 'windows') { - DEFAULT_OPTIONS.CPP = ['cl.exe', '/nologo', '/EHsc', '/Z7', '/std:c++17', '/W4', '/WX', '/MD']; - //TODO: could embed manifest to set UTF8 codepage - DEFAULT_OPTIONS.LINK = ['link.exe', '/nologo', '/SUBSYSTEM:CONSOLE', '/DEBUG:FASTLINK', '/INCREMENTAL:NO']; - } else if (maek.OS === 'linux') { - DEFAULT_OPTIONS.CPP = ['g++', '-std=c++17', '-Wall', '-Werror', '-g']; - DEFAULT_OPTIONS.LINK = ['g++', '-std=c++17', '-Wall', '-Werror', '-g']; - } else if (maek.OS === 'macos') { - DEFAULT_OPTIONS.CPP = ['clang++', '-std=c++17', '-Wall', '-Werror', '-g']; - DEFAULT_OPTIONS.LINK = ['clang++', '-std=c++17', '-Wall', '-Werror', '-g']; - } - - //any settings here override 'DEFAULT_OPTIONS': - maek.options = Object.assign({}, DEFAULT_OPTIONS); //shallow copy of DEFAULT_OPTIONS in case you want to console.log(maek.options) to check settings. - - //this combines DEFAULT_OPTIONS, maek.options, and localOptions: - function combineOptions(localOptions) { - //shallow copy of default options: - const combined = Object.assign({}, DEFAULT_OPTIONS); - //override with maek.options + complain on missing keys: - for (const key of Object.keys(maek.options)) { - if (!(key in combined)) throw new Error(`ERROR: '${key}' (in maek.options) not recognized.`); - combined[key] = maek.options[key]; - } - //override with localOptions + complain on missing keys: - for (const key of Object.keys(localOptions)) { - if (!(key in combined)) throw new Error(`ERROR: '${key}' (in local options) not recognized.`); - combined[key] = localOptions[key]; - } - return combined; - } - - //tasks is a map from targets -> tasks: - maek.tasks = {}; - - //----------------------------------------- - //RULES. - // helper functions that specify tasks: - - //COPY adds a task that copies a file: - maek.COPY = (srcFile, dstFile) => { - if (typeof srcFile !== "string") throw new Error("COPY: from should be a single file."); - if (typeof dstFile !== "string") throw new Error("COPY: to should be a single file."); - const task = async () => { - try { - await fsPromises.mkdir(path.dirname(dstFile), { recursive: true }); - await fsPromises.copyFile(srcFile, dstFile); - } catch (e) { - throw new BuildError(`Failed to copy '${srcFile}' to '${dstFile}': ${e}`); - } - }; - task.depends = [srcFile]; - task.label = `COPY ${dstFile}`; - maek.tasks[dstFile] = task; - - return dstFile; - }; - - - //maek.CPP makes an object from a c++ source file: - // cppFile is the source file name - // objFileBase (optional) is the output file (including any subdirectories, but not the extension) - maek.CPP = (cppFile, objFileBase, localOptions = {}) => { - //combine options: - const options = combineOptions(localOptions); - - //if objFileBase isn't given, compute by trimming extension from cppFile and appending to objPrefix: - if (typeof objFileBase === 'undefined') { - objFileBase = path.relative('', options.objPrefix + cppFile.replace(/\.[^.]*$/, '')); - } - - //object file gets os-dependent suffix: - const objFile = objFileBase + options.objSuffix; - - //computed dependencies go in a '.d' file stored next to the object file: - const depsFile = objFileBase + '.d'; - - let cc, command; - cc = [...options.CPP, ...options.CPPFlags]; - if (maek.OS === 'linux') { - command = [...cc, '-MD', '-MT', 'x ', '-MF', depsFile, '-c', '-o', objFile, cppFile]; - } else if (maek.OS === 'macos') { - command = [...cc, '-MD', '-MT', 'x ', '-MF', depsFile, '-c', '-o', objFile, cppFile]; - } else { //windows - command = [...cc, '/c', `/Fo${objFile}`, '/sourceDependencies', depsFile, '/Tp', cppFile]; - } - - //will be used by loadDeps to trim explicit dependencies: - async function loadDeps() { - const text = await fsPromises.readFile(depsFile, { encoding: 'utf8' }); - - if (maek.OS === 'windows') { - //parse JSON-encoded dependency info from /sourceDependencies: - const winpath = require('path').win32; - const parsed = JSON.parse(text); - let paths = [...parsed.Data.Includes, parsed.Data.Source]; - paths = paths.map(path => winpath.relative('', path).split('\\').join('/')); - paths = paths.sort(); - return paths; - } else { - //parse the makefile-style "targets : prerequisites" line from the file into a list of tokens: - let tokens = text - .replace(/\\?\n/g, ' ') //escaped newline (or regular newline) => whitespace - .trim() //remove leading and trailing whitespace - .replace(/([^\\])\s+/g, '$1\n') //run of non-escaped whitespace => single newline - .split('\n'); //split on single newlines - - //because of the `-MT 'x '` option, expect 'x :' at the start of the rule: - console.assert(tokens[0] === 'x'); - console.assert(tokens[1] === ':'); - tokens = tokens.slice(2); //remove the 'x :' - //tokens = tokens.map(path => path.relative('', path)); //hmmm does this do anything worthwhile? - tokens = tokens.sort(); //sort for consistency - - //NOTE: might want to do some path normalization here! - return tokens; - } - } - - //The actual build task: - const task = async () => { - //make object file: - await fsPromises.mkdir(path.dirname(objFile), { recursive: true }); - await fsPromises.mkdir(path.dirname(depsFile), { recursive: true }); - await run(command, `${task.label}: compile + prerequisites`, - async () => { - return { - read:[...await loadDeps()], - written:[objFile, depsFile] - }; - } - ); - }; - - task.depends = [cppFile, ...options.depends]; - - task.label = `CPP ${objFile}`; - - if (objFile in maek.tasks) { - throw new Error(`Task ${task.label} purports to create ${objFile}, but ${maek.tasks[objFile].label} already creates that file.`); - } - maek.tasks[objFile] = task; - - return objFile; - }; - - //maek.LINK links an executable file from a collection of object files: - // objFiles is an array of object file names - // exeFileBase is the base name of the executable file ('.exe' will be added on windows) - maek.LINK = (objFiles, exeFileBase, localOptions = {}) => { - const options = combineOptions(localOptions); - - const exeFile = exeFileBase + options.exeSuffix; - - let link, linkCommand; - link = [...options.LINK]; - if (maek.OS === 'linux') { - linkCommand = [...link, '-o', exeFile, ...objFiles, ...options.LINKLibs]; - } else if (maek.OS === 'macos') { - linkCommand = [...link, '-o', exeFile, ...objFiles, ...options.LINKLibs]; - } else { - linkCommand = [...link, `/out:${exeFile}`, ...objFiles, ...options.LINKLibs]; - } - - const task = async () => { - await fsPromises.mkdir(path.dirname(exeFile), { recursive: true }); - await run(linkCommand, `${task.label}: link`, - async () => { - return { - read:[...objFiles], - written:[exeFile] - }; - } - ); - }; - - task.depends = [...objFiles, ...options.depends]; - task.label = `LINK ${exeFile}`; - - if (exeFile in maek.tasks) { - throw new Error(`Task ${task.label} purports to create ${exeFile}, but ${maek.tasks[exeFile].label} already creates that file.`); - } - maek.tasks[exeFile] = task; - - return exeFile; - }; - - - //says something went wrong in building -- should fail loudly: - class BuildError extends Error { - constructor(message) { - super(message); - } - } - - //cache stores the hashes of files involved in run()'d commands: - let cache = {}; - - function loadCache() { - try { - const loaded = JSON.parse(fs.readFileSync(maek.CACHE_FILE, { encoding: 'utf8' })); - let assigned = 0; - let removed = 0; - for (const command of Object.keys(loaded)) { - //cache will have a 'files' and a 'hashes' line - if ('files' in loaded[command] && 'hashes' in loaded[command]) { - cache[command] = { - files:loaded[command].files, - hashes:loaded[command].hashes - }; - assigned += 1; - } else { - removed += 1; - } - } - if (maek.VERBOSE) console.log(` -- Loaded cache from '${maek.CACHE_FILE}'; had ${assigned} valid entries and ${removed} invalid ones.`); - } catch (e) { - if (maek.VERBOSE) console.log(` -- No cache loaded; starting fresh.`); - if (e.code !== 'ENOENT') { - console.warn(`Cache loading failed for unexpected reason:`, e); - } - } - } - - function saveCache() { - if (maek.VERBOSE) console.log(` -- Writing cache with ${Object.keys(cache).length} entries to '${maek.CACHE_FILE}'...`); - fs.writeFileSync(maek.CACHE_FILE, JSON.stringify(cache), { encoding: 'utf8' }); - } - - let runTime = 0.0; - - //runs a shell command (presented as an array) - // 'message' will be displayed above the command - // 'cacheInfoFn', if provided, will be called after function is run to determine which files to hash when caching the result - async function run(command, message, cacheInfoFn) { - - //cache key for the command -- encoded command name: - const cacheKey = JSON.stringify(command); - - //executable for the command: - const exe = await findExe(command); - - //if no cache info function, remove any existing cache entry: - if (!cacheInfoFn) { - delete cache[cacheKey]; - } - - //check for existing cache entry: - let extra = ''; //extra message - if (cacheKey in cache) { - const cached = cache[cacheKey].hashes; - const current = await hashFiles([exe, ...cache[cacheKey].files]); - if (JSON.stringify(current) === JSON.stringify(cached)) { - if (maek.VERBOSE) console.log(`\x1b[33m${message} [cached]\x1b[0m`); - return; - } else { - if (maek.VERBOSE) extra = ` \x1b[33m[cache miss!]\x1b[0m`; - } - } - - - if (typeof message !== 'undefined') { - console.log(`\x1b[90m${message}\x1b[0m${extra}`); - } - - //print a command in a way that can be copied to a shell to run: - let prettyCommand = ''; - for (const token of command) { - if (prettyCommand !== '') prettyCommand += ' '; - if (/[ \t\n!"'$&()*,;<>?[\\\]^`{|}~]/.test(token) - || token[0] === '=' - || token[0] === '#') { - //special characters => need to quote: - prettyCommand += "'" + token.replace(/'/g, "'\\''") + "'"; - } else { - prettyCommand += token; - } - } - console.log(' ' + prettyCommand); - - //package as a promise and await it finishing: - const before = performance.now(); - await new Promise((resolve, reject) => { - const proc = child_process.spawn(command[0], command.slice(1), { - shell: false, - stdio: ['ignore', 'inherit', 'inherit'] - }); - proc.on('exit', (code, signal) => { - if (code !== 0) { - process.stderr.write(`\n`); - reject(new BuildError(`exit ${code} from:\n \x1b[31m${prettyCommand}\x1b[0m\n`)); - } else { - resolve(); - } - }); - proc.on('error', (err) => { - reject(new BuildError(`${err.message} from:\n ${prettyCommand}`)); - }); - }); - runTime += performance.now() - before; - - //store result in cache: - if (cacheInfoFn) { - const {read, written} = await cacheInfoFn(); - - //if hashed one of the written files before, can't rely on it: - for (const file of written) { - delete hashCache[file]; - } - - //update cache with file content hashes: - const files = [...read, ...written]; - cache[cacheKey] = { - files:files, - hashes:await hashFiles([exe, ...files]) - }; - } - - } - - let hashCacheHits = 0; - let hashCache = {}; - let hashLoadTime = 0.0; - let hashComputeTime = 0.0; - - //hash a list of files and return a list of strings describing said hashes (or 'x' on missing file): - async function hashFiles(files) { - const crypto = require('crypto'); - - //helper that will hash a single file: (non-existent files get special hash 'x') - async function hashFile(file) { - if (file in hashCache) { - hashCacheHits += 1; - return hashCache[file]; - } - - //would likely be more efficient to use a pipe with large files, - //but this code is a bit more readable: - const hash = await new Promise((resolve, reject) => { - const beforeLoad = performance.now(); - fs.readFile(file, (err, data) => { - hashLoadTime += performance.now() - beforeLoad; - if (err) { - //if failed to read file, report hash as 'x': - if (err.code != "ENOENT") { - console.warn(`Failed to hash ${file} because of unexpected error ${err}`); //DEBUG - } - resolve(`x`); - } else { - const beforeHash = performance.now(); - //otherwise, report base64-encoded md5sum of file data: - const hash = crypto.createHash('md5'); - hash.update(data); - resolve(`${hash.digest('base64')}`); - hashComputeTime += performance.now() - beforeHash; - } - }); - }); - - hashCache[file] = hash; - return hash; - } - - //get all hashes: - const hashes = []; - for (let file of files) { - hashes.push(await hashFile(file)); - } - return hashes; - } - - //find an executable in the system path - // (used by run to figure out what to hash) - async function findExe(command) { - const osPath = require('path'); - let PATH; - if (maek.OS === 'windows') { - PATH = process.env.PATH.split(';'); - } else { - PATH = process.env.PATH.split(':'); - } - for (const prefix of PATH) { - const exe = osPath.resolve(prefix, command[0]); - try { - await fsPromises.access(exe, fs.constants.X_OK); - return exe; - } catch (e) { - if (e.code === 'ENOENT') continue; - else throw e; - } - } - throw new BuildError(`Couldn't find file for command '${command[0]}'`); - return "?"; - } - - //--------------------------------------- - //'update' actually runs tasks to make targets: - - maek.update = async (targets) => { - const before = performance.now(); - console.log(` -- Maek v0.2 on ${maek.OS} with ${maek.JOBS} max jobs updating '${targets.join("', '")}'...`); - - loadCache(); - process.on('SIGINT', () => { - console.log(`\x1b[91m!!! FAILED: interrupted\x1b[0m`); - saveCache(); - process.exit(1); - }); //allow saving cache on abort - - const tasks = maek.tasks; - - //clear temporary per-task data: - for (const target in tasks) { - delete tasks[target].neededBy; //which tasks need this task - delete tasks[target].finished; //is this task finished? - delete tasks[target].failed; //has this task failed? - } - - - //list of all tasks to run: - const pending = []; - - //add to list of tasks to run and make neededBy array: - function need(target, from) { - if (!(target in tasks)) { - //no task for the target? - if (target[0] === ':') { - //if it's abstract, that's an error: - throw new BuildError(`Target '${target}' (requested by ${from}) is abstract but doesn't have a task.`); - } - //otherwise, it's a plain file: add a task that checks it exists: - const task = async () => { - try { - await fsPromises.access(target, fs.constants.R_OK); - } catch (e) { - throw new BuildError(`Target '${target}' (requested by ${from}) doesn't exist and doesn't have a task to make it.`); - } - }; - task.depends = []; - task.label = `EXISTS '${target}'`; - tasks[target] = task; - } - if ('neededBy' in tasks[target]) return; - pending.push(tasks[target]); - tasks[target].neededBy = []; - for (let depend of tasks[target].depends) { - need(depend, `'${target}'`); - tasks[depend].neededBy.push(tasks[target]); - } - } - - //every requested target is needed: - for (const target of targets) { - need(target, 'user'); - } - - //---------------------------------- - //now run up to JOBS tasks at once: - - let ready = []; //tasks ready to run - let running = []; //tasks currently running - let CANCEL_ALL_TASKS = false; //skip remaining tasks? - - async function launch(task) { - running.push(task); - let failedDepends = []; - for (const depend of task.depends) { - if (tasks[depend].failed) { - failedDepends.push(depend); - } else { - console.assert(tasks[depend].finished, "all depends should be failed or finished"); - } - } - if (failedDepends.length) { - task.failed = true; - if (maek.VERBOSE) console.error(`!!! SKIPPED [${task.label}] because target(s) ${failedDepends.join(', ')} failed.`); - } - try { - if (!task.failed) { - await task(); - task.finished = true; - } - } catch (e) { - if (e instanceof BuildError) { - console.error(`\x1b[91m!!! FAILED [${task.label}] ${e.message}\x1b[0m`); - task.failed = true; - //if -q flag is set, immediately cancel all jobs: - if (maek.QUIT_EAGERLY) { - CANCEL_ALL_TASKS = true; //set flag so jobs cancel themselves - } - } else { - //don't expect any other exceptions, but if they do arise, re-throw 'em: - throw e; - } - } - //check all neededBy for potential readiness: - for (const needed of task.neededBy) { - let allDone = true; - for (const depend of needed.depends) { - if (!(tasks[depend].finished || tasks[depend].failed)) { - allDone = false; - } - } - if (allDone) { - ready.push(needed); - } - } - //remove task from 'running' list: - let i = running.indexOf(task); - console.assert(i !== -1, "running tasks must exist within running list"); - running.splice(i,1); - } - - //ready up anything that can be: - for (const task of pending) { - if (task.depends.length === 0) { - ready.push(task); - } - } - - //launch tasks until no more can be launched: - await new Promise((resolve,reject) => { - function pollTasks() { - //if can run something now, do so: - while (running.length < maek.JOBS && !CANCEL_ALL_TASKS && ready.length > 0) { - launch(ready.shift()); - } - //if can run something eventually, keep waiting: - if (running.length > 0 || (!CANCEL_ALL_TASKS && ready.length > 0)) { - setTimeout(pollTasks, 10); - } else { - resolve(); //otherwise, finish - } - } - setImmediate(pollTasks); - }); - - //confirm that nothing was left hanging (dependency loop!): - let failed = false; - let skipped = []; - for (const task of pending) { - if (!(task.finished || task.failed)) { - skipped.push(task.label); - } - if (!task.finished) { - failed = true; - } - } - - const after = performance.now(); - if (!failed) { - console.log(` -- SUCCESS: Target(s) '${targets.join("', '")}' brought up to date in ${((after - before) / 1000.0).toFixed(3)} seconds.`); - } else { - if (skipped.length) { - if (CANCEL_ALL_TASKS) { - console.log(`!!! SKIPPED ${skipped.length} tasks because of failure above.`); - } else { - console.log(`\x1b[91m!!! FAILED: tasks ${skipped.join(', ')} were never run (circular dependancy).\x1b[0m`); - } - } else { - console.log(`\x1b[91m!!! FAILED: see error(s) above.\x1b[0m`); - } - } - - //store cache to disk: - saveCache(); - - if (maek.VERBOSE) { - function t(ms) { return (ms / 1000.0).toFixed(3); } - console.log(`\x1b[35m -- Performance metrics:\x1b[0m`); - console.log(`\x1b[35m . hashCache ended up with ${Object.keys(hashCache).length} items and handled ${hashCacheHits} hits.\x1b[0m`); - console.log(`\x1b[35m . hashFiles spent ${t(hashLoadTime)} seconds loading and ${t(hashComputeTime)} hashing.\x1b[0m`); - console.log(`\x1b[35m . run spent ${t(runTime)} seconds running commands.\x1b[0m`); - } - - return !failed; - }; - - - //automatically call 'update' once the main body of the script has finished running: - process.nextTick(() => { - //parse the command line: - let targets = []; - for (let argi = 2; argi < process.argv.length; ++argi) { - const arg = process.argv[argi]; - if (arg === '--') { //-- target target ... - //the rest of the command line is targets: - targets.push(...process.argv.slice(argi + 1)); - break; - } else if (/^-j\d+$/.test(arg)) { //-jN - //set max jobs - maek.JOBS = parseInt(arg.substr(2)); - } else if (arg === '-v') { - //set verbose output - maek.VERBOSE = true; - } else if (arg === '-q') { - //set quit on on first error: - maek.QUIT_EAGERLY = true; - } else if (arg.startsWith('-')) { //unrecognized option - console.error(`Unrecognized option '${arg}'.`); - process.exit(1); - } else if (!arg.startsWith('-')) { //a target name - console.log(`Added target ${arg}.`); - targets.push(arg); - } - } - if (targets.length !== 0) { - maek.TARGETS = targets; - } - if (maek.TARGETS.length === 0) { - console.warn("No targets specified on command line and no default targets."); - } - - maek.update(maek.TARGETS).then((success) => { - process.exitCode = (success ? 0 : 1); - }); - }); - - return maek; + //---------------------------------- + //some setup + + //standard libraries: + const path = require('path').posix; //NOTE: expect posix-style paths even on windows + const fsPromises = require('fs').promises; + const fs = require('fs'); + const os = require('os'); + const performance = require('perf_hooks').performance; + const child_process = require('child_process'); + + //make it so that all paths/commands are relative to this file: + // (regardless of where you run it from) + console.log(`Building in ${__dirname}.`); + process.chdir(__dirname); + + //make it slightly more idiomatic to export: + const maek = module.exports; + + //----------------------------------------- + //Constants: + + //cache file location: + maek.CACHE_FILE = 'maek-cache.json'; + + //current OS: (with slightly nicer naming than os.platform() + maek.OS = (() => { + const platform = os.platform(); + if (platform === 'win32') return 'windows'; + else if (platform === 'darwin') return 'macos'; + else if (platform === 'linux') return 'linux'; + else { + console.error(`ERROR: Unrecognized platform ${os.platform()}.`); + process.exit(1); + } + })(); + + //----------------------------------------- + //Command line defaults: + + //maximum number of jobs to run: (change with -jN) + maek.JOBS = os.cpus().length + 1; + + //targets to build by default: (change by passing target names) + maek.TARGETS = []; + + //print extra info: (set by passing -v) + maek.VERBOSE = false; + + //quit on first failure: (set by passing -q) + maek.QUIT_EAGERLY = false; + + //----------------------------------------- + //options: set to change maek rule behavior + + const DEFAULT_OPTIONS = { + objPrefix: 'objs/', //prefix for object file paths (if not explicitly specified) + objSuffix: (maek.OS === 'windows' ? '.obj' : '.o'), //suffix for object files + exeSuffix: (maek.OS === 'windows' ? '.exe' : ''), //suffix for executable files + depends: [], //extra dependencies; generally only set locally + CPP: [], //the c++ compiler and any flags to start with (set below, per-OS) + CPPFlags: [], //extra flags for c++ compiler + LINK: [], //the linker and any flags to start with (set below, per-OS) + LINKLibs: [], //extra -L and -l flags for linker + } + + if (maek.OS === 'windows') { + DEFAULT_OPTIONS.CPP = ['cl.exe', '/nologo', '/EHsc', '/Z7', '/std:c++17', '/W4', '/WX', '/MD']; + //TODO: could embed manifest to set UTF8 codepage + DEFAULT_OPTIONS.LINK = ['link.exe', '/nologo', '/SUBSYSTEM:CONSOLE', '/DEBUG:FASTLINK', '/INCREMENTAL:NO']; + } else if (maek.OS === 'linux') { + DEFAULT_OPTIONS.CPP = ['g++', '-std=c++17', '-Wall', '-Werror', '-g']; + DEFAULT_OPTIONS.LINK = ['g++', '-std=c++17', '-Wall', '-Werror', '-g']; + } else if (maek.OS === 'macos') { + DEFAULT_OPTIONS.CPP = ['clang++', '-std=c++17', '-Wall', '-Werror', '-g']; + DEFAULT_OPTIONS.LINK = ['clang++', '-std=c++17', '-Wall', '-Werror', '-g']; + } + + //any settings here override 'DEFAULT_OPTIONS': + maek.options = Object.assign({}, DEFAULT_OPTIONS); //shallow copy of DEFAULT_OPTIONS in case you want to console.log(maek.options) to check settings. + + //this combines DEFAULT_OPTIONS, maek.options, and localOptions: + function combineOptions(localOptions) { + //shallow copy of default options: + const combined = Object.assign({}, DEFAULT_OPTIONS); + //override with maek.options + complain on missing keys: + for (const key of Object.keys(maek.options)) { + if (!(key in combined)) throw new Error(`ERROR: '${key}' (in maek.options) not recognized.`); + combined[key] = maek.options[key]; + } + //override with localOptions + complain on missing keys: + for (const key of Object.keys(localOptions)) { + if (!(key in combined)) throw new Error(`ERROR: '${key}' (in local options) not recognized.`); + combined[key] = localOptions[key]; + } + return combined; + } + + //tasks is a map from targets -> tasks: + maek.tasks = {}; + + //----------------------------------------- + //RULES. + // helper functions that specify tasks: + + //COPY adds a task that copies a file: + maek.COPY = (srcFile, dstFile) => { + if (typeof srcFile !== "string") throw new Error("COPY: from should be a single file."); + if (typeof dstFile !== "string") throw new Error("COPY: to should be a single file."); + const task = async () => { + try { + await fsPromises.mkdir(path.dirname(dstFile), {recursive: true}); + await fsPromises.copyFile(srcFile, dstFile); + } catch (e) { + throw new BuildError(`Failed to copy '${srcFile}' to '${dstFile}': ${e}`); + } + }; + task.depends = [srcFile]; + task.label = `COPY ${dstFile}`; + maek.tasks[dstFile] = task; + + return dstFile; + }; + + + //maek.CPP makes an object from a c++ source file: + // cppFile is the source file name + // objFileBase (optional) is the output file (including any subdirectories, but not the extension) + maek.CPP = (cppFile, objFileBase, localOptions = {}) => { + //combine options: + const options = combineOptions(localOptions); + + //if objFileBase isn't given, compute by trimming extension from cppFile and appending to objPrefix: + if (typeof objFileBase === 'undefined') { + objFileBase = path.relative('', options.objPrefix + cppFile.replace(/\.[^.]*$/, '')); + } + + //object file gets os-dependent suffix: + const objFile = objFileBase + options.objSuffix; + + //computed dependencies go in a '.d' file stored next to the object file: + const depsFile = objFileBase + '.d'; + + let cc, command; + cc = [...options.CPP, ...options.CPPFlags]; + if (maek.OS === 'linux') { + command = [...cc, '-MD', '-MT', 'x ', '-MF', depsFile, '-c', '-o', objFile, cppFile]; + } else if (maek.OS === 'macos') { + command = [...cc, '-MD', '-MT', 'x ', '-MF', depsFile, '-c', '-o', objFile, cppFile]; + } else { //windows + command = [...cc, '/c', `/Fo${objFile}`, '/sourceDependencies', depsFile, '/Tp', cppFile]; + } + + //will be used by loadDeps to trim explicit dependencies: + async function loadDeps() { + const text = await fsPromises.readFile(depsFile, {encoding: 'utf8'}); + + if (maek.OS === 'windows') { + //parse JSON-encoded dependency info from /sourceDependencies: + const winpath = require('path').win32; + const parsed = JSON.parse(text); + let paths = [...parsed.Data.Includes, parsed.Data.Source]; + paths = paths.map(path => winpath.relative('', path).split('\\').join('/')); + paths = paths.sort(); + return paths; + } else { + //parse the makefile-style "targets : prerequisites" line from the file into a list of tokens: + let tokens = text + .replace(/\\?\n/g, ' ') //escaped newline (or regular newline) => whitespace + .trim() //remove leading and trailing whitespace + .replace(/([^\\])\s+/g, '$1\n') //run of non-escaped whitespace => single newline + .split('\n'); //split on single newlines + + //because of the `-MT 'x '` option, expect 'x :' at the start of the rule: + console.assert(tokens[0] === 'x'); + console.assert(tokens[1] === ':'); + tokens = tokens.slice(2); //remove the 'x :' + //tokens = tokens.map(path => path.relative('', path)); //hmmm does this do anything worthwhile? + tokens = tokens.sort(); //sort for consistency + + //NOTE: might want to do some path normalization here! + return tokens; + } + } + + //The actual build task: + const task = async () => { + //make object file: + await fsPromises.mkdir(path.dirname(objFile), {recursive: true}); + await fsPromises.mkdir(path.dirname(depsFile), {recursive: true}); + await run(command, `${task.label}: compile + prerequisites`, + async () => { + return { + read: [...await loadDeps()], + written: [objFile, depsFile] + }; + } + ); + }; + + task.depends = [cppFile, ...options.depends]; + + task.label = `CPP ${objFile}`; + + if (objFile in maek.tasks) { + throw new Error(`Task ${task.label} purports to create ${objFile}, but ${maek.tasks[objFile].label} already creates that file.`); + } + maek.tasks[objFile] = task; + + return objFile; + }; + + //maek.LINK links an executable file from a collection of object files: + // objFiles is an array of object file names + // exeFileBase is the base name of the executable file ('.exe' will be added on windows) + maek.LINK = (objFiles, exeFileBase, localOptions = {}) => { + const options = combineOptions(localOptions); + + const exeFile = exeFileBase + options.exeSuffix; + + let link, linkCommand; + link = [...options.LINK]; + if (maek.OS === 'linux') { + linkCommand = [...link, '-o', exeFile, ...objFiles, ...options.LINKLibs]; + } else if (maek.OS === 'macos') { + linkCommand = [...link, '-o', exeFile, ...objFiles, ...options.LINKLibs]; + } else { + linkCommand = [...link, `/out:${exeFile}`, ...objFiles, ...options.LINKLibs]; + } + + const task = async () => { + await fsPromises.mkdir(path.dirname(exeFile), {recursive: true}); + await run(linkCommand, `${task.label}: link`, + async () => { + return { + read: [...objFiles], + written: [exeFile] + }; + } + ); + }; + + task.depends = [...objFiles, ...options.depends]; + task.label = `LINK ${exeFile}`; + + if (exeFile in maek.tasks) { + throw new Error(`Task ${task.label} purports to create ${exeFile}, but ${maek.tasks[exeFile].label} already creates that file.`); + } + maek.tasks[exeFile] = task; + + return exeFile; + }; + + + //says something went wrong in building -- should fail loudly: + class BuildError extends Error { + constructor(message) { + super(message); + } + } + + //cache stores the hashes of files involved in run()'d commands: + let cache = {}; + + function loadCache() { + try { + const loaded = JSON.parse(fs.readFileSync(maek.CACHE_FILE, {encoding: 'utf8'})); + let assigned = 0; + let removed = 0; + for (const command of Object.keys(loaded)) { + //cache will have a 'files' and a 'hashes' line + if ('files' in loaded[command] && 'hashes' in loaded[command]) { + cache[command] = { + files: loaded[command].files, + hashes: loaded[command].hashes + }; + assigned += 1; + } else { + removed += 1; + } + } + if (maek.VERBOSE) console.log(` -- Loaded cache from '${maek.CACHE_FILE}'; had ${assigned} valid entries and ${removed} invalid ones.`); + } catch (e) { + if (maek.VERBOSE) console.log(` -- No cache loaded; starting fresh.`); + if (e.code !== 'ENOENT') { + console.warn(`Cache loading failed for unexpected reason:`, e); + } + } + } + + function saveCache() { + if (maek.VERBOSE) console.log(` -- Writing cache with ${Object.keys(cache).length} entries to '${maek.CACHE_FILE}'...`); + fs.writeFileSync(maek.CACHE_FILE, JSON.stringify(cache), {encoding: 'utf8'}); + } + + let runTime = 0.0; + + //runs a shell command (presented as an array) + // 'message' will be displayed above the command + // 'cacheInfoFn', if provided, will be called after function is run to determine which files to hash when caching the result + async function run(command, message, cacheInfoFn) { + + //cache key for the command -- encoded command name: + const cacheKey = JSON.stringify(command); + + //executable for the command: + const exe = await findExe(command); + + //if no cache info function, remove any existing cache entry: + if (!cacheInfoFn) { + delete cache[cacheKey]; + } + + //check for existing cache entry: + let extra = ''; //extra message + if (cacheKey in cache) { + const cached = cache[cacheKey].hashes; + const current = await hashFiles([exe, ...cache[cacheKey].files]); + if (JSON.stringify(current) === JSON.stringify(cached)) { + if (maek.VERBOSE) console.log(`\x1b[33m${message} [cached]\x1b[0m`); + return; + } else { + if (maek.VERBOSE) extra = ` \x1b[33m[cache miss!]\x1b[0m`; + } + } + + + if (typeof message !== 'undefined') { + console.log(`\x1b[90m${message}\x1b[0m${extra}`); + } + + //print a command in a way that can be copied to a shell to run: + let prettyCommand = ''; + for (const token of command) { + if (prettyCommand !== '') prettyCommand += ' '; + if (/[ \t\n!"'$&()*,;<>?[\\\]^`{|}~]/.test(token) + || token[0] === '=' + || token[0] === '#') { + //special characters => need to quote: + prettyCommand += "'" + token.replace(/'/g, "'\\''") + "'"; + } else { + prettyCommand += token; + } + } + console.log(' ' + prettyCommand); + + //package as a promise and await it finishing: + const before = performance.now(); + await new Promise((resolve, reject) => { + const proc = child_process.spawn(command[0], command.slice(1), { + shell: false, + stdio: ['ignore', 'inherit', 'inherit'] + }); + proc.on('exit', (code, signal) => { + if (code !== 0) { + process.stderr.write(`\n`); + reject(new BuildError(`exit ${code} from:\n \x1b[31m${prettyCommand}\x1b[0m\n`)); + } else { + resolve(); + } + }); + proc.on('error', (err) => { + reject(new BuildError(`${err.message} from:\n ${prettyCommand}`)); + }); + }); + runTime += performance.now() - before; + + //store result in cache: + if (cacheInfoFn) { + const {read, written} = await cacheInfoFn(); + + //if hashed one of the written files before, can't rely on it: + for (const file of written) { + delete hashCache[file]; + } + + //update cache with file content hashes: + const files = [...read, ...written]; + cache[cacheKey] = { + files: files, + hashes: await hashFiles([exe, ...files]) + }; + } + + } + + let hashCacheHits = 0; + let hashCache = {}; + let hashLoadTime = 0.0; + let hashComputeTime = 0.0; + + //hash a list of files and return a list of strings describing said hashes (or 'x' on missing file): + async function hashFiles(files) { + const crypto = require('crypto'); + + //helper that will hash a single file: (non-existent files get special hash 'x') + async function hashFile(file) { + if (file in hashCache) { + hashCacheHits += 1; + return hashCache[file]; + } + + //would likely be more efficient to use a pipe with large files, + //but this code is a bit more readable: + const hash = await new Promise((resolve, reject) => { + const beforeLoad = performance.now(); + fs.readFile(file, (err, data) => { + hashLoadTime += performance.now() - beforeLoad; + if (err) { + //if failed to read file, report hash as 'x': + if (err.code != "ENOENT") { + console.warn(`Failed to hash ${file} because of unexpected error ${err}`); //DEBUG + } + resolve(`x`); + } else { + const beforeHash = performance.now(); + //otherwise, report base64-encoded md5sum of file data: + const hash = crypto.createHash('md5'); + hash.update(data); + resolve(`${hash.digest('base64')}`); + hashComputeTime += performance.now() - beforeHash; + } + }); + }); + + hashCache[file] = hash; + return hash; + } + + //get all hashes: + const hashes = []; + for (let file of files) { + hashes.push(await hashFile(file)); + } + return hashes; + } + + //find an executable in the system path + // (used by run to figure out what to hash) + async function findExe(command) { + const osPath = require('path'); + let PATH; + if (maek.OS === 'windows') { + PATH = process.env.PATH.split(';'); + } else { + PATH = process.env.PATH.split(':'); + } + for (const prefix of PATH) { + const exe = osPath.resolve(prefix, command[0]); + try { + await fsPromises.access(exe, fs.constants.X_OK); + return exe; + } catch (e) { + if (e.code === 'ENOENT') continue; + else throw e; + } + } + throw new BuildError(`Couldn't find file for command '${command[0]}'`); + return "?"; + } + + //--------------------------------------- + //'update' actually runs tasks to make targets: + + maek.update = async (targets) => { + const before = performance.now(); + console.log(` -- Maek v0.2 on ${maek.OS} with ${maek.JOBS} max jobs updating '${targets.join("', '")}'...`); + + loadCache(); + process.on('SIGINT', () => { + console.log(`\x1b[91m!!! FAILED: interrupted\x1b[0m`); + saveCache(); + process.exit(1); + }); //allow saving cache on abort + + const tasks = maek.tasks; + + //clear temporary per-task data: + for (const target in tasks) { + delete tasks[target].neededBy; //which tasks need this task + delete tasks[target].finished; //is this task finished? + delete tasks[target].failed; //has this task failed? + } + + + //list of all tasks to run: + const pending = []; + + //add to list of tasks to run and make neededBy array: + function need(target, from) { + if (!(target in tasks)) { + //no task for the target? + if (target[0] === ':') { + //if it's abstract, that's an error: + throw new BuildError(`Target '${target}' (requested by ${from}) is abstract but doesn't have a task.`); + } + //otherwise, it's a plain file: add a task that checks it exists: + const task = async () => { + try { + await fsPromises.access(target, fs.constants.R_OK); + } catch (e) { + throw new BuildError(`Target '${target}' (requested by ${from}) doesn't exist and doesn't have a task to make it.`); + } + }; + task.depends = []; + task.label = `EXISTS '${target}'`; + tasks[target] = task; + } + if ('neededBy' in tasks[target]) return; + pending.push(tasks[target]); + tasks[target].neededBy = []; + for (let depend of tasks[target].depends) { + need(depend, `'${target}'`); + tasks[depend].neededBy.push(tasks[target]); + } + } + + //every requested target is needed: + for (const target of targets) { + need(target, 'user'); + } + + //---------------------------------- + //now run up to JOBS tasks at once: + + let ready = []; //tasks ready to run + let running = []; //tasks currently running + let CANCEL_ALL_TASKS = false; //skip remaining tasks? + + async function launch(task) { + running.push(task); + let failedDepends = []; + for (const depend of task.depends) { + if (tasks[depend].failed) { + failedDepends.push(depend); + } else { + console.assert(tasks[depend].finished, "all depends should be failed or finished"); + } + } + if (failedDepends.length) { + task.failed = true; + if (maek.VERBOSE) console.error(`!!! SKIPPED [${task.label}] because target(s) ${failedDepends.join(', ')} failed.`); + } + try { + if (!task.failed) { + await task(); + task.finished = true; + } + } catch (e) { + if (e instanceof BuildError) { + console.error(`\x1b[91m!!! FAILED [${task.label}] ${e.message}\x1b[0m`); + task.failed = true; + //if -q flag is set, immediately cancel all jobs: + if (maek.QUIT_EAGERLY) { + CANCEL_ALL_TASKS = true; //set flag so jobs cancel themselves + } + } else { + //don't expect any other exceptions, but if they do arise, re-throw 'em: + throw e; + } + } + //check all neededBy for potential readiness: + for (const needed of task.neededBy) { + let allDone = true; + for (const depend of needed.depends) { + if (!(tasks[depend].finished || tasks[depend].failed)) { + allDone = false; + } + } + if (allDone) { + ready.push(needed); + } + } + //remove task from 'running' list: + let i = running.indexOf(task); + console.assert(i !== -1, "running tasks must exist within running list"); + running.splice(i, 1); + } + + //ready up anything that can be: + for (const task of pending) { + if (task.depends.length === 0) { + ready.push(task); + } + } + + //launch tasks until no more can be launched: + await new Promise((resolve, reject) => { + function pollTasks() { + //if can run something now, do so: + while (running.length < maek.JOBS && !CANCEL_ALL_TASKS && ready.length > 0) { + launch(ready.shift()); + } + //if can run something eventually, keep waiting: + if (running.length > 0 || (!CANCEL_ALL_TASKS && ready.length > 0)) { + setTimeout(pollTasks, 10); + } else { + resolve(); //otherwise, finish + } + } + + setImmediate(pollTasks); + }); + + //confirm that nothing was left hanging (dependency loop!): + let failed = false; + let skipped = []; + for (const task of pending) { + if (!(task.finished || task.failed)) { + skipped.push(task.label); + } + if (!task.finished) { + failed = true; + } + } + + const after = performance.now(); + if (!failed) { + console.log(` -- SUCCESS: Target(s) '${targets.join("', '")}' brought up to date in ${((after - before) / 1000.0).toFixed(3)} seconds.`); + } else { + if (skipped.length) { + if (CANCEL_ALL_TASKS) { + console.log(`!!! SKIPPED ${skipped.length} tasks because of failure above.`); + } else { + console.log(`\x1b[91m!!! FAILED: tasks ${skipped.join(', ')} were never run (circular dependancy).\x1b[0m`); + } + } else { + console.log(`\x1b[91m!!! FAILED: see error(s) above.\x1b[0m`); + } + } + + //store cache to disk: + saveCache(); + + if (maek.VERBOSE) { + function t(ms) { + return (ms / 1000.0).toFixed(3); + } + + console.log(`\x1b[35m -- Performance metrics:\x1b[0m`); + console.log(`\x1b[35m . hashCache ended up with ${Object.keys(hashCache).length} items and handled ${hashCacheHits} hits.\x1b[0m`); + console.log(`\x1b[35m . hashFiles spent ${t(hashLoadTime)} seconds loading and ${t(hashComputeTime)} hashing.\x1b[0m`); + console.log(`\x1b[35m . run spent ${t(runTime)} seconds running commands.\x1b[0m`); + } + + return !failed; + }; + + + //automatically call 'update' once the main body of the script has finished running: + process.nextTick(() => { + //parse the command line: + let targets = []; + for (let argi = 2; argi < process.argv.length; ++argi) { + const arg = process.argv[argi]; + if (arg === '--') { //-- target target ... + //the rest of the command line is targets: + targets.push(...process.argv.slice(argi + 1)); + break; + } else if (/^-j\d+$/.test(arg)) { //-jN + //set max jobs + maek.JOBS = parseInt(arg.substr(2)); + } else if (arg === '-v') { + //set verbose output + maek.VERBOSE = true; + } else if (arg === '-q') { + //set quit on on first error: + maek.QUIT_EAGERLY = true; + } else if (arg.startsWith('-')) { //unrecognized option + console.error(`Unrecognized option '${arg}'.`); + process.exit(1); + } else if (!arg.startsWith('-')) { //a target name + console.log(`Added target ${arg}.`); + targets.push(arg); + } + } + if (targets.length !== 0) { + maek.TARGETS = targets; + } + if (maek.TARGETS.length === 0) { + console.warn("No targets specified on command line and no default targets."); + } + + maek.update(maek.TARGETS).then((success) => { + process.exitCode = (success ? 0 : 1); + }); + }); + + return maek; } diff --git a/MonospaceFont.cpp b/MonospaceFont.cpp new file mode 100644 index 0000000..0d4b46f --- /dev/null +++ b/MonospaceFont.cpp @@ -0,0 +1,151 @@ +/* + * Created by Russell Emerine on 10/30/23. + * Mostly from https://learnopengl.com/In-Practice/Text-Rendering + * Image drawing code from Matei Budiu (mateib)'s Auriga (game4) + */ + +#include "gl_errors.hpp" + +#include "MonospaceFont.hpp" +#include "TexProgram.hpp" +#include "glm/gtc/type_ptr.hpp" +#include "load_save_png.hpp" + +MonospaceFont::MonospaceFont(const std::string &filename) { + glUseProgram(tex_program->program); + + glm::uvec2 size; + std::vector data; + load_png(data_path(filename), &size, &data, LowerLeftOrigin); + + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA, + size.x, + size.y, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + data.data() + ); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glBindTexture(GL_TEXTURE_2D, 0); + + GL_ERRORS(); + + // yes, integer division here + auto step = glm::uvec2(size.x / 32, size.y / 3); + for (unsigned char cc = 0; cc < 128; cc++) { + unsigned char c = cc; + if (c < ' ') { + c = ' '; + } + c -= ' '; + + auto start = glm::uvec2( + step.x * (c % 32), + step.y * (2 - c / 32) + ); + std::vector tex_coords{ + glm::vec2( + ((float) start.x + 0.2 * (float) step.x) / (float) size.x, + (float) start.y / (float) size.y + ), + glm::vec2( + ((float) start.x + 0.8 * (float) step.x) / (float) size.x, + (float) start.y / (float) size.y + ), + glm::vec2( + ((float) start.x + 0.8 * (float) step.x) / (float) size.x, + ((float) start.y + (float) step.y) / (float) size.y + ), + glm::vec2( + ((float) start.x + 0.2 * (float) step.x) / (float) size.x, + ((float) start.y + (float) step.y) / (float) size.y + ) + }; + + // generate tex_buf + GLuint tex_buf; + glGenBuffers(1, &tex_buf); + glBindBuffer(GL_ARRAY_BUFFER, tex_buf); + glBufferData(GL_ARRAY_BUFFER, tex_coords.size() * 2 * 4, tex_coords.data(), GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + GLuint vao; + glGenVertexArrays(1, &vao); + GLuint buf; + glGenBuffers(1, &buf); + + GL_ERRORS(); + + // now store character for later use + Character character = { + tex_buf, + buf, + vao, + }; + char_info.emplace(cc, character); + } + + glUseProgram(0); + + GL_ERRORS(); +} + +void MonospaceFont::draw(char c, glm::vec2 loc, glm::vec2 size) { + // I currently recalculate the vertex array each time, but it could instead be precalculated upon + // construction of the Terminal and stored there. + + // steps are multiplied by two to accound for the fact that it's [-1, 1] x [-1, 1] + std::vector positions{ + glm::vec3(loc.x, loc.y, 0.0f), + glm::vec3(loc.x + 2 * size.x, loc.y, 0.0f), + glm::vec3(loc.x + 2 * size.x, loc.y + 2 * size.y, 0.0f), + glm::vec3(loc.x, loc.y + 2 * size.y, 0.0f), + }; + + // The following code is from https://github.com/aehmttw/Auriga/blob/master/PlayMode.cpp + glUseProgram(tex_program->program); + glBindTexture(GL_TEXTURE_2D, texture); + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glBindVertexArray(char_info[c].vao); + + glBindBuffer(GL_ARRAY_BUFFER, char_info[c].buf); + glBufferData(GL_ARRAY_BUFFER, positions.size() * 3 * 4, positions.data(), GL_STATIC_DRAW); + glVertexAttribPointer(tex_program->Position_vec4, 3, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(tex_program->Position_vec4); + + glBindBuffer(GL_ARRAY_BUFFER, char_info[c].tex_buf); + glVertexAttribPointer(tex_program->TexCoord_vec2, 2, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(tex_program->TexCoord_vec2); + + glUniformMatrix4fv(tex_program->OBJECT_TO_CLIP_mat4, 1, GL_FALSE, glm::value_ptr( + glm::mat4( + 1, 0.0f, 0.0f, 0.0f, + 0.0f, 1, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + loc.x, loc.y, 0.0f, 1.0f + ))); + glUniform4f(tex_program->COLOR_vec4, 1, 1, 1, 1); + + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + glBindTexture(GL_TEXTURE_2D, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + glUseProgram(0); + + GL_ERRORS(); +} diff --git a/MonospaceFont.hpp b/MonospaceFont.hpp new file mode 100644 index 0000000..0208117 --- /dev/null +++ b/MonospaceFont.hpp @@ -0,0 +1,41 @@ +// +// Created by Russell Emerine on 10/30/23. +// +#pragma once + +#include +#include FT_FREETYPE_H +#include "GL.hpp" +#include +#include +#include +#include +#include +#include + +#include "data_path.hpp" +#include "read_write_chunk.hpp" +#include "Load.hpp" + +/* + * Represents a character's texture. + * Taken from https://learnopengl.com/In-Practice/Text-Rendering, + * but also i kinda changed everything because those features don't matter for monospace fonts. + */ +struct Character { + GLuint tex_buf; + // the following don't actually have information and are recalculated every use + GLuint buf; + GLuint vao; +}; + +struct MonospaceFont { + GLuint texture; // ID handle of the atlas texture + std::map char_info; + + explicit MonospaceFont(const std::string &filename); + + void draw(char c, glm::vec2 loc, glm::vec2 size); +}; + + diff --git a/PathFont.cpp b/PathFont.cpp index bf41a14..6f6c562 100644 --- a/PathFont.cpp +++ b/PathFont.cpp @@ -5,20 +5,22 @@ #include -PathFont::PathFont(uint32_t glyphs_, - const float *glyph_widths_, - const uint32_t *glyph_char_starts_, const uint8_t *chars_, - const uint32_t *glyph_coord_starts_, const float *coords_ - ) : glyphs(glyphs_), - glyph_widths(glyph_widths_), - glyph_char_starts(glyph_char_starts_), chars(chars_), - glyph_coord_starts(glyph_coord_starts_), coords(coords_) { - - for (uint32_t i = 0; i < glyphs; ++i) { - std::string str(reinterpret_cast< const char * >(chars + glyph_char_starts[i]), reinterpret_cast< const char * >(chars + glyph_char_starts[i+1])); - auto res = glyph_map.insert(std::make_pair(str, i)); - if (!res.second) { - std::cerr << "WARNING: ignoring duplicate glyph for '" << str << "'." << std::endl; - } - } +PathFont::PathFont( + uint32_t glyphs_, + const float *glyph_widths_, + const uint32_t *glyph_char_starts_, const uint8_t *chars_, + const uint32_t *glyph_coord_starts_, const float *coords_ +) : glyphs(glyphs_), + glyph_widths(glyph_widths_), + glyph_char_starts(glyph_char_starts_), chars(chars_), + glyph_coord_starts(glyph_coord_starts_), coords(coords_) { + + for (uint32_t i = 0; i < glyphs; ++i) { + std::string str(reinterpret_cast< const char * >(chars + glyph_char_starts[i]), + reinterpret_cast< const char * >(chars + glyph_char_starts[i + 1])); + auto res = glyph_map.insert(std::make_pair(str, i)); + if (!res.second) { + std::cerr << "WARNING: ignoring duplicate glyph for '" << str << "'." << std::endl; + } + } } diff --git a/PathFont.hpp b/PathFont.hpp index 136a29d..c35cf21 100644 --- a/PathFont.hpp +++ b/PathFont.hpp @@ -15,25 +15,27 @@ #include struct PathFont { - //meant to be intitialized with some pointers to constant data: - PathFont(uint32_t glyphs, - const float *glyph_widths, - const uint32_t *glyph_char_starts, const uint8_t *chars, - const uint32_t *glyph_coord_starts, const float *coords - ); - const uint32_t glyphs = 0; - const float *glyph_widths = nullptr; - - const uint32_t *glyph_char_starts = nullptr; //indices into 'chars' table - const uint8_t *chars = nullptr; - - const uint32_t *glyph_coord_starts = nullptr; //indices into 'coords' table - const float *coords = nullptr; - - //computed in constructor: - std::map< std::string, uint32_t > glyph_map; - - //the default font: - static PathFont font; + //meant to be intitialized with some pointers to constant data: + PathFont( + uint32_t glyphs, + const float *glyph_widths, + const uint32_t *glyph_char_starts, const uint8_t *chars, + const uint32_t *glyph_coord_starts, const float *coords + ); + + const uint32_t glyphs = 0; + const float *glyph_widths = nullptr; + + const uint32_t *glyph_char_starts = nullptr; //indices into 'chars' table + const uint8_t *chars = nullptr; + + const uint32_t *glyph_coord_starts = nullptr; //indices into 'coords' table + const float *coords = nullptr; + + //computed in constructor: + std::map glyph_map; + + //the default font: + static PathFont font; }; diff --git a/PlayMode.cpp b/PlayMode.cpp index e071b53..f8a9f9f 100644 --- a/PlayMode.cpp +++ b/PlayMode.cpp @@ -1,182 +1,665 @@ #include "PlayMode.hpp" +#include "LitColorTextureProgram.hpp" + #include "DrawLines.hpp" +#include "Mesh.hpp" +#include "Load.hpp" #include "gl_errors.hpp" #include "data_path.hpp" -#include "hex_dump.hpp" +#include "ECS/Entity.hpp" +#include "ECS/Components/EventHandler.hpp" +#include "spline.h" + #include -#include +#include #include -#include -PlayMode::PlayMode(Client &client_) : client(client_) { -} +GLuint artworld_meshes_for_lit_color_texture_program = 0; +GLuint textcube_meshes_for_lit_color_texture_program = 0; +GLuint wizard_meshes_for_lit_color_texture_program = 0; + +Load artworld_meshes(LoadTagDefault, []() -> MeshBuffer const * { + MeshBuffer const *ret = new MeshBuffer(data_path("artworld.pnct")); + artworld_meshes_for_lit_color_texture_program = ret->make_vao_for_program(lit_color_texture_program->program); + return ret; +}); + +Load wizard_meshes(LoadTagDefault, []() -> MeshBuffer const * { + MeshBuffer const *ret = new MeshBuffer(data_path("wizard.pnct")); + wizard_meshes_for_lit_color_texture_program = ret->make_vao_for_program(lit_color_texture_program->program); + return ret; +}); + +Mesh const *textFace; +Load textcube_meshes(LoadTagDefault, []() -> MeshBuffer const * { + MeshBuffer const *ret = new MeshBuffer(data_path("textcube.pnct")); + textcube_meshes_for_lit_color_texture_program = ret->make_vao_for_program(lit_color_texture_program->program); + textFace = &ret->lookup("TextFace"); + return ret; +}); -PlayMode::~PlayMode() { +Load artworld_scene(LoadTagDefault, []() -> Scene const * { + return new Scene( + data_path("artworld.scene"), + [&](Scene &scene, Scene::Transform *transform, std::string const &mesh_name) { + Mesh const &mesh = artworld_meshes->lookup(mesh_name); + + scene.drawables.emplace_back(std::make_shared(transform)); + std::shared_ptr &drawable = scene.drawables.back(); + + drawable->pipeline = lit_color_texture_program_pipeline; + + drawable->pipeline.vao = artworld_meshes_for_lit_color_texture_program; + drawable->pipeline.type = mesh.type; + drawable->pipeline.start = mesh.start; + drawable->pipeline.count = mesh.count; + drawable->wireframe_info.draw_frame = false; + drawable->wireframe_info.one_time_change = false; + }); +}); + +WalkMesh const *walkmesh = nullptr; +Load artworld_walkmeshes(LoadTagDefault, []() -> WalkMeshes const * { + auto *ret = new WalkMeshes(data_path("artworld.w")); + walkmesh = &ret->lookup("WalkMesh"); + return ret; +}); + +PlayMode::PlayMode() + : terminal(10, 30, glm::vec2(0.05f, 0.05f), glm::vec2(0.4f, 0.4f)), + scene(*artworld_scene) { + // TODO: remove this test code + std::cout << "Testing basic ECS mechanics..." << std::endl; + { + Entity a; + a.add_component([](const SDL_Event &evt, const glm::uvec2 &window_size) { + return false; + }); + } + std::cout << "Success!" << std::endl; + { + std::cout << "Testing spline" << std::endl; + glm::vec2 start(2.0, 0.0); + glm::vec2 end(0.0, 2.0); + Spline spline; + spline.set(0.0, start); + spline.set(1.0, end); + glm::vec2 query = spline.at(0.5); + assert(query.x == 1.0); + assert(query.y == 1.0); + std::cout << "spline ok" << std::endl; + } + + //create a player transform: + scene.transforms.emplace_back(); + + for (auto &t: scene.transforms) { + if (t.name == player.name) { + player.transform = &t; + } + } + //player.transform = &scene.transforms.back(); + + //create a player camera attached to a child of the player transform: + scene.transforms.emplace_back(); + scene.cameras.emplace_back(&scene.transforms.back()); + player.camera = &scene.cameras.back(); + player.camera->fovy = glm::radians(60.0f); + player.camera->near = 0.01f; + player.camera->transform->parent = player.transform; + + //default view point behind player + player.camera->transform->position = glm::vec3(-0.0f, -5.0f, 2.5f); + + //rotate camera to something pointing in way of player + // arcsin 0.1 ~ 6 degrees + player.camera->transform->rotation = glm::vec3(glm::radians(84.0f), glm::radians(0.0f), glm::radians(0.0f)); + //glm::angleAxis(glm::radians(90.0f), glm::vec3(1.0f, 0.0f, 0.0f)); + + //start player walking at nearest walk point: + player.at = walkmesh->nearest_walk_point(player.transform->position); + + //scene.transforms.emplace_back(); + //auto transform = &scene.transforms.back(); + //transform->scale *= 2.0f; + + Scene::Transform *transform = player.transform; + //transform->scale *= 2.0f; + Mesh const &mesh = wizard_meshes->lookup("wizard"); + scene.drawables.emplace_back(std::make_shared(transform)); + std::shared_ptr drawable = scene.drawables.back(); + + drawable->pipeline = lit_color_texture_program_pipeline; + + drawable->pipeline.vao = wizard_meshes_for_lit_color_texture_program; + drawable->pipeline.type = mesh.type; + drawable->pipeline.start = mesh.start; + drawable->pipeline.count = mesh.count; + + scene.transforms.emplace_back(); + transform = &scene.transforms.back(); + transform->position = glm::vec3(2.0, 2.0, 2.0); + scene.drawables.emplace_back(std::make_shared(transform)); + drawable = scene.drawables.back(); + + drawable->pipeline = lit_color_texture_program_pipeline; + drawable->pipeline.vao = textcube_meshes_for_lit_color_texture_program; + drawable->pipeline.type = textFace->type; + drawable->pipeline.start = textFace->start; + drawable->pipeline.count = textFace->count; + + + initialize_scene_metadata(); + initialize_collider("col_", artworld_meshes); + initialize_wireframe_objects("col_wire"); } -bool PlayMode::handle_event(SDL_Event const &evt, glm::uvec2 const &window_size) { +PlayMode::~PlayMode() = default; - if (evt.type == SDL_KEYDOWN) { - if (evt.key.repeat) { - //ignore repeats - } else if (evt.key.keysym.sym == SDLK_a) { - controls.left.downs += 1; - controls.left.pressed = true; - return true; - } else if (evt.key.keysym.sym == SDLK_d) { - controls.right.downs += 1; - controls.right.pressed = true; - return true; - } else if (evt.key.keysym.sym == SDLK_w) { - controls.up.downs += 1; - controls.up.pressed = true; - return true; - } else if (evt.key.keysym.sym == SDLK_s) { - controls.down.downs += 1; - controls.down.pressed = true; - return true; - } else if (evt.key.keysym.sym == SDLK_SPACE) { - controls.jump.downs += 1; - controls.jump.pressed = true; - return true; - } - } else if (evt.type == SDL_KEYUP) { - if (evt.key.keysym.sym == SDLK_a) { - controls.left.pressed = false; - return true; - } else if (evt.key.keysym.sym == SDLK_d) { - controls.right.pressed = false; - return true; - } else if (evt.key.keysym.sym == SDLK_w) { - controls.up.pressed = false; - return true; - } else if (evt.key.keysym.sym == SDLK_s) { - controls.down.pressed = false; - return true; - } else if (evt.key.keysym.sym == SDLK_SPACE) { - controls.jump.pressed = false; - return true; - } - } - - return false; +bool PlayMode::handle_event(SDL_Event const &evt, glm::uvec2 const &window_size) { + if (evt.type == SDL_KEYDOWN) { + Command command = terminal.handle_key(evt.key.keysym.sym); + if (command != Command::False) { + switch (command) { + case Command::False: + assert(false && "impossible"); + break; + case Command::True: + break; + case Command::OpenSesame: + unlock("unlock_"); + std::cout << "command was open sesame!\n"; + break; + case Command::Mirage: + update_wireframe(); + std::cout << "command was open mirage!\n"; + break; + } + return true; + } else if (evt.key.keysym.sym == SDLK_ESCAPE) { + SDL_SetRelativeMouseMode(SDL_FALSE); + return true; + } else if (evt.key.keysym.sym == SDLK_a) { + left.downs += 1; + left.pressed = true; + return true; + } else if (evt.key.keysym.sym == SDLK_d) { + right.downs += 1; + right.pressed = true; + return true; + } else if (evt.key.keysym.sym == SDLK_w) { + up.downs += 1; + up.pressed = true; + return true; + } else if (evt.key.keysym.sym == SDLK_s) { + down.downs += 1; + down.pressed = true; + return true; + } else if (evt.key.keysym.sym == SDLK_e) { + terminal.activate(); + } else if (evt.key.keysym.sym == SDLK_r) { + read.downs += 1; + read.pressed = true; + return true; + } else if (evt.key.keysym.sym == SDLK_SPACE) { + update_wireframe(); + return true; + } + } else if (evt.type == SDL_KEYUP) { + if (evt.key.keysym.sym == SDLK_a) { + left.pressed = false; + return true; + } else if (evt.key.keysym.sym == SDLK_d) { + right.pressed = false; + return true; + } else if (evt.key.keysym.sym == SDLK_w) { + up.pressed = false; + return true; + } else if (evt.key.keysym.sym == SDLK_s) { + down.pressed = false; + return true; + } else if (evt.key.keysym.sym == SDLK_r) { + read.pressed = false; + return true; + } + } else if (evt.type == SDL_MOUSEBUTTONDOWN) { + if (SDL_GetRelativeMouseMode() == SDL_FALSE) { + SDL_SetRelativeMouseMode(SDL_TRUE); + return true; + } + } else if (evt.type == SDL_MOUSEMOTION) { + if (SDL_GetRelativeMouseMode() == SDL_TRUE) { + glm::vec2 motion = glm::vec2( + evt.motion.xrel / float(window_size.y), + -evt.motion.yrel / float(window_size.y) + ); + glm::vec3 upDir = walkmesh->to_world_smooth_normal(player.at); + player.transform->rotation = + glm::angleAxis(-motion.x * player.camera->fovy, upDir) * player.transform->rotation; + + float pitch = glm::pitch(player.camera->transform->rotation); + pitch += motion.y * player.camera->fovy; + //camera looks down -z (basically at the player's feet) when pitch is at zero. + pitch = std::min(pitch, 0.95f * 3.1415926f); + pitch = std::max(pitch, 0.05f * 3.1415926f); + player.camera->transform->rotation = glm::angleAxis(pitch, glm::vec3(1.0f, 0.0f, 0.0f)); + + return true; + } + } + + return false; } void PlayMode::update(float elapsed) { + if (!animated && read.pressed) { + animated = true; + splineposition = Spline(); + splinerotation = Spline(); + splineposition.set(0.0f, player.camera->transform->position); + splineposition.set(1.0f, player.camera->transform->position); + + } + //player walking: + { + //combine inputs into a move: + constexpr float PlayerSpeed = 3.0f; + auto move = glm::vec2(0.0f); + if (left.pressed && !right.pressed) move.x = -1.0f; + if (!left.pressed && right.pressed) move.x = 1.0f; + if (down.pressed && !up.pressed) move.y = -1.0f; + if (!down.pressed && up.pressed) move.y = 1.0f; + + //make it so that moving diagonally doesn't go faster: + if (move != glm::vec2(0.0f)) move = glm::normalize(move) * PlayerSpeed * elapsed; + + //get move in world coordinate system: + glm::vec3 remain = player.transform->make_local_to_world() * glm::vec4(move.x, move.y, 0.0f, 0.0f); + + //Collision + { + auto c = scene.collider_name_map[player.name]; + bool has_collision = false; + + + // If there is collision, reverse the remain vector at the collision direction? + int idx = -1; + float overlap = std::numeric_limits::infinity(); + + + for (auto collider: scene.colliders) { + if (collider->name == player.name) { + continue; + } else { + if (c->intersect(collider)) { + has_collision = true; + // Only one collision at a time? + std::tie(idx, overlap) = c->least_collison_axis(collider); + break; + } + } + } + + if (has_collision) { + remain[idx] += overlap; + } + } + + //using a for() instead of a while() here so that if walkpoint gets stuck in + // some awkward case, code will not infinite loop: + for (uint32_t iter = 0; iter < 10; ++iter) { + if (remain == glm::vec3(0.0f)) break; + WalkPoint end; + float time; + walkmesh->walk_in_triangle(player.at, remain, &end, &time); + player.at = end; + if (time == 1.0f) { + //finished within triangle: + remain = glm::vec3(0.0f); + break; + } + //some step remains: + remain *= (1.0f - time); + //try to step over edge: + glm::quat rotation; + if (walkmesh->cross_edge(player.at, &end, &rotation)) { + //stepped to a new triangle: + player.at = end; + //rotate step to follow surface: + remain = rotation * remain; + } else { + //ran into a wall, bounce / slide along it: + glm::vec3 const &a = walkmesh->vertices[player.at.indices.x]; + glm::vec3 const &b = walkmesh->vertices[player.at.indices.y]; + glm::vec3 const &c = walkmesh->vertices[player.at.indices.z]; + glm::vec3 along = glm::normalize(b - a); + glm::vec3 normal = glm::normalize(glm::cross(b - a, c - a)); + glm::vec3 in = glm::cross(normal, along); + + //check how much 'remain' is pointing out of the triangle: + float d = glm::dot(remain, in); + if (d < 0.0f) { + //bounce off of the wall: + remain += (-1.25f * d) * in; + } else { + //if it's just pointing along the edge, bend slightly away from wall: + remain += 0.01f * d * in; + } + } + } + + if (remain != glm::vec3(0.0f)) { + std::cout << "NOTE: code used full iteration budget for walking." << std::endl; + } + + //update player's position to respect walking: + player.transform->position = walkmesh->to_world_point(player.at); + + { //update player's rotation to respect local (smooth) up-vector: + + glm::quat adjust = glm::rotation( + player.transform->rotation * glm::vec3(0.0f, 0.0f, 1.0f), //current up vector + //walkmesh->to_world_smooth_normal(player.at) //smoothed up vector at walk location + glm::vec3(0.0, 0.0, 1.0) + ); + player.transform->rotation = glm::normalize(adjust * player.transform->rotation); + } + + /* + glm::mat4x3 frame = camera->transform->make_local_to_parent(); + glm::vec3 right = frame[0]; + //glm::vec3 up = frame[1]; + glm::vec3 forward = -frame[2]; - //queue data for sending to server: - controls.send_controls_message(&client.connection); - - //reset button press counters: - controls.left.downs = 0; - controls.right.downs = 0; - controls.up.downs = 0; - controls.down.downs = 0; - controls.jump.downs = 0; - - //send/receive data: - client.poll([this](Connection *c, Connection::Event event){ - if (event == Connection::OnOpen) { - std::cout << "[" << c->socket << "] opened" << std::endl; - } else if (event == Connection::OnClose) { - std::cout << "[" << c->socket << "] closed (!)" << std::endl; - throw std::runtime_error("Lost connection to server!"); - } else { assert(event == Connection::OnRecv); - //std::cout << "[" << c->socket << "] recv'd data. Current buffer:\n" << hex_dump(c->recv_buffer); std::cout.flush(); //DEBUG - bool handled_message; - try { - do { - handled_message = false; - if (game.recv_state_message(c)) handled_message = true; - } while (handled_message); - } catch (std::exception const &e) { - std::cerr << "[" << c->socket << "] malformed message from server: " << e.what() << std::endl; - //quit the game: - throw e; - } - } - }, 0.0); + camera->transform->position += move.x * right + move.y * forward; + */ + } + + auto bbox = scene.collider_name_map[player.name]; + bbox->update_BBox(player.transform); + + //reset button press counters: + left.downs = 0; + right.downs = 0; + up.downs = 0; + down.downs = 0; } void PlayMode::draw(glm::uvec2 const &drawable_size) { + //update camera aspect ratio for drawable: + player.camera->aspect = float(drawable_size.x) / float(drawable_size.y); + + //set up light type and position for lit_color_texture_program: + // TODO: consider using the Light(s) in the scene to do this + glUseProgram(lit_color_texture_program->program); + glUniform1i(lit_color_texture_program->LIGHT_TYPE_int, 1); + glUniform3fv(lit_color_texture_program->LIGHT_DIRECTION_vec3, 1, glm::value_ptr(glm::vec3(0.0f, 0.0f, -1.0f))); + glUniform3fv(lit_color_texture_program->LIGHT_ENERGY_vec3, 1, glm::value_ptr(glm::vec3(1.0f, 1.0f, 0.95f))); + glUseProgram(0); + + glClearColor(0.5f, 0.5f, 0.5f, 1.0f); + glClearDepth(1.0f); //1.0 is actually the default value to clear the depth buffer to, but FYI you can change it. + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); //this is the default depth comparison function, but FYI you can change it. + + + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + scene.draw(*player.camera, false); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + scene.draw(*player.camera, true); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + terminal.draw(); + + GL_ERRORS(); +} + + +void PlayMode::update_wireframe() { + std::string name_to_real, name_to_wireframe; + std::shared_ptr collider_to_real = nullptr; // Add back to fully draw + std::shared_ptr collider_to_wireframe = nullptr; // draw wireframe + + // Test the frame thing? + + auto c = scene.collider_name_map[player.name]; + + if (has_paint_ability) { + // remove real object, only draw wireframe + for (auto it = wireframe_objects.begin(); it != wireframe_objects.end(); it++) { + auto collider = *it; + if (collider->name == player.name) { + continue; + } + auto dist = c->min_distance(collider); + if (dist < 0.5) { + std::string name = collider->name; + // If this is already a wireframe + if (!current_wireframe_objects_map.count(name)) { + collider_to_wireframe = collider; + name_to_wireframe = name; + break; + + } + } + } + // turn wireframe object real + if (collider_to_wireframe == nullptr) { + // Add it back + for (auto &it: current_wireframe_objects_map) { + std::string name = it.first; + auto collider = it.second; + auto dist = c->min_distance(collider); + if (dist < 0.5 && !c->intersect(collider)) { + collider_to_real = collider; + name_to_real = name; + break; + } + } + + } + } else { // Paintbrush case // This is ugly code but it works.. + for (auto it = wireframe_objects.begin(); it != wireframe_objects.end(); it++) { + auto collider = *it; + if (collider->name == player.name || collider->name.find("Paintbrush") == std::string::npos) { + continue; + } + auto dist = c->min_distance(collider); + if (dist < 0.5) { + std::string name = collider->name; + // If this is already a wireframe + if (!current_wireframe_objects_map.count(name)) { + collider_to_wireframe = collider; + name_to_wireframe = name; + has_paint_ability = true; + break; + + } + } + } + if (collider_to_wireframe == nullptr) { + // Add it back + for (auto &it: current_wireframe_objects_map) { + std::string name = it.first; + if (name.find("Paintbrush") == std::string::npos) { + continue; + } + + auto collider = it.second; + auto dist = c->min_distance(collider); + if (dist < 0.5 && !c->intersect(collider)) { + collider_to_real = collider; + name_to_real = name; + has_paint_ability = true; + break; + } + } + + } + } + + + if (collider_to_real) { + // Add back bounding box + if (wf_obj_block_map.count(name_to_real)) { + scene.colliders.push_back(collider_to_real); + } + // remove virtual bounding box + else if (wf_obj_pass_map.count(name_to_real)) { + scene.colliders.remove(collider_to_real); + } + + current_wireframe_objects_map.erase(name_to_real); + auto d = scene.drawble_name_map[name_to_real]; + // If first_time_add/remove + if (d->wireframe_info.one_time_change) { + wireframe_objects.remove(collider_to_real); + wf_obj_block_map.erase(name_to_real); + wf_obj_pass_map.erase(name_to_real); + } + d->wireframe_info.draw_frame = false; + } + + if (collider_to_wireframe) { + // remove bounding box + if (wf_obj_block_map.count(name_to_wireframe)) { + scene.colliders.remove(collider_to_wireframe); + } else if (wf_obj_pass_map.count(name_to_wireframe)) { + scene.colliders.push_back(collider_to_wireframe); + } + + + current_wireframe_objects_map[name_to_wireframe] = collider_to_wireframe; + auto d = scene.drawble_name_map[name_to_wireframe]; + // If first_time_add/remove + if (d->wireframe_info.one_time_change) { + wireframe_objects.remove(collider_to_wireframe); + wf_obj_block_map.erase(name_to_wireframe); + wf_obj_pass_map.erase(name_to_wireframe); + current_wireframe_objects_map.erase(name_to_wireframe); + } + d->wireframe_info.draw_frame = true; + } +} + + +// prefix_on(off)_(onetime)_xxxxx +// on means draw full color at first +// check if there is a prefix_on(off)_(onetime)_xxxxx_invisible +void PlayMode::initialize_wireframe_objects(std::string prefix) { + for (auto &c: scene.colliders) { + if (c->name.find(prefix) != std::string::npos) { + wireframe_objects.push_back(c); + // Only one time? + auto d = scene.drawble_name_map[c->name]; + + if (c->name.find("pass") != std::string::npos) { + //wf_obj_pass.push_back(c); + wf_obj_pass_map[c->name] = c; + } else if (c->name.find("block") != std::string::npos) { + //wf_obj_block.push_back(c); + wf_obj_block_map[c->name] = c; + } else { + std::runtime_error("Unknown type of wireframe object"); + } + + if (c->name.find("onetime") != std::string::npos) { + d->wireframe_info.one_time_change = true; + } else { + d->wireframe_info.one_time_change = false; + } + if (c->name.find("on") != std::string::npos) { + d->wireframe_info.draw_frame = false; + } else { + d->wireframe_info.draw_frame = true; + current_wireframe_objects_map[c->name] = c; + } + } + } + + // remove colliders in wf_obj_block_map && colliders is currently wireframe + // remove colliders in wf_obj_pass_map && colliders is currently real + for (auto it: wf_obj_block_map) { + auto d = scene.drawble_name_map[it.second->name]; + if (d->wireframe_info.draw_frame == true) { + scene.colliders.remove(it.second); + } + } + + for (auto it: wf_obj_pass_map) { + auto d = scene.drawble_name_map[it.second->name]; + if (d->wireframe_info.draw_frame == false) { + scene.colliders.remove(it.second); + } + } +} + +// Should be called after all drawables are loaded into the list +void PlayMode::initialize_scene_metadata() { + std::shared_ptr walkmesh_to_remove = nullptr; + for (auto d: scene.drawables) { + std::string name = d->transform->name; + if (name == "WalkMesh") { + walkmesh_to_remove = d; + } else { + scene.drawble_name_map[name] = d; + } + } + + if (walkmesh_to_remove) { + scene.drawables.remove(walkmesh_to_remove); + } +} + + +// Which mesh to lookup? +// prefix_xxxxx +void PlayMode::initialize_collider(std::string prefix, Load meshes) { + for (auto &it: meshes->meshes) { + std::string name = it.first; + auto mesh = it.second; + if (name.find(prefix) != std::string::npos || name == "Player") { + glm::vec3 min = mesh.min; + glm::vec3 max = mesh.max; + auto collider = std::make_shared(name, min, max, min, max); + auto d = scene.drawble_name_map[name]; + collider->update_BBox(d->transform); + scene.colliders.push_back(collider); + scene.collider_name_map[name] = collider; + } + } +} + - static std::array< glm::vec2, 16 > const circle = [](){ - std::array< glm::vec2, 16 > ret; - for (uint32_t a = 0; a < ret.size(); ++a) { - float ang = a / float(ret.size()) * 2.0f * float(M_PI); - ret[a] = glm::vec2(std::cos(ang), std::sin(ang)); - } - return ret; - }(); - - glClearColor(0.1f, 0.1f, 0.1f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - glDisable(GL_DEPTH_TEST); - - //figure out view transform to center the arena: - float aspect = float(drawable_size.x) / float(drawable_size.y); - float scale = std::min( - 2.0f * aspect / (Game::ArenaMax.x - Game::ArenaMin.x + 2.0f * Game::PlayerRadius), - 2.0f / (Game::ArenaMax.y - Game::ArenaMin.y + 2.0f * Game::PlayerRadius) - ); - glm::vec2 offset = -0.5f * (Game::ArenaMax + Game::ArenaMin); - - glm::mat4 world_to_clip = glm::mat4( - scale / aspect, 0.0f, 0.0f, offset.x, - 0.0f, scale, 0.0f, offset.y, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f - ); - - { - DrawLines lines(world_to_clip); - - //helper: - auto draw_text = [&](glm::vec2 const &at, std::string const &text, float H) { - lines.draw_text(text, - glm::vec3(at.x, at.y, 0.0), - glm::vec3(H, 0.0f, 0.0f), glm::vec3(0.0f, H, 0.0f), - glm::u8vec4(0x00, 0x00, 0x00, 0x00)); - float ofs = (1.0f / scale) / drawable_size.y; - lines.draw_text(text, - glm::vec3(at.x + ofs, at.y + ofs, 0.0), - glm::vec3(H, 0.0f, 0.0f), glm::vec3(0.0f, H, 0.0f), - glm::u8vec4(0xff, 0xff, 0xff, 0x00)); - }; - - lines.draw(glm::vec3(Game::ArenaMin.x, Game::ArenaMin.y, 0.0f), glm::vec3(Game::ArenaMax.x, Game::ArenaMin.y, 0.0f), glm::u8vec4(0xff, 0x00, 0xff, 0xff)); - lines.draw(glm::vec3(Game::ArenaMin.x, Game::ArenaMax.y, 0.0f), glm::vec3(Game::ArenaMax.x, Game::ArenaMax.y, 0.0f), glm::u8vec4(0xff, 0x00, 0xff, 0xff)); - lines.draw(glm::vec3(Game::ArenaMin.x, Game::ArenaMin.y, 0.0f), glm::vec3(Game::ArenaMin.x, Game::ArenaMax.y, 0.0f), glm::u8vec4(0xff, 0x00, 0xff, 0xff)); - lines.draw(glm::vec3(Game::ArenaMax.x, Game::ArenaMin.y, 0.0f), glm::vec3(Game::ArenaMax.x, Game::ArenaMax.y, 0.0f), glm::u8vec4(0xff, 0x00, 0xff, 0xff)); - - for (auto const &player : game.players) { - glm::u8vec4 col = glm::u8vec4(player.color.x*255, player.color.y*255, player.color.z*255, 0xff); - if (&player == &game.players.front()) { - //mark current player (which server sends first): - lines.draw( - glm::vec3(player.position + Game::PlayerRadius * glm::vec2(-0.5f,-0.5f), 0.0f), - glm::vec3(player.position + Game::PlayerRadius * glm::vec2( 0.5f, 0.5f), 0.0f), - col - ); - lines.draw( - glm::vec3(player.position + Game::PlayerRadius * glm::vec2(-0.5f, 0.5f), 0.0f), - glm::vec3(player.position + Game::PlayerRadius * glm::vec2( 0.5f,-0.5f), 0.0f), - col - ); - } - for (uint32_t a = 0; a < circle.size(); ++a) { - lines.draw( - glm::vec3(player.position + Game::PlayerRadius * circle[a], 0.0f), - glm::vec3(player.position + Game::PlayerRadius * circle[(a+1)%circle.size()], 0.0f), - col - ); - } - - draw_text(player.position + glm::vec2(0.0f, -0.1f + Game::PlayerRadius), player.name, 0.09f); - } - } - GL_ERRORS(); +// Item to unlock must be a collider +void PlayMode::unlock(std::string prefix) { + + auto c = scene.collider_name_map[player.name]; + + std::shared_ptr collider_to_remove = nullptr; + std::string name_to_remove; + + for (auto collider: scene.colliders) { + if (collider->name.find(prefix) != std::string::npos) { + auto dist = c->min_distance(collider); + if (dist < 2.0) { + collider_to_remove = collider; + name_to_remove = collider->name; + break; + } + } else { + continue; + } + } + // Remove it from drawables and collider datastructure + auto d = scene.drawble_name_map[name_to_remove]; + scene.drawables.remove(d); + scene.drawble_name_map.erase(name_to_remove); + scene.colliders.remove(collider_to_remove); + scene.collider_name_map.erase(name_to_remove); } diff --git a/PlayMode.hpp b/PlayMode.hpp index de1ee65..4e64933 100644 --- a/PlayMode.hpp +++ b/PlayMode.hpp @@ -1,7 +1,11 @@ #include "Mode.hpp" -#include "Connection.hpp" -#include "Game.hpp" +#include "Scene.hpp" +#include "WalkMesh.hpp" +#include "Load.hpp" +#include "Mesh.hpp" +#include "Terminal.hpp" +#include "spline.h" #include @@ -9,26 +13,65 @@ #include struct PlayMode : Mode { - PlayMode(Client &client); - virtual ~PlayMode(); - - //functions called by main loop: - virtual bool handle_event(SDL_Event const &, glm::uvec2 const &window_size) override; - virtual void update(float elapsed) override; - virtual void draw(glm::uvec2 const &drawable_size) override; - - //----- game state ----- - - //input tracking for local player: - Player::Controls controls; - - //latest game state (from server): - Game game; - - //last message from server: - std::string server_message; - - //connection to server: - Client &client; - + PlayMode(); + + ~PlayMode() override; + + //functions called by main loop: + bool handle_event(SDL_Event const &, glm::uvec2 const &window_size) override; + + void update(float elapsed) override; + + void draw(glm::uvec2 const &drawable_size) override; + + //----- game state ----- + + Terminal terminal; + + //input tracking: + struct Button { + uint8_t downs = 0; + uint8_t pressed = 0; + } left, right, down, up, read; + // camera animation + bool animated = false; + Spline splineposition, splinerotation; + + //local copy of the game scene (so code can change it during gameplay): + Scene scene; + + //player info: + struct Player { + WalkPoint at; + //transform is at player's feet and will be yawed by mouse left/right motion: + Scene::Transform *transform = nullptr; + //camera is at player's head and will be pitched by mouse up/down motion: + Scene::Camera *camera = nullptr; + + //other metadata + std::string name = "Player"; + } player; + + // Wireframe logics + bool has_paint_ability = false; + std::list> wireframe_objects; + std::unordered_map> current_wireframe_objects_map; + //std::list> wf_obj_pass; // Object on walkmesh, blocked by invisible bbox when it's wireframe + std::unordered_map> wf_obj_pass_map; + //std::list> wf_obj_block; // Normal object, blocked when it's real by bbox + std::unordered_map> wf_obj_block_map; + + void update_wireframe(); + + void initialize_wireframe_objects(std::string prefix); + + + // Unlock logics(for open sesame) + void unlock(std::string prefix); + + + //initilization functions + void initialize_scene_metadata(); + + void initialize_collider(std::string prefix_pattern, Load meshes); }; diff --git a/Scene.cpp b/Scene.cpp index 636ca0b..cc04796 100644 --- a/Scene.cpp +++ b/Scene.cpp @@ -80,19 +80,24 @@ glm::mat4 Scene::Camera::make_projection() const { //------------------------- -void Scene::draw(Camera const &camera) const { +void Scene::draw(Camera const &camera, bool draw_frame ) const { assert(camera.transform); glm::mat4 world_to_clip = camera.make_projection() * glm::mat4(camera.transform->make_world_to_local()); glm::mat4x3 world_to_light = glm::mat4x3(1.0f); - draw(world_to_clip, world_to_light); + draw(world_to_clip, world_to_light, draw_frame); } -void Scene::draw(glm::mat4 const &world_to_clip, glm::mat4x3 const &world_to_light) const { +void Scene::draw(glm::mat4 const &world_to_clip, glm::mat4x3 const &world_to_light, bool draw_frame) const { //Iterate through all drawables, sending each one to OpenGL: for (auto const &drawable : drawables) { + if (drawable->wireframe_info.draw_frame != draw_frame){ + continue; + } + + //Reference to drawable's pipeline for convenience: - Scene::Drawable::Pipeline const &pipeline = drawable.pipeline; + Scene::Drawable::Pipeline const &pipeline = drawable->pipeline; //skip any drawables without a shader program set: if (pipeline.program == 0) continue; @@ -111,8 +116,12 @@ void Scene::draw(glm::mat4 const &world_to_clip, glm::mat4x3 const &world_to_lig //Configure program uniforms: //the object-to-world matrix is used in all three of these uniforms: - assert(drawable.transform); //drawables *must* have a transform - glm::mat4x3 object_to_world = drawable.transform->make_local_to_world(); + assert(drawable->transform); //drawables *must* have a transform + glm::mat4x3 object_to_world = drawable->transform->make_local_to_world(); + + if(pipeline.draw_frame != -1U){ + glUniform1i(pipeline.draw_frame,draw_frame); + } //OBJECT_TO_CLIP takes vertices from object space to clip space: if (pipeline.OBJECT_TO_CLIP_mat4 != -1U) { @@ -360,7 +369,7 @@ void Scene::set(Scene const &other, std::unordered_map< Transform const *, Trans //copy other's drawables, updating transform pointers: drawables = other.drawables; for (auto &d : drawables) { - d.transform = transform_to_transform.at(d.transform); + d->transform = transform_to_transform.at(d->transform); } //copy other's cameras, updating transform pointers: @@ -375,3 +384,174 @@ void Scene::set(Scene const &other, std::unordered_map< Transform const *, Trans l.transform = transform_to_transform.at(l.transform); } } + + + +// Collision code +bool Scene::Collider::intersect(std::shared_ptr c){ + if ( + min.x <= c->max.x && + max.x >= c->min.x && + min.y <= c->max.y && + max.y >= c->min.y && + min.z <= c->max.z && + max.z >= c->min.z + ) + return true; + else + return false; + +} + + +bool Scene::Collider::point_intersect(glm::vec3 p){ + if ( + p.x > min.x && + p.x < max.x && + p.y > min.y && + p.y < max.y && + p.z > min.z && + p.z < max.z + ) + return true; + else + return false; +} + + +std::vector Scene::Collider::get_vertices(){ + std::vector xs {min_original.x, max_original.x}; + std::vector ys {min_original.y, max_original.y}; + std::vector zs {min_original.z, max_original.z}; + + std::vector ret; + + for (uint8_t i = 0; i < xs.size(); i++){ + for (uint8_t j = 0; j < ys.size(); j++){ + for (uint8_t k = 0; k < zs.size(); k++){ + glm::vec3 tmp = glm::vec3(xs[i],ys[j],zs[k]); + ret.push_back(tmp); + } + } + } + + return ret; +} + + +void Scene::Collider::update_BBox(Scene::Transform * t){ + + std::vector current_vertices = get_vertices(); + //std::vector new_vertices; + + std::vector xs; + std::vector ys; + std::vector zs; + + auto trans = t->make_local_to_world(); + + for (auto v : current_vertices){ + auto newVertex = trans * glm::vec4{v,1}; + xs.push_back(newVertex.x); + ys.push_back(newVertex.y); + zs.push_back(newVertex.z); + } + + // Make it axis-aligned again + float minX = std::numeric_limits::max(); + float minY = minX; + float minZ = minX; + + float maxX = -std::numeric_limits::max(); + float maxY = maxX; + float maxZ = maxX; + + assert(xs.size() == ys.size() && ys.size() == zs.size()); + for (uint8_t i = 0; i < xs.size(); i++){ + if (xs[i] < minX){ + minX = xs[i]; + } + if (xs[i] > maxX){ + maxX = xs[i]; + } + if (ys[i] < minY){ + minY = ys[i]; + } + if (ys[i] > maxY){ + maxY = ys[i]; + } + if (zs[i] < minZ){ + minZ = zs[i]; + } + if (zs[i] > maxZ){ + maxZ = zs[i]; + } + } + + min.x = minX; min.y = minY; min.z = minZ; + max.x = maxX; max.y = maxY; max.z = maxZ; + + +} + +// https://stackoverflow.com/questions/65107289/minimum-distance-between-two-axis-aligned-boxes-in-n-dimensions +float Scene::Collider::min_distance(std::shared_ptr c){ + auto delta1 = min - c->max; + auto delta2 = c->min - max; + + glm::vec3 u,v; + + u.x = std::max(delta1.x,0.0f); + u.y = std::max(delta1.y,0.0f); + u.z = std::max(delta1.z,0.0f); + v.x = std::max(delta2.x,0.0f); + v.y = std::max(delta2.y,0.0f); + v.z = std::max(delta2.z,0.0f); + + float distance = std::sqrt(u.x * u.x + u.y * u.y + u.z * u.z + v.x * v.x + v.y * v.y + v.z * v.z); + + return distance; +} + +// Find the overlapped distance of three axis +//https://stackoverflow.com/questions/16691524/calculating-the-overlap-distance-of-two-1d-line-segments +std::pair Scene::Collider::least_collison_axis(std::shared_ptr c){ + + auto overlap = [](float min1,float max1, float min2, float max2)->float{ + return std::max(0.0f,std::min(max1,max2) - std::max(min1,min2)); + }; + + + float min_overlap = std::numeric_limits::infinity(); + int idx = -1; + float m1 = 0.0; + float m2 = 0.0; + + for(auto i = 0; i < 3; i++){ + float min1 = min[i]; + float max1 = max[i]; + float min2 = c->min[i]; + float max2 = c->max[i]; + + auto tmp = overlap(min1,max1,min2,max2); + + // Should not have these situations + assert(tmp >= 0.0); + + if (tmp < min_overlap){ + m1 = max1; + m2 = max2; + min_overlap = tmp; + idx = i; + } + } + + + //assert(!(min[idx] > c->min[idx] && max[idx] < c->max[idx])); + //assert(!(min[idx] < c->min[idx] && max[idx] > c->max[idx])); + if (m1 < m2){ + min_overlap = -min_overlap; + } + + return std::make_pair(idx,min_overlap); +} \ No newline at end of file diff --git a/Scene.hpp b/Scene.hpp index bd55410..68a5e62 100644 --- a/Scene.hpp +++ b/Scene.hpp @@ -50,6 +50,12 @@ struct Scene { }; struct Drawable { + struct{ + bool draw_frame = false; + bool one_time_change = false; + } wireframe_info; + + //a 'Drawable' attaches attribute data to a transform: Drawable(Transform *transform_) : transform(transform_) { assert(transform); } Transform * transform; @@ -70,6 +76,8 @@ struct Scene { GLuint OBJECT_TO_LIGHT_mat4x3 = -1U; //uniform location for object to light space (== world space) matrix GLuint NORMAL_TO_LIGHT_mat3 = -1U; //uniform location for normal to light space (== world space) matrix + GLuint draw_frame = -1U; + std::function< void() > set_uniforms; //(optional) function to set any other useful uniforms //texture objects to bind for the first TextureCount textures: @@ -116,17 +124,60 @@ struct Scene { float spot_fov = glm::radians(45.0f); //spot cone fov (in radians) }; + + struct Collider{ + + std::string name; + + Collider(std::string name, glm::vec3 min, glm::vec3 max, glm::vec3 min_o, glm::vec3 max_o): min_original(min_o),max_original(max_o) { + this->min = min; + this->max = max; + this->name = name; + } + + const glm::vec3 min_original = glm::vec3( std::numeric_limits< float >::infinity()); + const glm::vec3 max_original = glm::vec3( std::numeric_limits< float >::infinity()); + + glm::vec3 min = glm::vec3( std::numeric_limits< float >::infinity()); + glm::vec3 max = glm::vec3(-std::numeric_limits< float >::infinity()); + + + bool intersect(Collider c); + bool intersect(std::shared_ptr c); + + bool point_intersect(glm::vec3 point); + + std::vector get_vertices(); + + void update_BBox(Transform * t); + + + float min_distance(std::shared_ptr c); + + // Should only be called when there is a collision + std::pair least_collison_axis(std::shared_ptr c); + + }; + + //Scenes, of course, may have many of the above objects: std::list< Transform > transforms; - std::list< Drawable > drawables; + std::list< std::shared_ptr> drawables; std::list< Camera > cameras; std::list< Light > lights; + + std::unordered_map> drawble_name_map; + std::list< std::shared_ptr > colliders; + std::unordered_map> collider_name_map; + + + //The "draw" function provides a convenient way to pass all the things in a scene to OpenGL: - void draw(Camera const &camera) const; + void draw(Camera const &camera, bool draw_frame = false) const; //..sometimes, you want to draw with a custom projection matrix and/or light space: - void draw(glm::mat4 const &world_to_clip, glm::mat4x3 const &world_to_light = glm::mat4x3(1.0f)) const; + void draw(glm::mat4 const &world_to_clip, glm::mat4x3 const &world_to_light = glm::mat4x3(1.0f), bool draw_frame = false) const; //add transforms/objects/cameras from a scene file to this scene: // the 'on_drawable' callback gives your code a chance to look up mesh data and make Drawables: diff --git a/ShowMeshesMode.cpp b/ShowMeshesMode.cpp index b21a014..92ae2af 100644 --- a/ShowMeshesMode.cpp +++ b/ShowMeshesMode.cpp @@ -19,8 +19,8 @@ ShowMeshesMode::ShowMeshesMode(MeshBuffer const &buffer_) : buffer(buffer_) { } { //create a drawable to hold the current mesh: scene.transforms.emplace_back(); - scene.drawables.emplace_back(&scene.transforms.back()); - scene_drawable = &scene.drawables.back(); + scene.drawables.emplace_back(new Scene::Drawable(&scene.transforms.back())); + scene_drawable = scene.drawables.back(); scene_drawable->pipeline = show_meshes_program_pipeline; scene_drawable->pipeline.vao = vao; diff --git a/ShowMeshesMode.hpp b/ShowMeshesMode.hpp index 153404b..c5a3781 100644 --- a/ShowMeshesMode.hpp +++ b/ShowMeshesMode.hpp @@ -43,5 +43,5 @@ struct ShowMeshesMode : Mode { //mode uses a small Scene to arrange things for viewing: Scene scene; Scene::Camera *scene_camera = nullptr; - Scene::Drawable *scene_drawable = nullptr; + std::shared_ptr scene_drawable = nullptr; }; diff --git a/Terminal.cpp b/Terminal.cpp new file mode 100644 index 0000000..aa903dc --- /dev/null +++ b/Terminal.cpp @@ -0,0 +1,115 @@ +// +// Created by Russell Emerine on 10/30/23. +// + +#include "GL.hpp" +#include "Terminal.hpp" + +Terminal::Terminal(size_t rows, size_t cols, glm::vec2 loc, glm::vec2 size) + : font("UbuntuMono.png"), rows(rows), cols(cols), loc(loc), size(size) { + assert(rows > 0); + assert(cols > 0); + + /* + This should be here but for now there isn't a way to guarantee that it comes last. + + add_component([this](const SDL_Event &evt, const glm::uvec2 &window_size) { + if (evt.type == SDL_KEYDOWN) { + return handle_key(evt.key.keysym.sym); + } else { + return false; + } + }); + */ +} + +void Terminal::activate() { + active = true; +} + +void Terminal::deactivate() { + active = false; +} + +Command Terminal::handle_key(SDL_Keycode key) { + if (!active) return Command::False; + + std::string keyname(SDL_GetKeyName(key)); + std::locale locale("C"); + if (key == SDLK_ESCAPE) { + deactivate(); + return Command::True; + } else if (key == SDLK_BACKSPACE) { + if (!text.empty() && !text.back().empty()) { + text.back().pop_back(); + } + return Command::True; + } else if (key == SDLK_RETURN) { + Command command = Command::True; + if (!text.empty()) { + if (text.back() == "open sesame") { + command = Command::OpenSesame; + text.emplace_back("opening..."); + if (text.size() > rows) { + text.erase(text.begin()); + } + } else if (text.back() == "mirage") { + command = Command::Mirage; + text.emplace_back("activating illusion magic..."); + if (text.size() > rows) { + text.erase(text.begin()); + } + } else if (!text.back().empty()) { + text.emplace_back("invalid command"); + if (text.size() > rows) { + text.erase(text.begin()); + } + } + } + + text.emplace_back(); + if (text.size() > rows) { + text.erase(text.begin()); + } + return command; + } else if (key == SDLK_SPACE) { + char c = ' '; + if (!text.empty() && text.back().size() < cols) { + text.back().push_back(c); + } + return Command::True; + } else if (keyname.size() == 1 && std::isgraph(keyname[0], locale)) { + char c = std::tolower(keyname[0], locale); + if (!text.empty() && text.back().size() < cols) { + text.back().push_back(c); + } + return Command::True; + } + + return Command::False; +} + +void Terminal::draw() { + if (!active) return; + + // lol this works + font.draw(' ', loc, size); + + for (size_t row = 0; row < rows; row++) { + for (size_t col = 0; col < cols; col++) { + char c = ' '; + if (row < text.size() && col < text[row].size()) { + c = text[row][col]; + } + auto char_size = glm::vec2(size.x / (float) cols, size.y / (float) rows); + font.draw( + c, + glm::vec2( + loc.x + (float) col * char_size.x, + loc.y + size.y - (float) row * char_size.y + ), + char_size + ); + } + } +} diff --git a/Terminal.hpp b/Terminal.hpp new file mode 100644 index 0000000..57a9495 --- /dev/null +++ b/Terminal.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "ECS/Entity.hpp" +#include "MonospaceFont.hpp" + +/* + * The possible commands that can be entered into the terminal + */ +enum struct Command { + False = 0, // only falsey value, means handle_key did not process the input + True, // means handle_key did process the input but there's nothing else interesting to report + OpenSesame, + Mirage +}; + +// TODO: in the future, maybe make "reacts to a terminal command" be a component and have the Terminal call a related system + +/* + * An on-screen Terminal + */ +struct Terminal : Entity { + MonospaceFont font; + + bool active = false; + + std::vector text = {""}; + + size_t rows, cols; + glm::vec2 loc, size; + + /* + * Construct a Terminal of as many rows and columns of characters + * at the specified location on screen (bottom left corner, coordinates in [-1, 1] x [-1, 1]) + * with the specified display size (as fraction of [-1, 1] x [-1, 1]). + */ + Terminal(size_t rows, size_t cols, glm::vec2 loc, glm::vec2 size); + + void activate(); + + void deactivate(); + + /* + * Handle a key event + */ + Command handle_key(SDL_Keycode key); + + /* + * Draw the Terminal. This should come last, after other drawing. + */ + void draw(); +}; diff --git a/TexProgram.cpp b/TexProgram.cpp new file mode 100644 index 0000000..cea0f80 --- /dev/null +++ b/TexProgram.cpp @@ -0,0 +1,84 @@ +// From Matei Budiu (mateib)'s Auriga (game4) +#include "TexProgram.hpp" + +#include "gl_compile_program.hpp" +#include "gl_errors.hpp" + +Scene::Drawable::Pipeline tex_program_pipeline; + +Load tex_program(LoadTagEarly, []() -> TexProgram const * { + TexProgram *ret = new TexProgram(); + + //----- build the pipeline template ----- + tex_program_pipeline.program = ret->program; + + //make a 1-pixel white texture to bind by default: + GLuint tex; + glGenTextures(1, &tex); + + glBindTexture(GL_TEXTURE_2D, tex); + std::vector tex_data(1, glm::u8vec4(0xff)); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, tex_data.data()); + 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_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glBindTexture(GL_TEXTURE_2D, 0); + + tex_program_pipeline.textures[0].texture = tex; + tex_program_pipeline.textures[0].target = GL_TEXTURE_2D; + + return ret; +}); + +// Inspired by Jim McCann's message in the course Discord on how to create a textured quad: +// https://discord.com/channels/1144815260629479486/1154543452520984686/1156347836888256582 +TexProgram::TexProgram() { + //Compile vertex and fragment shaders using the convenient 'gl_compile_program' helper function: + program = gl_compile_program( + //vertex shader: + "#version 330\n" + "uniform mat4 OBJECT_TO_CLIP;\n" + "uniform vec4 COLOR;\n" + "in vec4 Position;\n" + "in vec2 TexCoord;\n" + "out vec2 texCoord;\n" + "void main() {\n" + " gl_Position = OBJECT_TO_CLIP * Position;\n" + " texCoord = TexCoord;\n" + "}\n", + //fragment shader: + "#version 330\n" + "uniform sampler2D TEX;\n" + "uniform vec4 COLOR;\n" + "in vec2 texCoord;\n" + "out vec4 fragColor;\n" + "void main() {\n" + " fragColor = texture(TEX, texCoord) * COLOR;\n" + "}\n" + ); + //As you can see above, adjacent strings in C/C++ are concatenated. + //this is very useful for writing long shader programs inline. + + //look up the locations of vertex attributes: + Position_vec4 = glGetAttribLocation(program, "Position"); + TexCoord_vec2 = glGetAttribLocation(program, "TexCoord"); + + //look up the locations of uniforms: + COLOR_vec4 = glGetUniformLocation(program, "COLOR"); + OBJECT_TO_CLIP_mat4 = glGetUniformLocation(program, "OBJECT_TO_CLIP"); + GLuint TEX_sampler2D = glGetUniformLocation(program, "TEX"); + + //set TEX to always refer to texture binding zero: + glUseProgram(program); //bind program -- glUniform* calls refer to this program now + + glUniform1i(TEX_sampler2D, 0); //set TEX to sample from GL_TEXTURE0 + + glUseProgram(0); //unbind program -- glUniform* calls refer to ??? now +} + +TexProgram::~TexProgram() { + glDeleteProgram(program); + program = 0; +} + diff --git a/TexProgram.hpp b/TexProgram.hpp new file mode 100644 index 0000000..14c16d4 --- /dev/null +++ b/TexProgram.hpp @@ -0,0 +1,34 @@ +// From Matei Budiu (mateib)'s Auriga (game4) +#pragma once + +#include "GL.hpp" +#include "Load.hpp" +#include "Scene.hpp" + +// Inspired by Jim McCann's message in the course Discord on how to create a textured quad: +// https://discord.com/channels/1144815260629479486/1154543452520984686/1156347836888256582 + +//Shader program that draws transformed, lit, textured vertices tinted with vertex colors: +struct TexProgram { + TexProgram(); + ~TexProgram(); + + GLuint program = 0; + + //Attribute (per-vertex variable) locations: + GLuint Position_vec4 = -1U; + GLuint TexCoord_vec2 = -1U; + + //Uniform (per-invocation variable) locations: + GLuint OBJECT_TO_CLIP_mat4 = -1U; + GLuint COLOR_vec4 = -1U; + + //Textures: + //TEXTURE0 - texture that is accessed by TexCoord +}; + +extern Load tex_program; + +//For convenient scene-graph setup, copy this object: +// NOTE: by default, has texture bound to 1-pixel white texture -- so it's okay to use with vertex-color-only meshes. +extern Scene::Drawable::Pipeline tex_program_pipeline; diff --git a/WalkMesh.cpp b/WalkMesh.cpp new file mode 100644 index 0000000..a3b2959 --- /dev/null +++ b/WalkMesh.cpp @@ -0,0 +1,315 @@ +#include "WalkMesh.hpp" + +#include "read_write_chunk.hpp" + +#include +#include + +#include +#include +#include +#include + +/* + * File from Russell Emerine (remerine)'s game6, original citations were as follows: + * + * The following code was mostly written by myself, but checked against + * Michael Stroucken (stroucki) and + * Sirui (Ray) Huang (siruih)'s implementations from class, + * as well as Nellie Tonev (ntonev)'s implementation + */ + +WalkMesh::WalkMesh(std::vector const &vertices_, std::vector const &normals_, + std::vector const &triangles_) + : vertices(vertices_), normals(normals_), triangles(triangles_) { + + //construct next_vertex map (maps each edge to the next vertex in the triangle): + next_vertex.reserve(triangles.size() * 3); + auto do_next = [this](uint32_t a, uint32_t b, uint32_t c) { + auto ret = next_vertex.insert(std::make_pair(glm::uvec2(a, b), c)); + assert(ret.second); + }; + for (auto const &tri: triangles) { + do_next(tri.x, tri.y, tri.z); + do_next(tri.y, tri.z, tri.x); + do_next(tri.z, tri.x, tri.y); + } + + //DEBUG: are vertex normals consistent with geometric normals? + for (auto const &tri: triangles) { + glm::vec3 const &a = vertices[tri.x]; + glm::vec3 const &b = vertices[tri.y]; + glm::vec3 const &c = vertices[tri.z]; + glm::vec3 out = glm::normalize(glm::cross(b - a, c - a)); + + float da = glm::dot(out, normals[tri.x]); + float db = glm::dot(out, normals[tri.y]); + float dc = glm::dot(out, normals[tri.z]); + + assert(da > 0.1f && db > 0.1f && dc > 0.1f); + } +} + +//project pt to the plane of triangle a,b,c and return the barycentric weights of the projected point: +glm::vec3 barycentric_weights(glm::vec3 const &a, glm::vec3 const &b, glm::vec3 const &c, glm::vec3 const &pt) { + glm::vec3 abh = glm::cross(glm::cross(c - a, b - a), b - a); + glm::vec3 bch = glm::cross(glm::cross(a - b, c - b), c - b); + glm::vec3 cah = glm::cross(glm::cross(b - c, a - c), a - c); + float hab = glm::dot(pt - a, abh) / glm::length(abh); + float hbc = glm::dot(pt - b, bch) / glm::length(bch); + float hca = glm::dot(pt - c, cah) / glm::length(cah); + glm::vec3 w = glm::vec3( + glm::length(c - b) * hbc, + glm::length(a - c) * hca, + glm::length(b - a) * hab + ); + return w / (w[0] + w[1] + w[2]); +} + +WalkPoint WalkMesh::nearest_walk_point(glm::vec3 const &world_point) const { + assert(!triangles.empty() && "Cannot start on an empty walkmesh"); + + WalkPoint closest; + float closest_dis2 = std::numeric_limits::infinity(); + + for (auto const &tri: triangles) { + //find closest point on triangle: + + glm::vec3 const &a = vertices[tri.x]; + glm::vec3 const &b = vertices[tri.y]; + glm::vec3 const &c = vertices[tri.z]; + + //get barycentric coordinates of closest point in the plane of (a,b,c): + glm::vec3 coords = barycentric_weights(a, b, c, world_point); + + //is that point inside the triangle? + if (coords.x >= 0.0f && coords.y >= 0.0f && coords.z >= 0.0f) { + //yes, point is inside triangle. + float dis2 = glm::length2(world_point - to_world_point(WalkPoint(tri, coords))); + if (dis2 < closest_dis2) { + closest_dis2 = dis2; + closest.indices = tri; + closest.weights = coords; + } + } else { + //check triangle vertices and edges: + auto check_edge = [&world_point, &closest, &closest_dis2, this](uint32_t ai, uint32_t bi, uint32_t ci) { + glm::vec3 const &a = vertices[ai]; + glm::vec3 const &b = vertices[bi]; + + //find closest point on line segment ab: + float along = glm::dot(world_point - a, b - a); + float max = glm::dot(b - a, b - a); + glm::vec3 pt; + glm::vec3 coords; + if (along < 0.0f) { + pt = a; + coords = glm::vec3(1.0f, 0.0f, 0.0f); + } else if (along > max) { + pt = b; + coords = glm::vec3(0.0f, 1.0f, 0.0f); + } else { + float amt = along / max; + pt = glm::mix(a, b, amt); + coords = glm::vec3(1.0f - amt, amt, 0.0f); + } + + float dis2 = glm::length2(world_point - pt); + if (dis2 < closest_dis2) { + closest_dis2 = dis2; + closest.indices = glm::uvec3(ai, bi, ci); + closest.weights = coords; + } + }; + check_edge(tri.x, tri.y, tri.z); + check_edge(tri.y, tri.z, tri.x); + check_edge(tri.z, tri.x, tri.y); + } + } + assert(closest.indices.x < vertices.size()); + assert(closest.indices.y < vertices.size()); + assert(closest.indices.z < vertices.size()); + return closest; +} + + +void WalkMesh::walk_in_triangle(WalkPoint const &start, glm::vec3 const &step, WalkPoint *end_, float *time_) const { + assert(end_); + auto &end = *end_; + + assert(time_); + auto &time = *time_; + + auto &a = vertices[start.indices.x]; + auto &b = vertices[start.indices.y]; + auto &c = vertices[start.indices.z]; + glm::vec3 bary_step = barycentric_weights(a, b, c, to_world_point(start) + step) - start.weights; + + // if no edge is crossed, event will just be taking the whole step: + time = 1.0f; + end = start; + + // figure out which edge (if any) is crossed first. + // set time and end appropriately. + int crossed_edge = -1; + for (int i = 0; i < 3; i++) { + float t = -start.weights[i] / bary_step[i]; + if (bary_step[i] < 0 && t < time) { + time = t; + crossed_edge = i; + } + } + end = WalkPoint(start.indices, start.weights + time * bary_step); + + // Remember: our convention is that when a WalkPoint is on an edge, + // then at.weights.z == 0.0f (so will likely need to re-order the indices) + switch (crossed_edge) { + case 0: + end = WalkPoint( + glm::uvec3(end.indices.y, end.indices.z, end.indices.x), + glm::vec3(end.weights.y, end.weights.z, 0.0f) + ); + break; + case 1: + end = WalkPoint( + glm::uvec3(end.indices.z, end.indices.x, end.indices.y), + glm::vec3(end.weights.z, end.weights.x, 0.0f) + ); + break; + case 2: + // just in case + end.weights.z = 0.0f; + break; + default: + break; + } +} + +bool WalkMesh::cross_edge(WalkPoint const &start, WalkPoint *end_, glm::quat *rotation_) const { + assert(end_); + auto &end = *end_; + + assert(rotation_); + auto &rotation = *rotation_; + + assert(start.weights.z == 0.0f); //*must* be on an edge. + + //check if 'edge' is a non-boundary edge: + auto pair = next_vertex.find(glm::uvec2(start.indices.y, start.indices.x)); + if (pair != next_vertex.end()) { + //make 'end' represent the same (world) point, but on triangle (edge.y, edge.x, [other point]): + end = WalkPoint( + glm::uvec3( + start.indices.y, + start.indices.x, + pair->second + ), + glm::vec3( + start.weights.y, + start.weights.x, + 0.0f + ) + ); + + //make 'rotation' the rotation that takes (start.indices)'s normal to (end.indices)'s normal: + glm::vec3 start_norm = glm::normalize(glm::cross( + vertices[start.indices.x] - vertices[start.indices.z], + vertices[start.indices.y] - vertices[start.indices.z] + )); + glm::vec3 end_norm = glm::normalize(glm::cross( + vertices[end.indices.x] - vertices[end.indices.z], + vertices[end.indices.y] - vertices[end.indices.z] + )); + + rotation = glm::rotation(start_norm, end_norm); + + return true; + } else { + end = start; + rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + return false; + } +} + + +WalkMeshes::WalkMeshes(std::string const &filename) { + std::ifstream file(filename, std::ios::binary); + + std::vector vertices; + read_chunk(file, "p...", &vertices); + + std::vector normals; + read_chunk(file, "n...", &normals); + + std::vector triangles; + read_chunk(file, "tri0", &triangles); + + std::vector names; + read_chunk(file, "str0", &names); + + struct IndexEntry { + uint32_t name_begin, name_end; + uint32_t vertex_begin, vertex_end; + uint32_t triangle_begin, triangle_end; + }; + + std::vector index; + read_chunk(file, "idxA", &index); + + if (file.peek() != EOF) { + std::cerr << "WARNING: trailing data in walkmesh file '" << filename << "'" << std::endl; + } + + //----------------- + + if (vertices.size() != normals.size()) { + throw std::runtime_error("Mis-matched position and normal sizes in '" + filename + "'"); + } + + for (auto const &e: index) { + if (!(e.name_begin <= e.name_end && e.name_end <= names.size())) { + throw std::runtime_error("Invalid name indices in index of '" + filename + "'"); + } + if (!(e.vertex_begin <= e.vertex_end && e.vertex_end <= vertices.size())) { + throw std::runtime_error("Invalid vertex indices in index of '" + filename + "'"); + } + if (!(e.triangle_begin <= e.triangle_end && e.triangle_end <= triangles.size())) { + throw std::runtime_error("Invalid triangle indices in index of '" + filename + "'"); + } + + //copy vertices/normals: + std::vector wm_vertices(vertices.begin() + e.vertex_begin, vertices.begin() + e.vertex_end); + std::vector wm_normals(normals.begin() + e.vertex_begin, normals.begin() + e.vertex_end); + + //remap triangles: + std::vector wm_triangles; + wm_triangles.reserve(e.triangle_end - e.triangle_begin); + for (uint32_t ti = e.triangle_begin; ti != e.triangle_end; ++ti) { + if (!((e.vertex_begin <= triangles[ti].x && triangles[ti].x < e.vertex_end) + && (e.vertex_begin <= triangles[ti].y && triangles[ti].y < e.vertex_end) + && (e.vertex_begin <= triangles[ti].z && triangles[ti].z < e.vertex_end))) { + throw std::runtime_error("Invalid triangle in '" + filename + "'"); + } + wm_triangles.emplace_back( + triangles[ti].x - e.vertex_begin, + triangles[ti].y - e.vertex_begin, + triangles[ti].z - e.vertex_begin + ); + } + + std::string name(names.begin() + e.name_begin, names.begin() + e.name_end); + + auto ret = meshes.emplace(name, WalkMesh(wm_vertices, wm_normals, wm_triangles)); + if (!ret.second) { + throw std::runtime_error("WalkMesh with duplicated name '" + name + "' in '" + filename + "'"); + } + } +} + +WalkMesh const &WalkMeshes::lookup(std::string const &name) const { + auto f = meshes.find(name); + if (f == meshes.end()) { + throw std::runtime_error("WalkMesh with name '" + name + "' not found."); + } + return f->second; +} diff --git a/WalkMesh.hpp b/WalkMesh.hpp new file mode 100644 index 0000000..e9c41ae --- /dev/null +++ b/WalkMesh.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include //allows the use of 'uvec2' as an unordered_map key + +#include +#include +#include + +//"WalkPoint" represents location on the WalkMesh as barycentric coordinates on a triangle: +struct WalkPoint { + //indices of current triangle (in CCW order): + glm::uvec3 indices = glm::uvec3(-1U); + //barycentric coordinates for current point: + glm::vec3 weights = glm::vec3(std::numeric_limits::quiet_NaN()); + + //NOTE: by convention, if WalkPoint is on an edge, indices/weights will be arranged so that weights.z will be 0.0. + WalkPoint(glm::uvec3 const &indices_, glm::vec3 const &weights_) : indices(indices_), weights(weights_) {} + + WalkPoint() = default; +}; + +struct WalkMesh { + //Walk mesh will keep track of triangles, vertices: + std::vector vertices; + std::vector normals; //normals for interpolated 'up' direction + std::vector triangles; //CCW-oriented + + //This "next vertex" map includes [a,b]->c, [b,c]->a, and [c,a]->b for each triangle (a,b,c), and is useful for checking what's over an edge from a given point: + std::unordered_map next_vertex; + + //Construct new WalkMesh and build next_vertex structure: + WalkMesh(std::vector const &vertices_, std::vector const &normals_, + std::vector const &triangles_); + + //used to initialize walking -- finds the closest point on the walk mesh: + // (should only need to call this at the start of a level) + WalkPoint nearest_walk_point(glm::vec3 const &world_point) const; + + + //take a step on a triangle, stopping at edges: + // if the step stays within the triangle: + // - *end will be the position after stepping + // - *remaining_step will be glm::vec3(0.0) + // if the step reaches a triangle edge: + // - *end will be a position along the edge (i.e., end->weights.z == 0.0f) + // - *remaining_step will contain the amount of step remaining + void walk_in_triangle( + WalkPoint const &start, //[in] starting location on triangle + glm::vec3 const &step, //[in] step to take (in world space). Will be projected to triangle. + WalkPoint *end, //[out] final position in triangle + float *time //[out] time at which edge is encountered, or 1.0 if whole step is within triangle + ) const; + + //traverse over a triangle edge, adjusting facing direction + // if edge is a boundary edge: + // - *end gets start + // - *rotation is the identity + // - function returns false + // if edge is an internal edge: + // - end->weights is the other triangle along start.triangle.xy + // - *rotation brings vectors in the plane of start.triangle.xyz to vectors in the plane of end->triangle.xyz + // - function returns true + bool cross_edge( + WalkPoint const &start, //[in] walkpoint on triangle edge + WalkPoint *end, //[out] end walkpoint, having crossed edge + glm::quat *rotation //[out] rotation over edge + ) const; + + //used to read back results of walking: + glm::vec3 to_world_point(WalkPoint const &wp) const { + //if you were looking here for the lesson solution, well, here you go: + // (but please do make sure you understand what this is doing) + return wp.weights.x * vertices[wp.indices.x] + + wp.weights.y * vertices[wp.indices.y] + + wp.weights.z * vertices[wp.indices.z]; + } + + //read back a smoothed normal (average of vertex normals) at a walkpoint: + glm::vec3 to_world_smooth_normal(WalkPoint const &wp) const { + return glm::normalize( + wp.weights.x * normals[wp.indices.x] + + wp.weights.y * normals[wp.indices.y] + + wp.weights.z * normals[wp.indices.z] + ); + } + + //read back a triangle normal at a walkpoint: + glm::vec3 to_world_triangle_normal(WalkPoint const &wp) const { + glm::vec3 const &a = vertices[wp.indices.x]; + glm::vec3 const &b = vertices[wp.indices.y]; + glm::vec3 const &c = vertices[wp.indices.z]; + return glm::normalize(glm::cross(b - a, c - a)); + } + +}; + +struct WalkMeshes { + //load a list of named WalkMeshes from a file: + explicit WalkMeshes(std::string const &filename); + + //retrieve a WalkMesh by name: + WalkMesh const &lookup(std::string const &name) const; + + //internals: + std::unordered_map meshes; +}; diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..29cb2ff --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +node Maekfile.js +./dist/game \ No newline at end of file diff --git a/client.cpp b/client.cpp deleted file mode 100644 index f15d171..0000000 --- a/client.cpp +++ /dev/null @@ -1,212 +0,0 @@ -#include "PlayMode.hpp" - -#include "Connection.hpp" -#include "Mode.hpp" -#include "Load.hpp" -#include "Sound.hpp" -#include "GL.hpp" -#include "load_save_png.hpp" - -#include - -#include -#include -#include -#include -#include - -#ifdef _WIN32 -extern "C" { uint32_t GetACP(); } -#endif -int main(int argc, char **argv) { -#ifdef _WIN32 - { //when compiled on windows, check that code page is forced to utf-8 (makes file loading/saving work right): - //see: https://docs.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page - uint32_t code_page = GetACP(); - if (code_page == 65001) { - std::cout << "Code page is properly set to UTF-8." << std::endl; - } else { - std::cout << "WARNING: code page is set to " << code_page << " instead of 65001 (UTF-8). Some file handling functions may fail." << std::endl; - } - } - - //when compiled on windows, unhandled exceptions don't have their message printed, which can make debugging simple issues difficult. - try { -#endif - //------------ command line arguments ------------ - if (argc != 3) { - std::cerr << "Usage:\n\t./client " << std::endl; - return 1; - } - - //------------ connect to server -------------- - Client client(argv[1], argv[2]); - - //------------ initialization ------------ - - //Initialize SDL library: - SDL_Init(SDL_INIT_VIDEO); - - //Ask for an OpenGL context version 3.3, core profile, enable debug: - SDL_GL_ResetAttributes(); - SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); - SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); - - //create window: - SDL_Window *window = SDL_CreateWindow( - "gp23 game6: multiplayer", //TODO: remember to set a title for your game! - SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - 1280, 720, //TODO: modify window size if you'd like - SDL_WINDOW_OPENGL - | SDL_WINDOW_RESIZABLE //uncomment to allow resizing - | SDL_WINDOW_ALLOW_HIGHDPI //uncomment for full resolution on high-DPI screens - ); - - //prevent exceedingly tiny windows when resizing: - SDL_SetWindowMinimumSize(window,100,100); - - if (!window) { - std::cerr << "Error creating SDL window: " << SDL_GetError() << std::endl; - return 1; - } - - //Create OpenGL context: - SDL_GLContext context = SDL_GL_CreateContext(window); - - if (!context) { - SDL_DestroyWindow(window); - std::cerr << "Error creating OpenGL context: " << SDL_GetError() << std::endl; - return 1; - } - - //On windows, load OpenGL entrypoints: (does nothing on other platforms) - init_GL(); - - //Set VSYNC + Late Swap (prevents crazy FPS): - if (SDL_GL_SetSwapInterval(-1) != 0) { - std::cerr << "NOTE: couldn't set vsync + late swap tearing (" << SDL_GetError() << ")." << std::endl; - if (SDL_GL_SetSwapInterval(1) != 0) { - std::cerr << "NOTE: couldn't set vsync (" << SDL_GetError() << ")." << std::endl; - } - } - - //Hide mouse cursor (note: showing can be useful for debugging): - //SDL_ShowCursor(SDL_DISABLE); - - //------------ init sound -------------- - Sound::init(); - - //------------ load assets -------------- - call_load_functions(); - - //------------ create game mode + make current -------------- - Mode::set_current(std::make_shared< PlayMode >(client)); - - //------------ main loop ------------ - - //this inline function will be called whenever the window is resized, - // and will update the window_size and drawable_size variables: - glm::uvec2 window_size; //size of window (layout pixels) - glm::uvec2 drawable_size; //size of drawable (physical pixels) - //On non-highDPI displays, window_size will always equal drawable_size. - auto on_resize = [&](){ - int w,h; - SDL_GetWindowSize(window, &w, &h); - window_size = glm::uvec2(w, h); - SDL_GL_GetDrawableSize(window, &w, &h); - drawable_size = glm::uvec2(w, h); - glViewport(0, 0, drawable_size.x, drawable_size.y); - }; - on_resize(); - - //This will loop until the current mode is set to null: - while (Mode::current) { - //every pass through the game loop creates one frame of output - // by performing three steps: - - { //(1) process any events that are pending - static SDL_Event evt; - while (SDL_PollEvent(&evt) == 1) { - //handle resizing: - if (evt.type == SDL_WINDOWEVENT && evt.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { - on_resize(); - } - //handle input: - if (Mode::current && Mode::current->handle_event(evt, window_size)) { - // mode handled it; great - } else if (evt.type == SDL_QUIT) { - Mode::set_current(nullptr); - break; - } else if (evt.type == SDL_KEYDOWN && evt.key.keysym.sym == SDLK_PRINTSCREEN) { - // --- screenshot key --- - std::string filename = "screenshot.png"; - std::cout << "Saving screenshot to '" << filename << "'." << std::endl; - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - glReadBuffer(GL_FRONT); - int w,h; - SDL_GL_GetDrawableSize(window, &w, &h); - std::vector< glm::u8vec4 > data(w*h); - glReadPixels(0,0,w,h, GL_RGBA, GL_UNSIGNED_BYTE, data.data()); - for (auto &px : data) { - px.a = 0xff; - } - save_png(filename, glm::uvec2(w,h), data.data(), LowerLeftOrigin); - } - } - if (!Mode::current) break; - } - - { //(2) call the current mode's "update" function to deal with elapsed time: - auto current_time = std::chrono::high_resolution_clock::now(); - static auto previous_time = current_time; - float elapsed = std::chrono::duration< float >(current_time - previous_time).count(); - previous_time = current_time; - - //if frames are taking a very long time to process, - //lag to avoid spiral of death: - elapsed = std::min(0.1f, elapsed); - - Mode::current->update(elapsed); - if (!Mode::current) break; - } - - { //(3) call the current mode's "draw" function to produce output: - - Mode::current->draw(drawable_size); - } - - //Wait until the recently-drawn frame is shown before doing it all again: - SDL_GL_SwapWindow(window); - } - - - //------------ teardown ------------ - Sound::shutdown(); - - SDL_GL_DeleteContext(context); - context = 0; - - SDL_DestroyWindow(window); - window = NULL; - - return 0; - -#ifdef _WIN32 - } catch (std::exception const &e) { - std::cerr << "Unhandled exception:\n" << e.what() << std::endl; - return 1; - } catch (...) { - std::cerr << "Unhandled exception (unknown type)." << std::endl; - throw; - } -#endif -} diff --git a/dist/README-fonts.txt b/dist/README-fonts.txt new file mode 100644 index 0000000..5981503 --- /dev/null +++ b/dist/README-fonts.txt @@ -0,0 +1,4 @@ +The Ubuntu Mono font uses the Ubuntu Font License, which is attached. +The .png file was created by Russell Emerine as a screenshot of +https://fonts.google.com/specimen/Ubuntu+Mono/tester?preview.text=ABCDEFGHIJKLMNOPQRSTUVWXYZ +with the text displayed in the file. diff --git a/dist/UFL.txt b/dist/UFL.txt new file mode 100644 index 0000000..6e722c8 --- /dev/null +++ b/dist/UFL.txt @@ -0,0 +1,96 @@ +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/dist/UbuntuMono.png b/dist/UbuntuMono.png new file mode 100644 index 0000000..9eae406 Binary files /dev/null and b/dist/UbuntuMono.png differ diff --git a/dist/artworld.pnct b/dist/artworld.pnct new file mode 100644 index 0000000..56ef581 Binary files /dev/null and b/dist/artworld.pnct differ diff --git a/dist/artworld.scene b/dist/artworld.scene new file mode 100644 index 0000000..654e447 Binary files /dev/null and b/dist/artworld.scene differ diff --git a/dist/artworld.w b/dist/artworld.w new file mode 100644 index 0000000..3b30041 Binary files /dev/null and b/dist/artworld.w differ diff --git a/dist/artworld_delete.pnct b/dist/artworld_delete.pnct new file mode 100644 index 0000000..d4e2cbf Binary files /dev/null and b/dist/artworld_delete.pnct differ diff --git a/dist/artworld_delete.scene b/dist/artworld_delete.scene new file mode 100644 index 0000000..3bb14e5 Binary files /dev/null and b/dist/artworld_delete.scene differ diff --git a/dist/artworld_delete.w b/dist/artworld_delete.w new file mode 100644 index 0000000..3b30041 Binary files /dev/null and b/dist/artworld_delete.w differ diff --git a/dist/textcube.pnct b/dist/textcube.pnct new file mode 100644 index 0000000..e862cae Binary files /dev/null and b/dist/textcube.pnct differ diff --git a/dist/wizard.pnct b/dist/wizard.pnct new file mode 100644 index 0000000..08413cc Binary files /dev/null and b/dist/wizard.pnct differ diff --git a/fonts/README.md b/fonts/README.md new file mode 100644 index 0000000..5981503 --- /dev/null +++ b/fonts/README.md @@ -0,0 +1,4 @@ +The Ubuntu Mono font uses the Ubuntu Font License, which is attached. +The .png file was created by Russell Emerine as a screenshot of +https://fonts.google.com/specimen/Ubuntu+Mono/tester?preview.text=ABCDEFGHIJKLMNOPQRSTUVWXYZ +with the text displayed in the file. diff --git a/fonts/UFL.txt b/fonts/UFL.txt new file mode 100644 index 0000000..6e722c8 --- /dev/null +++ b/fonts/UFL.txt @@ -0,0 +1,96 @@ +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/fonts/UbuntuMono-Bold.ttf b/fonts/UbuntuMono-Bold.ttf new file mode 100644 index 0000000..01ad81b Binary files /dev/null and b/fonts/UbuntuMono-Bold.ttf differ diff --git a/fonts/UbuntuMono-BoldItalic.ttf b/fonts/UbuntuMono-BoldItalic.ttf new file mode 100644 index 0000000..731884e Binary files /dev/null and b/fonts/UbuntuMono-BoldItalic.ttf differ diff --git a/fonts/UbuntuMono-Italic.ttf b/fonts/UbuntuMono-Italic.ttf new file mode 100644 index 0000000..b89338d Binary files /dev/null and b/fonts/UbuntuMono-Italic.ttf differ diff --git a/fonts/UbuntuMono-Regular.ttf b/fonts/UbuntuMono-Regular.ttf new file mode 100644 index 0000000..4977028 Binary files /dev/null and b/fonts/UbuntuMono-Regular.ttf differ diff --git a/fonts/UbuntuMono.png b/fonts/UbuntuMono.png new file mode 100644 index 0000000..9eae406 Binary files /dev/null and b/fonts/UbuntuMono.png differ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..f5dd3ee --- /dev/null +++ b/main.cpp @@ -0,0 +1,216 @@ +//Mode.hpp declares the "Mode::current" static member variable, which is used to decide where event-handling, updating, and drawing events go: +#include "Mode.hpp" + +//The 'PlayMode' mode plays the game: +#include "PlayMode.hpp" + +//For asset loading: +#include "Load.hpp" + +//For sound init: +#include "Sound.hpp" + +//GL.hpp will include a non-namespace-polluting set of opengl prototypes: +#include "GL.hpp" + +//for screenshots: +#include "load_save_png.hpp" + +//Includes for libSDL: +#include + +//...and for c++ standard library functions: +#include +#include +#include +#include +#include + +#ifdef _WIN32 +extern "C" { uint32_t GetACP(); } +#endif + +int main(int argc, char **argv) { +#ifdef _WIN32 + { //when compiled on windows, check that code page is forced to utf-8 (makes file loading/saving work right): + //see: https://docs.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page + uint32_t code_page = GetACP(); + if (code_page == 65001) { + std::cout << "Code page is properly set to UTF-8." << std::endl; + } else { + std::cout << "WARNING: code page is set to " << code_page << " instead of 65001 (UTF-8). Some file handling functions may fail." << std::endl; + } + } + + //when compiled on windows, unhandled exceptions don't have their message printed, which can make debugging simple issues difficult. + try { +#endif + + //------------ initialization ------------ + + //Initialize SDL library: + SDL_Init(SDL_INIT_VIDEO); + + //Ask for an OpenGL context version 3.3, core profile, enable debug: + SDL_GL_ResetAttributes(); + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); + + //create window: + SDL_Window *window = SDL_CreateWindow( + "gp23 game5: walking simulator", //TODO: remember to set a title for your game! + SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + 1280, 720, //TODO: modify window size if you'd like + SDL_WINDOW_OPENGL + | SDL_WINDOW_RESIZABLE //uncomment to allow resizing + | SDL_WINDOW_ALLOW_HIGHDPI //uncomment for full resolution on high-DPI screens + ); + + //prevent exceedingly tiny windows when resizing: + SDL_SetWindowMinimumSize(window, 100, 100); + + if (!window) { + std::cerr << "Error creating SDL window: " << SDL_GetError() << std::endl; + return 1; + } + + //Create OpenGL context: + SDL_GLContext context = SDL_GL_CreateContext(window); + + if (!context) { + SDL_DestroyWindow(window); + std::cerr << "Error creating OpenGL context: " << SDL_GetError() << std::endl; + return 1; + } + + //On windows, load OpenGL entrypoints: (does nothing on other platforms) + init_GL(); + + //Set VSYNC + Late Swap (prevents crazy FPS): + if (SDL_GL_SetSwapInterval(-1) != 0) { + std::cerr << "NOTE: couldn't set vsync + late swap tearing (" << SDL_GetError() << ")." << std::endl; + if (SDL_GL_SetSwapInterval(1) != 0) { + std::cerr << "NOTE: couldn't set vsync (" << SDL_GetError() << ")." << std::endl; + } + } + + //Hide mouse cursor (note: showing can be useful for debugging): + //SDL_ShowCursor(SDL_DISABLE); + + //------------ init sound -------------- + Sound::init(); + + //------------ load assets -------------- + call_load_functions(); + + //------------ create game mode + make current -------------- + Mode::set_current(std::make_shared()); + + //------------ main loop ------------ + + //this inline function will be called whenever the window is resized, + // and will update the window_size and drawable_size variables: + glm::uvec2 window_size; //size of window (layout pixels) + glm::uvec2 drawable_size; //size of drawable (physical pixels) + //On non-highDPI displays, window_size will always equal drawable_size. + auto on_resize = [&]() { + int w, h; + SDL_GetWindowSize(window, &w, &h); + window_size = glm::uvec2(w, h); + SDL_GL_GetDrawableSize(window, &w, &h); + drawable_size = glm::uvec2(w, h); + glViewport(0, 0, drawable_size.x, drawable_size.y); + }; + on_resize(); + + //This will loop until the current mode is set to null: + while (Mode::current) { + //every pass through the game loop creates one frame of output + // by performing three steps: + + { //(1) process any events that are pending + static SDL_Event evt; + while (SDL_PollEvent(&evt) == 1) { + //handle resizing: + if (evt.type == SDL_WINDOWEVENT && evt.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + on_resize(); + } + //handle input: + if (Mode::current && Mode::current->handle_event(evt, window_size)) { + // mode handled it; great + } else if (evt.type == SDL_QUIT || (evt.type == SDL_KEYDOWN && evt.key.keysym.sym == SDLK_q)) { + Mode::set_current(nullptr); + break; + } else if (evt.type == SDL_KEYDOWN && evt.key.keysym.sym == SDLK_p) { + // --- screenshot key --- + std::string filename = "screenshot.png"; + std::cout << "Saving screenshot to '" << filename << "'." << std::endl; + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glReadBuffer(GL_FRONT); + int w, h; + SDL_GL_GetDrawableSize(window, &w, &h); + std::vector data(w * h); + glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, data.data()); + for (auto &px: data) { + px.a = 0xff; + } + save_png(filename, glm::uvec2(w, h), data.data(), LowerLeftOrigin); + } + } + if (!Mode::current) break; + } + + { //(2) call the current mode's "update" function to deal with elapsed time: + auto current_time = std::chrono::high_resolution_clock::now(); + static auto previous_time = current_time; + float elapsed = std::chrono::duration(current_time - previous_time).count(); + previous_time = current_time; + + //if frames are taking a very long time to process, + //lag to avoid spiral of death: + elapsed = std::min(0.1f, elapsed); + + Mode::current->update(elapsed); + if (!Mode::current) break; + } + + { //(3) call the current mode's "draw" function to produce output: + + Mode::current->draw(drawable_size); + } + + //Wait until the recently-drawn frame is shown before doing it all again: + SDL_GL_SwapWindow(window); + } + + + //------------ teardown ------------ + Sound::shutdown(); + + SDL_GL_DeleteContext(context); + context = nullptr; + + SDL_DestroyWindow(window); + window = nullptr; + + return 0; + +#ifdef _WIN32 + } catch (std::exception const &e) { + std::cerr << "Unhandled exception:\n" << e.what() << std::endl; + return 1; + } catch (...) { + std::cerr << "Unhandled exception (unknown type)." << std::endl; + throw; + } +#endif +} diff --git a/meshcoord.h b/meshcoord.h new file mode 100644 index 0000000..415724a --- /dev/null +++ b/meshcoord.h @@ -0,0 +1,14 @@ +#pragma once + +#include "WalkMesh.hpp" + +/** +Identify a position in the universe by + pointer to a WalkMesh + a WalkPoint within that Walkmesh +*/ + +struct MeshCoord { + WalkMesh* walkmesh; + WalkPoint walkpoint; +}; diff --git a/models/artworld.blend b/models/artworld.blend new file mode 100644 index 0000000..d3d11c6 Binary files /dev/null and b/models/artworld.blend differ diff --git a/models/artworld_scene.blend b/models/artworld_scene.blend new file mode 100644 index 0000000..4ef4fc7 Binary files /dev/null and b/models/artworld_scene.blend differ diff --git a/models/artworld_scene_delete.blend b/models/artworld_scene_delete.blend new file mode 100644 index 0000000..cfad446 Binary files /dev/null and b/models/artworld_scene_delete.blend differ diff --git a/models/bed.blend b/models/bed.blend new file mode 100644 index 0000000..f2616a9 Binary files /dev/null and b/models/bed.blend differ diff --git a/models/bed_combined.blend b/models/bed_combined.blend new file mode 100644 index 0000000..8f20b81 Binary files /dev/null and b/models/bed_combined.blend differ diff --git a/models/bucket.blend b/models/bucket.blend new file mode 100644 index 0000000..39fbb0f Binary files /dev/null and b/models/bucket.blend differ diff --git a/models/displayplate.blend b/models/displayplate.blend new file mode 100644 index 0000000..8aacdbc Binary files /dev/null and b/models/displayplate.blend differ diff --git a/models/easel.blend b/models/easel.blend new file mode 100644 index 0000000..adaf89a Binary files /dev/null and b/models/easel.blend differ diff --git a/models/eraser.blend b/models/eraser.blend new file mode 100644 index 0000000..df3a181 Binary files /dev/null and b/models/eraser.blend differ diff --git a/models/frame.blend b/models/frame.blend new file mode 100644 index 0000000..d7fcbda Binary files /dev/null and b/models/frame.blend differ diff --git a/models/houses.blend b/models/houses.blend new file mode 100644 index 0000000..9426980 Binary files /dev/null and b/models/houses.blend differ diff --git a/models/journal.blend b/models/journal.blend new file mode 100644 index 0000000..4c785e1 Binary files /dev/null and b/models/journal.blend differ diff --git a/models/paintbrush.blend b/models/paintbrush.blend new file mode 100644 index 0000000..297a409 Binary files /dev/null and b/models/paintbrush.blend differ diff --git a/models/paper.blend b/models/paper.blend new file mode 100644 index 0000000..2201761 Binary files /dev/null and b/models/paper.blend differ diff --git a/models/pencil.blend b/models/pencil.blend new file mode 100644 index 0000000..4f28a5e Binary files /dev/null and b/models/pencil.blend differ diff --git a/models/pencil_combined.blend b/models/pencil_combined.blend new file mode 100644 index 0000000..fcf0609 Binary files /dev/null and b/models/pencil_combined.blend differ diff --git a/models/popsicle_stick.blend b/models/popsicle_stick.blend new file mode 100644 index 0000000..91fbccd Binary files /dev/null and b/models/popsicle_stick.blend differ diff --git a/models/textcube.blend b/models/textcube.blend new file mode 100644 index 0000000..a00efbb Binary files /dev/null and b/models/textcube.blend differ diff --git a/models/watercolors.blend b/models/watercolors.blend new file mode 100644 index 0000000..0abe67d Binary files /dev/null and b/models/watercolors.blend differ diff --git a/models/wizard.blend b/models/wizard.blend new file mode 100644 index 0000000..89fd33a Binary files /dev/null and b/models/wizard.blend differ diff --git a/models/wizard_beta_combined.blend b/models/wizard_beta_combined.blend new file mode 100644 index 0000000..924a229 Binary files /dev/null and b/models/wizard_beta_combined.blend differ diff --git a/models/wizard_combined.blend b/models/wizard_combined.blend new file mode 100644 index 0000000..96e3f8c Binary files /dev/null and b/models/wizard_combined.blend differ diff --git a/read_write_chunk.hpp b/read_write_chunk.hpp index 837aaec..e6ae2f4 100644 --- a/read_write_chunk.hpp +++ b/read_write_chunk.hpp @@ -31,7 +31,7 @@ void read_chunk(std::istream &from, std::string const &magic, std::vector< T > * } if (header.size % sizeof(T) != 0) { - throw std::runtime_error("Size of chunk not divisible by element size"); + throw std::runtime_error("size of chunk not divisible by element size"); } to.resize(header.size / sizeof(T)); diff --git a/scenes/artworld.pnct b/scenes/artworld.pnct new file mode 100644 index 0000000..56ef581 Binary files /dev/null and b/scenes/artworld.pnct differ diff --git a/scenes/artworld.scene b/scenes/artworld.scene new file mode 100644 index 0000000..654e447 Binary files /dev/null and b/scenes/artworld.scene differ diff --git a/scenes/artworld.w b/scenes/artworld.w new file mode 100644 index 0000000..3b30041 Binary files /dev/null and b/scenes/artworld.w differ diff --git a/scenes/artworld_delete.pnct b/scenes/artworld_delete.pnct new file mode 100644 index 0000000..d4e2cbf Binary files /dev/null and b/scenes/artworld_delete.pnct differ diff --git a/scenes/artworld_delete.scene b/scenes/artworld_delete.scene new file mode 100644 index 0000000..3bb14e5 Binary files /dev/null and b/scenes/artworld_delete.scene differ diff --git a/scenes/artworld_delete.w b/scenes/artworld_delete.w new file mode 100644 index 0000000..3b30041 Binary files /dev/null and b/scenes/artworld_delete.w differ diff --git a/scenes/mesh.w b/scenes/mesh.w new file mode 100644 index 0000000..e6d97a8 Binary files /dev/null and b/scenes/mesh.w differ diff --git a/server.cpp b/server.cpp deleted file mode 100644 index 23a94e6..0000000 --- a/server.cpp +++ /dev/null @@ -1,130 +0,0 @@ - -#include "Connection.hpp" - -#include "hex_dump.hpp" - -#include "Game.hpp" - -#include -#include -#include -#include -#include - -#ifdef _WIN32 -extern "C" { uint32_t GetACP(); } -#endif -int main(int argc, char **argv) { -#ifdef _WIN32 - { //when compiled on windows, check that code page is forced to utf-8 (makes file loading/saving work right): - //see: https://docs.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page - uint32_t code_page = GetACP(); - if (code_page == 65001) { - std::cout << "Code page is properly set to UTF-8." << std::endl; - } else { - std::cout << "WARNING: code page is set to " << code_page << " instead of 65001 (UTF-8). Some file handling functions may fail." << std::endl; - } - } - - //when compiled on windows, unhandled exceptions don't have their message printed, which can make debugging simple issues difficult. - try { -#endif - - //------------ argument parsing ------------ - - if (argc != 2) { - std::cerr << "Usage:\n\t./server " << std::endl; - return 1; - } - - //------------ initialization ------------ - - Server server(argv[1]); - - //------------ main loop ------------ - - //keep track of which connection is controlling which player: - std::unordered_map< Connection *, Player * > connection_to_player; - //keep track of game state: - Game game; - - while (true) { - static auto next_tick = std::chrono::steady_clock::now() + std::chrono::duration< double >(Game::Tick); - //process incoming data from clients until a tick has elapsed: - while (true) { - auto now = std::chrono::steady_clock::now(); - double remain = std::chrono::duration< double >(next_tick - now).count(); - if (remain < 0.0) { - next_tick += std::chrono::duration< double >(Game::Tick); - break; - } - - //helper used on client close (due to quit) and server close (due to error): - auto remove_connection = [&](Connection *c) { - auto f = connection_to_player.find(c); - assert(f != connection_to_player.end()); - game.remove_player(f->second); - connection_to_player.erase(f); - }; - - server.poll([&](Connection *c, Connection::Event evt){ - if (evt == Connection::OnOpen) { - //client connected: - - //create some player info for them: - connection_to_player.emplace(c, game.spawn_player()); - - } else if (evt == Connection::OnClose) { - //client disconnected: - - remove_connection(c); - - } else { assert(evt == Connection::OnRecv); - //got data from client: - //std::cout << "current buffer:\n" << hex_dump(c->recv_buffer); std::cout.flush(); //DEBUG - - //look up in players list: - auto f = connection_to_player.find(c); - assert(f != connection_to_player.end()); - Player &player = *f->second; - - //handle messages from client: - try { - bool handled_message; - do { - handled_message = false; - if (player.controls.recv_controls_message(c)) handled_message = true; - //TODO: extend for more message types as needed - } while (handled_message); - } catch (std::exception const &e) { - std::cout << "Disconnecting client:" << e.what() << std::endl; - c->close(); - remove_connection(c); - } - } - }, remain); - } - - //update current game state - game.update(Game::Tick); - - //send updated game state to all clients - for (auto &[c, player] : connection_to_player) { - game.send_state_message(c, player); - } - - } - - - return 0; - -#ifdef _WIN32 - } catch (std::exception const &e) { - std::cerr << "Unhandled exception:\n" << e.what() << std::endl; - return 1; - } catch (...) { - std::cerr << "Unhandled exception (unknown type)." << std::endl; - throw; - } -#endif -} diff --git a/show-scene.cpp b/show-scene.cpp index d3d7c53..2b54238 100644 --- a/show-scene.cpp +++ b/show-scene.cpp @@ -111,15 +111,15 @@ int main(int argc, char **argv) { if (!buffer_vao) return; Mesh const &mesh = buffer->lookup(mesh_name); - scene.drawables.emplace_back(transform); - Scene::Drawable &drawable = scene.drawables.back(); + scene.drawables.emplace_back(new Scene::Drawable(transform)); + std::shared_ptr drawable = scene.drawables.back(); - drawable.pipeline = show_scene_program_pipeline; + drawable->pipeline = show_scene_program_pipeline; - drawable.pipeline.vao = buffer_vao; - drawable.pipeline.type = mesh.type; - drawable.pipeline.start = mesh.start; - drawable.pipeline.count = mesh.count; + drawable->pipeline.vao = buffer_vao; + drawable->pipeline.type = mesh.type; + drawable->pipeline.start = mesh.start; + drawable->pipeline.count = mesh.count; }); } catch (std::exception &e) { diff --git a/spline.cpp b/spline.cpp new file mode 100644 index 0000000..8f7a1f6 --- /dev/null +++ b/spline.cpp @@ -0,0 +1,119 @@ +// from 15-462 by Jim McCann and Michael Stroucken +#include "spline.h" + +template T Spline::at(float time) const { + + //A1T1b: Evaluate a Catumull-Rom spline + + // Given a time, find the nearest positions & tangent values + // defined by the control point map. + + // Transform them for use with cubic_unit_spline + + // Be wary of edge cases! What if time is before the first knot, + // before the second knot, etc... + + // special 1 + if (knots.empty()) { + return T(); + } + + // special 2 + if (knots.size() == 1) { + return knots.begin()->second; + } + + // special 3 + const auto& begin = knots.begin(); + float begintime = begin->first; + if (time <= begintime) { + return begin->second; + } + + // special 4 + const auto& end = std::prev(knots.end()); + float endtime = end->first; + if (time >= endtime) { + return end->second; + } + + auto k2i = knots.upper_bound(time); + T p2 = k2i->second; + float t2 = k2i->first; + auto k1i = std::prev(k2i); + T p1 = k1i->second; + float t1 = k1i->first; + T p0; + float t0; + // mirror 1 + if (k1i == knots.begin()) { + p0 = p1 - (p2 - p1); + t0 = t1 - (t2 - t1); + } else { + auto k0i = std::prev(k1i); + p0 = k0i->second; + t0 = k0i->first; + } + + T p3; + float t3; + // mirror 2 + if (k2i == std::prev(knots.end())) { + p3 = p2 + (p2 - p1); + t3 = t2 + (t2 - t1); + } else { + auto k3i = std::next(k2i); + p3 = k3i->second; + t3 = k3i->first; + } + + T m0 = (p2 - p0) / (t2 - t0); + T m1 = (p3 - p1) / (t3 - t1); + + // tangents are defined per time unit + // need to have equivalents in [0, 1] space + float localfactor = t2 - t1; + + float unittime = (time - t1) / localfactor; + return cubic_unit_spline(unittime, p1, p2, localfactor * m0, localfactor * m1); + + // return cubic_unit_spline(0.0f, T(), T(), T(), T()); +} + +template +T Spline::cubic_unit_spline(float time, const T& position0, const T& position1, + const T& tangent0, const T& tangent1) { + + //A4T1a: Hermite Curve over the unit interval + + // Given time in [0,1] compute the cubic spline coefficients and use them to compute + // the interpolated value at time 'time' based on the positions & tangents + + // Note that Spline is parameterized on type T, which allows us to create splines over + // any type that supports the * and + operators. + + //return T(); + assert(time >= 0.0f); + assert(time <= 1.0f); + float squaretime = time * time; + float cubetime = squaretime * time; + glm::mat4 hermite( + 2.0f, 1.0f, -2.0f, 1.0f, + -3.0f, -2.0f, 3.0f, -1.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, 0.0f + ); + glm::vec4 times(cubetime, squaretime, time, 1.0f); + glm::vec4 ht = hermite * times; + + T result(ht[0] * position0 + ht[1] * tangent0 + ht[2] * position1 + ht[3] * tangent1); + + return result; +} + +template class Spline; +template class Spline; +template class Spline; +template class Spline; +template class Spline; +template class Spline; diff --git a/spline.h b/spline.h new file mode 100644 index 0000000..11a99fe --- /dev/null +++ b/spline.h @@ -0,0 +1,161 @@ +// from 15-462 by Jim McCann +#pragma once + +#include +#include +#include +#include + +/** +Add times and positions to spline with set(time, position) +Obtain interpolated position with at(time) +*/ +template class Spline { +public: + + // Returns the interpolated value. + T at(float time) const; + + // Purely for convenience, returns the exact same + // value as at()---simply lets one evaluate a spline + // f as though it were a function f(t) (which it is!) + T operator()(float time) const { + return at(time); + } + + // Sets the value of the spline at a given time (i.e., knot), + // creating a new knot at this time if necessary. + void set(float time, T value) { + knots[time] = value; + } + + // Removes the knot closest to the given time + void erase(float time) { + knots.erase(time); + } + + // Checks if time t is a control point + bool has(float t) const { + return knots.count(t); + } + + // Checks if there are any control points + bool any() const { + return !knots.empty(); + } + + // Removes all control points + void clear() { + knots.clear(); + } + + // Removes control points after t + void crop(float t) { + auto e = knots.lower_bound(t); + knots.erase(e, knots.end()); + } + + // Returns set of keys + std::set keys() const { + std::set ret; + for (auto& e : knots) ret.insert(e.first); + return ret; + } + + // Given a time between 0 and 1, evaluates a cubic polynomial with + // the given endpoint and tangent values at the beginning (0) and + // end (1) of the interval + static T cubic_unit_spline(float time, const T& position0, const T& position1, + const T& tangent0, const T& tangent1); + + std::map knots; +}; + +template<> class Spline { +public: + + //Spline< Quat > uses piecewise linear interpolation: + glm::quat at(float time) const { + if (knots.empty()) return glm::quat(); + if (knots.size() == 1) return knots.begin()->second; + if (knots.begin()->first > time) return knots.begin()->second; + auto k2 = knots.upper_bound(time); + if (k2 == knots.end()) return std::prev(knots.end())->second; + auto k1 = std::prev(k2); + float t = (time - k1->first) / (k2->first - k1->first); + return slerp(k1->second, k2->second, t); + } + glm::quat operator()(float time) const { + return at(time); + } + void set(float time, glm::quat value) { + knots[time] = value; + } + void erase(float time) { + knots.erase(time); + } + std::set keys() const { + std::set ret; + for (auto& e : knots) ret.insert(e.first); + return ret; + } + bool has(float t) const { + return knots.count(t); + } + bool any() const { + return !knots.empty(); + } + void clear() { + knots.clear(); + } + void crop(float t) { + auto e = knots.lower_bound(t); + knots.erase(e, knots.end()); + } + + std::map knots; +}; + +template<> class Spline { +public: + + //Spline< bool > uses piecewise constant interpolation: + bool at(float time) const { + if (knots.empty()) return false; + if (knots.size() == 1) return knots.begin()->second; + if (knots.begin()->first > time) return knots.begin()->second; + auto k2 = knots.upper_bound(time); + if (k2 == knots.end()) return std::prev(knots.end())->second; + return std::prev(k2)->second; + } + + bool operator()(float time) const { + return at(time); + } + void set(float time, bool value) { + knots[time] = value; + } + void erase(float time) { + knots.erase(time); + } + std::set keys() const { + std::set ret; + for (auto& e : knots) ret.insert(e.first); + return ret; + } + bool has(float t) const { + return knots.count(t); + } + bool any() const { + return !knots.empty(); + } + void clear() { + knots.clear(); + } + void crop(float t) { + auto e = knots.lower_bound(t); + knots.erase(e, knots.end()); + } + + std::map knots; +};