├── .gitignore ├── AUTHORS ├── LICENSE ├── README.md ├── dub.sdl ├── libs ├── COPYING-FreeType ├── COPYING-OpenALSoft ├── COPYING-XIPH ├── COPYING-glfw3 ├── OpenAL32.dll ├── freetype.dll ├── glfw3.dll ├── libvorbis.dll ├── libvorbisfile.dll ├── ogg.lib ├── vorbis.lib └── vorbisfile.lib ├── res ├── fonts │ ├── KosugiMaru.ttf │ └── LICENSE-KosugiMaru.txt └── shaders │ ├── batch.frag │ ├── batch.vert │ ├── font.frag │ ├── font.vert │ ├── tile.frag │ └── tile.vert └── source ├── engine ├── anim │ └── package.d ├── audio │ ├── astream │ │ ├── ogg.d │ │ └── package.d │ ├── music.d │ ├── package.d │ └── sound.d ├── core │ ├── astack.d │ ├── log.d │ ├── package.d │ ├── state.d │ ├── strings.d │ └── window.d ├── game.d ├── i18n │ └── package.d ├── input │ ├── keyboard.d │ ├── mouse.d │ └── package.d ├── math │ ├── camera.d │ ├── obb.d │ ├── package.d │ └── transform.d ├── net │ └── package.d ├── package.d ├── render │ ├── batcher.d │ ├── fbo.d │ ├── package.d │ ├── shader.d │ ├── texture │ │ ├── atlas.d │ │ ├── font.d │ │ ├── package.d │ │ └── packer.d │ └── tile.d ├── ui │ ├── package.d │ ├── widget.d │ └── widgets │ │ └── label.d └── vn │ ├── character.d │ ├── dialg.d │ ├── log.d │ ├── package.d │ ├── render │ ├── dialg.d │ └── package.d │ └── script │ ├── instr.d │ └── package.d └── vorbisfile.d /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | docs/ 5 | out/ 6 | /km-engine 7 | libkm-engine.so 8 | libkm-engine.dylib 9 | libkm-engine.a 10 | km-engine.dll 11 | km-engine.lib 12 | libkm-engine-test-* 13 | *.zip 14 | *.exe 15 | *.o 16 | *.obj 17 | *.lst 18 | *.*~ 19 | dub.selections.json 20 | log.txt -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | This file lists the people who've helped create Kitsune Mahjong 2 | 3 | === Code === 4 | Luna Nielsen - Engine Programming, Game Programming, Graphics Programming 5 | 6 | === Art === 7 | Amase - Character Art -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Kitsunebi Games 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kitsune Mahjong Engine 2 | Kitsune Mahjong is a game about fox girls playing Mahjong, the game was initially going to be fully open source. 3 | For now the actual gameplay will be closed source. 4 | 5 | In this source tree you'll find the engine (and if you look back in the history you'll find a mahjong solitaire game implemented on top.) 6 | 7 | ## Development/Test Builds 8 | 9 | To get access to development builds of Kitsune Mahjong please [support Luna on Patreon](https://www.patreon.com/clipsey). 10 | You'll get access to a special set of channels for the game; including one where Luna occasionally uploads development builds. 11 | 12 | You can discuss the game and see git push logs for the engine and game on Luna's Discord Server 13 | * [English](https://discord.gg/AMpbKAB) 14 | * [日本語](https://discord.gg/Bd3makR) 15 | 16 | ## Planned Features 17 | This is an non-exhaustive list of planned features we're working on. 18 | Planned features may be subject to change. 19 | 20 | * Riichi Mahjong 21 | * CPU Opponents 22 | * Network Play 23 | * WiFi-Play[1] 24 | * 3 and 2 player Riichi variants 25 | * Mahjong Solitaire 26 | * Various Minigames using Mahjong-like tiles 27 | * Single Player Story Mode 28 | * Yuri romcom visual novel with riichi mahjong matches spliced in 29 | * Multiple supported languages 30 | * English 31 | * Japanese 32 | 33 | [1] When/if Nintendo Switch port gets completed. 34 | Development can be followed on the [キツネビ](https://twitter.com/Kitsunebi_Games) Twitter account as well as [Luna's personal twitter](https://twitter.com/Clipsey5) 35 | 36 | ## Contributing 37 | I will not be accepting any pull-requests for this repository. 38 | 39 | ## Dependencies 40 | The Kitsune Mahjong engine requires the following dependencies to be present to work: 41 | * OpenAL Driver ([OpenAL-Soft included on Windows](https://github.com/kcat/openal-soft)) 42 | * OpenGL Driver 43 | * GLFW3 44 | * libogg 45 | * libvorbis 46 | * libvorbisfile 47 | * FreeType 48 | * Kosugi Maru Font (in [`res/fonts`](/res/fonts) w/ license) 49 | 50 | On Windows these libraries are copied from the included libs/ folder. 51 | 52 | ## How to use 53 | Add `km-engine` as a dependency to your project (`dub add km-engine`) 54 | 55 | Bootstrap the engine with the following code: 56 | ```d 57 | import engine; 58 | void _init() { 59 | // Initialize your game's resources 60 | GameWindow.title = "My Game"; 61 | } 62 | 63 | void _update() { 64 | // Update and draw your game 65 | } 66 | 67 | void _border() { 68 | // Draw a border if you want to 69 | } 70 | 71 | void _postUpdate() { 72 | // Draw a border if you want to 73 | } 74 | 75 | void _cleanup() { 76 | // Clean up resources when game is requested to close. 77 | } 78 | 79 | int main() { 80 | // Sets the essential game functions 81 | gameInit = &_init; 82 | gameUpdate = &_update; 83 | gameCleanup = &_cleanup; 84 | gameBorder = &_border; 85 | gamePostUpdate = &_postUpdate; 86 | 87 | // Handle game initialization, looping and closing the engine after use. 88 | // It's recommended using a try/catch block to catch any errors that might pop up. 89 | initEngine(); 90 | startGame(vec2i(1920, 1080)); // The variable is the desired size of the game's frame buffer. 91 | closeEngine(); 92 | return 0; 93 | } 94 | ``` -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "km-engine" 2 | description "Game engine for Kitsune Mahjong" 3 | authors "Luna Nielsen" "Kitsunebi Games" 4 | copyright "Copyright © 2020, Kitsunebi Games" 5 | license "BSD 2-clause" 6 | dependency "gl3n" version="~>1.3.1" 7 | dependency "bindbc-opengl" version="~>0.13.0" 8 | dependency "bindbc-openal" version="~>0.4.1" 9 | dependency "imagefmt" version="~>2.1.0" 10 | dependency "dcontain" version="~>1.0.3" 11 | dependency "bindbc-freetype" version="~>0.9.1" 12 | dependency "sharpevents" version="~>2.0.0" 13 | dependency "bindbc-glfw" version="~>0.10.0" 14 | libs "vorbisfile" "ogg" "vorbis" 15 | versions "GL_33" "GLFW_33" 16 | stringImportPaths "res/" 17 | targetPath "out/" 18 | copyFiles "libs/COPYING-FreeType" "libs/COPYING-XIPH" "libs/COPYING-OpenALSoft" "libs/COPYING-glfw3" "res/fonts/LICENSE-KosugiMaru.txt" 19 | 20 | // For Windows 21 | sourceFiles "libs/ogg.lib" "libs/vorbisfile.lib" "libs/vorbis.lib" platform="windows" 22 | copyFiles "libs/libvorbis.dll" "libs/libvorbisfile.dll" "libs/freetype.dll" "libs/OpenAL32.dll" "libs/glfw3.dll" platform="windows" -------------------------------------------------------------------------------- /libs/COPYING-FreeType: -------------------------------------------------------------------------------- 1 | The FreeType Project LICENSE 2 | ---------------------------- 3 | 4 | 2006-Jan-27 5 | 6 | Copyright 1996-2002, 2006 by 7 | David Turner, Robert Wilhelm, and Werner Lemberg 8 | 9 | 10 | 11 | Introduction 12 | ============ 13 | 14 | The FreeType Project is distributed in several archive packages; 15 | some of them may contain, in addition to the FreeType font engine, 16 | various tools and contributions which rely on, or relate to, the 17 | FreeType Project. 18 | 19 | This license applies to all files found in such packages, and 20 | which do not fall under their own explicit license. The license 21 | affects thus the FreeType font engine, the test programs, 22 | documentation and makefiles, at the very least. 23 | 24 | This license was inspired by the BSD, Artistic, and IJG 25 | (Independent JPEG Group) licenses, which all encourage inclusion 26 | and use of free software in commercial and freeware products 27 | alike. As a consequence, its main points are that: 28 | 29 | o We don't promise that this software works. However, we will be 30 | interested in any kind of bug reports. (`as is' distribution) 31 | 32 | o You can use this software for whatever you want, in parts or 33 | full form, without having to pay us. (`royalty-free' usage) 34 | 35 | o You may not pretend that you wrote this software. If you use 36 | it, or only parts of it, in a program, you must acknowledge 37 | somewhere in your documentation that you have used the 38 | FreeType code. (`credits') 39 | 40 | We specifically permit and encourage the inclusion of this 41 | software, with or without modifications, in commercial products. 42 | We disclaim all warranties covering The FreeType Project and 43 | assume no liability related to The FreeType Project. 44 | 45 | 46 | Finally, many people asked us for a preferred form for a 47 | credit/disclaimer to use in compliance with this license. We thus 48 | encourage you to use the following text: 49 | 50 | """ 51 | Portions of this software are copyright © The FreeType 52 | Project (www.freetype.org). All rights reserved. 53 | """ 54 | 55 | Please replace with the value from the FreeType version you 56 | actually use. 57 | 58 | 59 | Legal Terms 60 | =========== 61 | 62 | 0. Definitions 63 | -------------- 64 | 65 | Throughout this license, the terms `package', `FreeType Project', 66 | and `FreeType archive' refer to the set of files originally 67 | distributed by the authors (David Turner, Robert Wilhelm, and 68 | Werner Lemberg) as the `FreeType Project', be they named as alpha, 69 | beta or final release. 70 | 71 | `You' refers to the licensee, or person using the project, where 72 | `using' is a generic term including compiling the project's source 73 | code as well as linking it to form a `program' or `executable'. 74 | This program is referred to as `a program using the FreeType 75 | engine'. 76 | 77 | This license applies to all files distributed in the original 78 | FreeType Project, including all source code, binaries and 79 | documentation, unless otherwise stated in the file in its 80 | original, unmodified form as distributed in the original archive. 81 | If you are unsure whether or not a particular file is covered by 82 | this license, you must contact us to verify this. 83 | 84 | The FreeType Project is copyright (C) 1996-2000 by David Turner, 85 | Robert Wilhelm, and Werner Lemberg. All rights reserved except as 86 | specified below. 87 | 88 | 1. No Warranty 89 | -------------- 90 | 91 | THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY 92 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 93 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 94 | PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS 95 | BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO 96 | USE, OF THE FREETYPE PROJECT. 97 | 98 | 2. Redistribution 99 | ----------------- 100 | 101 | This license grants a worldwide, royalty-free, perpetual and 102 | irrevocable right and license to use, execute, perform, compile, 103 | display, copy, create derivative works of, distribute and 104 | sublicense the FreeType Project (in both source and object code 105 | forms) and derivative works thereof for any purpose; and to 106 | authorize others to exercise some or all of the rights granted 107 | herein, subject to the following conditions: 108 | 109 | o Redistribution of source code must retain this license file 110 | (`FTL.TXT') unaltered; any additions, deletions or changes to 111 | the original files must be clearly indicated in accompanying 112 | documentation. The copyright notices of the unaltered, 113 | original files must be preserved in all copies of source 114 | files. 115 | 116 | o Redistribution in binary form must provide a disclaimer that 117 | states that the software is based in part of the work of the 118 | FreeType Team, in the distribution documentation. We also 119 | encourage you to put an URL to the FreeType web page in your 120 | documentation, though this isn't mandatory. 121 | 122 | These conditions apply to any software derived from or based on 123 | the FreeType Project, not just the unmodified files. If you use 124 | our work, you must acknowledge us. However, no fee need be paid 125 | to us. 126 | 127 | 3. Advertising 128 | -------------- 129 | 130 | Neither the FreeType authors and contributors nor you shall use 131 | the name of the other for commercial, advertising, or promotional 132 | purposes without specific prior written permission. 133 | 134 | We suggest, but do not require, that you use one or more of the 135 | following phrases to refer to this software in your documentation 136 | or advertising materials: `FreeType Project', `FreeType Engine', 137 | `FreeType library', or `FreeType Distribution'. 138 | 139 | As you have not signed this license, you are not required to 140 | accept it. However, as the FreeType Project is copyrighted 141 | material, only this license, or another one contracted with the 142 | authors, grants you the right to use, distribute, and modify it. 143 | Therefore, by using, distributing, or modifying the FreeType 144 | Project, you indicate that you understand and accept all the terms 145 | of this license. 146 | 147 | 4. Contacts 148 | ----------- 149 | 150 | There are two mailing lists related to FreeType: 151 | 152 | o freetype@nongnu.org 153 | 154 | Discusses general use and applications of FreeType, as well as 155 | future and wanted additions to the library and distribution. 156 | If you are looking for support, start in this list if you 157 | haven't found anything to help you in the documentation. 158 | 159 | o freetype-devel@nongnu.org 160 | 161 | Discusses bugs, as well as engine internals, design issues, 162 | specific licenses, porting, etc. 163 | 164 | Our home page can be found at 165 | 166 | https://www.freetype.org 167 | 168 | 169 | --- end of FTL.TXT --- 170 | -------------------------------------------------------------------------------- /libs/COPYING-XIPH: -------------------------------------------------------------------------------- 1 | This project uses libraries from the Xiph.org foundation. 2 | The following libraries are used: 3 | * libogg 4 | * libvorbis 5 | * libvorbisfile 6 | 7 | Copyright (c) 2002-2020 Xiph.org Foundation 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions 11 | are met: 12 | 13 | - Redistributions of source code must retain the above copyright 14 | notice, this list of conditions and the following disclaimer. 15 | 16 | - Redistributions in binary form must reproduce the above copyright 17 | notice, this list of conditions and the following disclaimer in the 18 | documentation and/or other materials provided with the distribution. 19 | 20 | - Neither the name of the Xiph.org Foundation nor the names of its 21 | contributors may be used to endorse or promote products derived from 22 | this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 25 | ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 26 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 27 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION 28 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 29 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 30 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 31 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 32 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 34 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | -------------------------------------------------------------------------------- /libs/COPYING-glfw3: -------------------------------------------------------------------------------- 1 | Copyright (c) 2002-2006 Marcus Geelnard 2 | 3 | Copyright (c) 2006-2019 Camilla Löwy 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would 16 | be appreciated but is not required. 17 | 18 | 2. Altered source versions must be plainly marked as such, and must not 19 | be misrepresented as being the original software. 20 | 21 | 3. This notice may not be removed or altered from any source 22 | distribution. 23 | 24 | -------------------------------------------------------------------------------- /libs/OpenAL32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunebiGames/km-engine/aed20ab650df71773df72a4b57fad43400e6b0f8/libs/OpenAL32.dll -------------------------------------------------------------------------------- /libs/freetype.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunebiGames/km-engine/aed20ab650df71773df72a4b57fad43400e6b0f8/libs/freetype.dll -------------------------------------------------------------------------------- /libs/glfw3.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunebiGames/km-engine/aed20ab650df71773df72a4b57fad43400e6b0f8/libs/glfw3.dll -------------------------------------------------------------------------------- /libs/libvorbis.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunebiGames/km-engine/aed20ab650df71773df72a4b57fad43400e6b0f8/libs/libvorbis.dll -------------------------------------------------------------------------------- /libs/libvorbisfile.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunebiGames/km-engine/aed20ab650df71773df72a4b57fad43400e6b0f8/libs/libvorbisfile.dll -------------------------------------------------------------------------------- /libs/ogg.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunebiGames/km-engine/aed20ab650df71773df72a4b57fad43400e6b0f8/libs/ogg.lib -------------------------------------------------------------------------------- /libs/vorbis.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunebiGames/km-engine/aed20ab650df71773df72a4b57fad43400e6b0f8/libs/vorbis.lib -------------------------------------------------------------------------------- /libs/vorbisfile.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunebiGames/km-engine/aed20ab650df71773df72a4b57fad43400e6b0f8/libs/vorbisfile.lib -------------------------------------------------------------------------------- /res/fonts/KosugiMaru.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunebiGames/km-engine/aed20ab650df71773df72a4b57fad43400e6b0f8/res/fonts/KosugiMaru.ttf -------------------------------------------------------------------------------- /res/fonts/LICENSE-KosugiMaru.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /res/shaders/batch.frag: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | #version 330 8 | in vec2 texUVs; 9 | in vec4 exColor; 10 | out vec4 outColor; 11 | 12 | uniform sampler2D tex; 13 | 14 | void main() { 15 | vec2 texSize = vec2(textureSize(tex, 0)); 16 | 17 | outColor = texture(tex, texUVs/texSize) * exColor; 18 | } -------------------------------------------------------------------------------- /res/shaders/batch.vert: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | #version 330 8 | uniform mat4 vp; 9 | layout(location = 0) in vec2 verts; 10 | layout(location = 1) in vec2 uvs; 11 | layout(location = 2) in vec4 color; 12 | 13 | out vec2 texUVs; 14 | out vec4 exColor; 15 | 16 | void main() { 17 | gl_Position = vp * vec4(verts.x, verts.y, 0, 1); 18 | texUVs = uvs; 19 | exColor = color; 20 | } -------------------------------------------------------------------------------- /res/shaders/font.frag: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | #version 330 8 | in vec2 texUVs; 9 | in vec4 exColor; 10 | out vec4 outColor; 11 | 12 | uniform sampler2D tex; 13 | 14 | void main() { 15 | vec2 texSize = vec2(textureSize(tex, 0)); 16 | float r = texture(tex, texUVs/texSize).r; 17 | 18 | outColor = vec4(1, 1, 1, r) * exColor; 19 | } -------------------------------------------------------------------------------- /res/shaders/font.vert: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | #version 330 8 | uniform mat4 vp; 9 | layout(location = 0) in vec2 verts; 10 | layout(location = 1) in vec2 uvs; 11 | layout(location = 2) in vec4 color; 12 | 13 | out vec2 texUVs; 14 | out vec4 exColor; 15 | 16 | void main() { 17 | gl_Position = vp * vec4(verts.x, verts.y, 0, 1); 18 | texUVs = uvs; 19 | exColor = color; 20 | } -------------------------------------------------------------------------------- /res/shaders/tile.frag: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | #version 330 8 | in vec2 texUVs; 9 | out vec4 outColor; 10 | 11 | uniform sampler2D tex; 12 | 13 | uniform bool available; 14 | 15 | void main() { 16 | outColor = texture(tex, texUVs); 17 | if (!available) { 18 | outColor = outColor * vec4(0.8, 0.8, 0.8, 1); 19 | } 20 | } -------------------------------------------------------------------------------- /res/shaders/tile.vert: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | #version 330 8 | uniform mat4 mvp; 9 | layout(location = 0) in vec3 verts; 10 | layout(location = 1) in vec2 uvs; 11 | 12 | out vec2 texUVs; 13 | 14 | void main() { 15 | gl_Position = mvp * vec4(verts.xyz, 1); 16 | texUVs = uvs; 17 | } -------------------------------------------------------------------------------- /source/engine/anim/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Animation Subsystem 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.anim; -------------------------------------------------------------------------------- /source/engine/audio/astream/ogg.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.audio.astream.ogg; 8 | import engine.audio.astream; 9 | import vorbisfile; 10 | import std.string; 11 | import engine.core.log; 12 | import std.exception; 13 | import std.typecons; 14 | 15 | /** 16 | An Ogg Vorbis audio stream 17 | */ 18 | class OggStream : AudioStream { 19 | private: 20 | string fname; 21 | OggVorbis_File file; 22 | int section; 23 | int word; 24 | int signed; 25 | 26 | int bitrate_; 27 | 28 | 29 | void verifyError(int error) { 30 | switch(error) { 31 | case OV_EREAD: throw new Exception("A read from media returned an error"); 32 | case OV_ENOTVORBIS: throw new Exception("File is not a valid ogg vorbis file"); 33 | case OV_EVERSION: throw new Exception("Vorbis version mismatch"); 34 | case OV_EBADHEADER: throw new Exception("Bad OGG Vorbis header"); 35 | case OV_EFAULT: throw new Exception("OGG Vorbis bug or stack corruption"); 36 | default: break; 37 | } 38 | } 39 | 40 | public: 41 | 42 | /** 43 | Deallocates on deconstruction 44 | */ 45 | ~this() { 46 | ov_clear(&file); 47 | } 48 | 49 | /** 50 | Opens the OGG Vorbis stream file 51 | */ 52 | this(string file, bool bit16) { 53 | this.fname = file; 54 | 55 | // Open the file and verify it opened correctly 56 | int error = ov_fopen(file.toStringz, &this.file); 57 | this.verifyError(error); 58 | 59 | // Get info about file 60 | auto info = ov_info(&this.file, -1); 61 | enforce(info.channels <= 2, "Too many channels in OGG Vorbis file"); 62 | 63 | this.bitrate_ = info.rate; 64 | 65 | // Set info about this file 66 | this.channels = info.channels; 67 | if (this.channels == 1) { 68 | this.format = bit16 ? Format.Mono16 : Format.Mono8; 69 | } else { 70 | this.format = bit16 ? Format.Stereo16 : Format.Stereo8; 71 | } 72 | word = format == Format.Stereo8 || format == Format.Mono8 ? 1 : 2; 73 | signed = format == Format.Mono16 || format == Format.Stereo16 ? 1 : 0; 74 | } 75 | 76 | ptrdiff_t iReadSamples(ref ubyte[] toArray, size_t readLength) { 77 | 78 | // Read a verify the success of the read 79 | ptrdiff_t readAmount = cast(ptrdiff_t)ov_read(&file, cast(byte*)toArray.ptr, cast(int)readLength, 0, word, signed, §ion); 80 | assert(readAmount >= 0, "An error occured trying to read from the ogg vorbis stream"); 81 | return readAmount; 82 | } 83 | 84 | override: 85 | ptrdiff_t readSamples(ref ubyte[] toArray) { 86 | ubyte[] tmpBuf = new ubyte[4096]; 87 | ptrdiff_t buffOffset; 88 | ptrdiff_t buffLength; 89 | do { 90 | buffLength = iReadSamples(tmpBuf, toArray.length-buffOffset); 91 | toArray[buffOffset..buffOffset+buffLength] = tmpBuf[0..buffLength]; 92 | buffOffset += buffLength; 93 | } while(buffOffset < toArray.length && buffLength > 0); 94 | return buffOffset; 95 | } 96 | 97 | /** 98 | Gets whether the file can be seeked 99 | */ 100 | bool canSeek() { 101 | return cast(bool)ov_seekable(&file); 102 | } 103 | 104 | /** 105 | Seek to a PCM location in the stream 106 | */ 107 | void seek(size_t location) { 108 | ov_pcm_seek(&file, location); 109 | } 110 | 111 | /** 112 | Get the position in the stream 113 | */ 114 | size_t tell() { 115 | return ov_pcm_tell(&file); 116 | } 117 | 118 | /** 119 | Gets the bitrate 120 | */ 121 | size_t bitrate() { 122 | return bitrate_; 123 | } 124 | 125 | /** 126 | Gets info about the OGG audio 127 | 128 | Only music usually uses this 129 | */ 130 | AudioInfo getInfo() { 131 | 132 | // Inline function to get the ogg comments as a D string array 133 | string[] getOggInfo() { 134 | string[] fields; 135 | 136 | // Iterate over every comment 137 | foreach(i; 0..file.vc.comments) { 138 | immutable(int) commentLength = file.vc.comment_lengths[i]; 139 | string comment = cast(string)file.vc.user_comments[i][0..commentLength]; 140 | fields ~= comment; 141 | } 142 | return fields; 143 | } 144 | 145 | // Parse ogg info as a array of key and value 146 | string[2] parseOggInfo(string info) { 147 | auto idx = info.indexOf("="); 148 | enforce(idx >= 0, "Invalid info"); 149 | return [info[0..idx], info[idx+1..$]]; 150 | } 151 | 152 | // Inline function to parse the ogg info 153 | string[string] parseOggInfos() { 154 | string[string] infos; 155 | 156 | string[] fields = getOggInfo(); 157 | foreach(i, field; fields) { 158 | try { 159 | string[2] info = parseOggInfo(field); 160 | infos[info[0]] = info[1]; 161 | } catch (Exception ex) { 162 | AppLog.warn("Ogg Subsystem", "Failed the parse comment field %s: %s! Got data %s", i, ex.msg, field); 163 | } 164 | } 165 | 166 | return infos; 167 | } 168 | 169 | AudioInfo info; 170 | string[string] kv = parseOggInfos(); 171 | info.file = this.fname; 172 | if ("ARTIST" in kv) info.artist = kv["ARTIST"]; 173 | if ("TITLE" in kv) info.title = kv["TITLE"]; 174 | if ("ALBUM" in kv) info.album = kv["ALBUM"]; 175 | if ("PERFOMER" in kv) info.performer = kv["PERFOMER"]; 176 | if ("DATE" in kv) info.date = kv["DATE"]; 177 | return info; 178 | } 179 | } -------------------------------------------------------------------------------- /source/engine/audio/astream/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.audio.astream; 8 | import engine.audio.astream.ogg; 9 | import std.path; 10 | import bindbc.openal; 11 | import std.format; 12 | import std.range; 13 | 14 | /** 15 | Open an audio file 16 | */ 17 | AudioStream open(string file, bool bit16 = true) { 18 | switch(file.extension) { 19 | case ".ogg": return new OggStream(file, bit16); 20 | default: throw new Exception("Unsupported file type (%s)".format(file.extension)); 21 | } 22 | } 23 | 24 | 25 | /** 26 | Information about audio, usually music 27 | */ 28 | struct AudioInfo { 29 | 30 | /** 31 | The file of the audio 32 | */ 33 | string file; 34 | 35 | /** 36 | The title of the audio 37 | */ 38 | string title; 39 | 40 | /** 41 | The artist behind the music 42 | */ 43 | string artist; 44 | 45 | /** 46 | The person performing the music 47 | */ 48 | string performer; 49 | 50 | /** 51 | The person performing the music 52 | */ 53 | string album; 54 | 55 | /** 56 | Date of release, usually year 57 | */ 58 | string date; 59 | 60 | /** 61 | Get the audio info as a string 62 | 63 | According to Danish law even things CC0 or public domain has to include crediting 64 | In the case the music doesn't (eg. the player drags music in to the game's music folder that is pirated) 65 | We'll leave a little message for them :) 66 | */ 67 | string toString() { 68 | if (artist.empty || title.empty) return "%s\n[Info missing! yarr harr?]".format(file); 69 | 70 | string base = "%s - %s".format(artist, title); 71 | if (!album.empty) base ~= " from %s".format(album); 72 | if (!date.empty) base ~= " (%s)".format(date); 73 | if (!performer.empty) base ~= "\nPerfomed by %s".format(performer); 74 | return base; 75 | } 76 | } 77 | 78 | /** 79 | Audio format of a stream 80 | */ 81 | enum Format { 82 | Mono8 = AL_FORMAT_MONO8, 83 | Mono16 = AL_FORMAT_MONO16, 84 | Stereo8 = AL_FORMAT_STEREO8, 85 | Stereo16 = AL_FORMAT_STEREO16 86 | } 87 | 88 | /** 89 | A stream of audio 90 | */ 91 | abstract class AudioStream { 92 | public: 93 | /** 94 | The amount of channels in the audio stream 95 | */ 96 | int channels; 97 | 98 | /** 99 | Audio format of the stream 100 | */ 101 | Format format; 102 | 103 | /** 104 | Read all samples from the stream till the end 105 | */ 106 | final ubyte[] readAll() { 107 | ubyte[] data = new ubyte[4096]; 108 | ubyte[] outData; 109 | ptrdiff_t readCount = 0; 110 | do { 111 | readCount = readSamples(data); 112 | outData ~= data[0..readCount]; 113 | } while(readCount > 0); 114 | 115 | if (canSeek) seek(0); 116 | return outData; 117 | } 118 | 119 | abstract: 120 | /** 121 | Read samples from the audio stream in to the array 122 | 123 | Returns the amount of samples read 124 | Returns 0 if there's no more samples 125 | */ 126 | ptrdiff_t readSamples(ref ubyte[] toArray); 127 | 128 | /** 129 | Gets whether the file can be seeked 130 | */ 131 | bool canSeek(); 132 | 133 | /** 134 | Seek to a PCM location in the stream 135 | */ 136 | void seek(size_t location); 137 | 138 | /** 139 | Get the position in the stream 140 | */ 141 | size_t tell(); 142 | 143 | /** 144 | Gets the bitrate of the stream 145 | */ 146 | size_t bitrate(); 147 | 148 | /** 149 | Try to get the information about the audio 150 | 151 | This information is usually only used for music 152 | */ 153 | AudioInfo getInfo(); 154 | } -------------------------------------------------------------------------------- /source/engine/audio/music.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.audio.music; 8 | import core.thread; 9 | import engine.audio.astream; 10 | import bindbc.openal; 11 | import engine.math; 12 | import std.math : isFinite; 13 | import engine; 14 | import events; 15 | 16 | /** 17 | The game's main playlist instance 18 | */ 19 | __gshared Playlist GamePlaylist; 20 | 21 | /** 22 | Initializes the game's main playlist instance 23 | */ 24 | void initPlaylist() { 25 | GamePlaylist = new Playlist(); 26 | GamePlaylist.addMusicFrom("assets/music/"); 27 | } 28 | 29 | /** 30 | A playlist of music 31 | */ 32 | class Playlist { 33 | private: 34 | bool isStopped = true; 35 | size_t current; 36 | Music[] songs; 37 | 38 | // Smarter shuffle function that avoids playing the same track twice 39 | size_t smartShuffle() { 40 | import std.random : uniform; 41 | size_t value; 42 | do { 43 | value = uniform(0, songs.length); 44 | } while(value != current); 45 | return value; 46 | } 47 | 48 | void nextSong() { 49 | 50 | // If we skip to the next song stop the current one from playing 51 | if (songs[current].isPlaying) { 52 | songs[current].stop(); 53 | } 54 | 55 | // Next song is the same 56 | if (repeatOne) { 57 | songs[current].play(); 58 | return; 59 | } 60 | 61 | if (shuffle) { 62 | 63 | // Shuffle to next song using the "smart" algorithm 64 | current = smartShuffle(); 65 | } else { 66 | // Increment song id 67 | current++; 68 | 69 | // Constrain ids to the length of the song array 70 | current %= songs.length; 71 | } 72 | 73 | // Play the new song 74 | songs[current].play(); 75 | onSongChange(cast(void*)this, songs[current].getInfo()); 76 | } 77 | 78 | public: 79 | 80 | ~this() { 81 | foreach(song; songs) { 82 | destroy(song); 83 | } 84 | } 85 | 86 | /** 87 | Repeat a single song 88 | */ 89 | bool repeatOne; 90 | 91 | /** 92 | Shuffle songs 93 | */ 94 | bool shuffle; 95 | 96 | /** 97 | Event called when song changes 98 | */ 99 | Event!AudioInfo onSongChange = new Event!AudioInfo(); 100 | 101 | /** 102 | Add Music to the playlist 103 | */ 104 | void addMusic(Music music) { 105 | songs ~= music; 106 | 107 | // We want to roll over to the first song so set it to the last 108 | current = songs.length-1; 109 | } 110 | 111 | /** 112 | Adds all .ogg files that could be found in a path recursively 113 | */ 114 | void addMusicFrom(string path) { 115 | import std.file : dirEntries, SpanMode; 116 | import std.algorithm : filter, endsWith; 117 | foreach(entry; dirEntries(path, SpanMode.depth).filter!(f => f.name.endsWith(".ogg"))) { 118 | addMusic(new Music(entry)); 119 | AppLog.info("Playlist", "Added song %s...", entry); 120 | } 121 | } 122 | 123 | /** 124 | Play the playlist 125 | */ 126 | void play() { 127 | isStopped = false; 128 | nextSong(); 129 | } 130 | 131 | /** 132 | Stop music from playing 133 | */ 134 | void stop() { 135 | isStopped = true; 136 | songs[current].stop(); 137 | } 138 | 139 | /** 140 | Gets whether the current song is playing 141 | */ 142 | bool isCurrentSongPlaying() { 143 | return songs[current].isPlaying(); 144 | } 145 | 146 | /** 147 | Play next song 148 | */ 149 | void next() { 150 | 151 | // Avoid accidentally starting the playlist again 152 | if (isStopped) return; 153 | 154 | // Play the next song in the playlist 155 | nextSong(); 156 | } 157 | 158 | /** 159 | Updates the playlist 160 | */ 161 | void update() { 162 | 163 | // Skip if we're stopped 164 | if (isStopped) return; 165 | 166 | // Play next song if the current one is stopped 167 | if (!isCurrentSongPlaying) next(); 168 | } 169 | } 170 | 171 | /** 172 | A stream of audio that can be played 173 | */ 174 | class Music { 175 | private: 176 | enum MUSIC_BUFF_SIZE = 4096; 177 | enum MUSIC_BUFF_COUNT = 4; 178 | 179 | long lastReadLength; 180 | AudioStream stream; 181 | ALuint sourceId; 182 | ALint processed; 183 | 184 | bool running; 185 | bool looping; 186 | 187 | Thread playerThread; 188 | void playThread() { 189 | 190 | // The processing buffer 191 | ALuint pBuf; 192 | ubyte[] pBufData = new ubyte[MUSIC_BUFF_SIZE*MUSIC_BUFF_COUNT]; 193 | ALint state; 194 | 195 | // The buffer chain 196 | ALuint[MUSIC_BUFF_COUNT] buffers; 197 | alGenBuffers(MUSIC_BUFF_COUNT, buffers.ptr); 198 | 199 | // Fill buffers with initial data 200 | foreach(i; 0..MUSIC_BUFF_COUNT) { 201 | lastReadLength = stream.readSamples(pBufData); 202 | alBufferData(buffers[i], stream.format, pBufData.ptr, cast(int)lastReadLength, cast(int)stream.bitrate); 203 | alSourceQueueBuffers(sourceId, 1, &buffers[i]); 204 | } 205 | 206 | // Start playing 207 | alSourcePlay(sourceId); 208 | mainLoop: while(running) { 209 | 210 | // Check how much data OpenAL has processed 211 | alGetSourcei(sourceId, AL_BUFFERS_PROCESSED, &processed); 212 | alGetError(); 213 | 214 | while(processed--) { 215 | 216 | // Unqueue the most recent cleared buffer 217 | alSourceUnqueueBuffers(sourceId, 1, &pBuf); 218 | 219 | // Read samples to buffer 220 | lastReadLength = stream.readSamples(pBufData); 221 | 222 | if (lastReadLength == 0) { 223 | // If we're at the end and we should loop then loop. (if possible) 224 | if (looping && stream.canSeek) { 225 | 226 | // Seek back to start of stream and read samples 227 | stream.seek(0); 228 | lastReadLength = stream.readSamples(pBufData); 229 | 230 | debug AppLog.info("Music Debug", "Music %s looped...", sourceId); 231 | 232 | } else { 233 | break mainLoop; 234 | } 235 | } 236 | 237 | // Buffer the data to OpenAL 238 | alBufferData(pBuf, stream.format, pBufData.ptr, cast(int)lastReadLength, cast(int)stream.bitrate); 239 | 240 | // Re-queue buffer 241 | alSourceQueueBuffers(sourceId, 1, &pBuf); 242 | 243 | // Get the current state of the buffer 244 | alGetSourcei(sourceId, AL_SOURCE_STATE, &state); 245 | 246 | // If stream is paused keep pausing here 247 | while (state == AL_PAUSED) { 248 | 249 | // Quit out if the music is stopped 250 | if (!running) break mainLoop; 251 | 252 | // Otherwise wait 253 | alGetSourcei(sourceId, AL_SOURCE_STATE, &state); 254 | Thread.sleep(10.msecs); 255 | } 256 | 257 | // Make sure if the buffer stops (due to running out) that we restart it 258 | if (state != AL_PLAYING) { 259 | alSourcePlay(sourceId); 260 | } 261 | } 262 | 263 | 264 | // Don't make the thread use all of the cpu 265 | Thread.sleep(10.msecs); 266 | } 267 | 268 | // Cleanup 269 | stream.seek(0); 270 | running = false; 271 | alDeleteBuffers(2, buffers.ptr); 272 | 273 | debug AppLog.info("Music Debug", "Music %s stopped...", sourceId); 274 | } 275 | 276 | public: 277 | ~this() { 278 | this.stop(); 279 | alDeleteSources(1, &sourceId); 280 | } 281 | 282 | /** 283 | Construct a sound from a file path 284 | */ 285 | this(string file) { 286 | this(open(file)); 287 | } 288 | 289 | /** 290 | Construct a sound 291 | */ 292 | this(AudioStream stream) { 293 | 294 | // Generate buffer 295 | this.stream = stream; 296 | 297 | // Generate source 298 | alGenSources(1, &sourceId); 299 | alSourcef(sourceId, AL_PITCH, 1); 300 | alSourcef(sourceId, AL_GAIN, 0.5); 301 | } 302 | 303 | /** 304 | Play sound 305 | */ 306 | void play(float gain = float.nan, float pitch = float.nan) { 307 | 308 | // We don't want to start multiple threads playing the same music 309 | if (running) return; 310 | 311 | // Seek back to start of music (just in case) 312 | if (lastReadLength == 0) { 313 | stream.seek(0); 314 | } 315 | 316 | // Set music start values if needed 317 | if (pitch.isFinite) alSourcef(sourceId, AL_PITCH, pitch); 318 | if (gain.isFinite) alSourcef(sourceId, AL_GAIN, gain); 319 | 320 | // Start thread and play music 321 | running = true; 322 | playerThread = new Thread(&playThread); 323 | playerThread.start(); 324 | } 325 | 326 | /** 327 | Set the pitch of this music 328 | */ 329 | void setPitch(float pitch) { 330 | alSourcef(sourceId, AL_PITCH, pitch); 331 | } 332 | 333 | /** 334 | Set the pitch of this music 335 | */ 336 | void setGain(float gain) { 337 | alSourcef(sourceId, AL_GAIN, gain); 338 | } 339 | 340 | /** 341 | Set the pitch of this music 342 | */ 343 | void setLooping(bool loop) { 344 | looping = loop; 345 | } 346 | 347 | /** 348 | Pause the song 349 | */ 350 | void pause() { 351 | alSourcePause(sourceId); 352 | } 353 | 354 | /** 355 | Stop sound 356 | */ 357 | void stop() { 358 | running = false; 359 | alSourceStop(sourceId); 360 | if (playerThread !is null) { 361 | playerThread.join(); 362 | playerThread = null; 363 | } 364 | } 365 | 366 | /** 367 | Gets whether a song is currently playing 368 | */ 369 | bool isPlaying() { 370 | return running; 371 | } 372 | 373 | /** 374 | Get info about the music 375 | */ 376 | AudioInfo getInfo() { 377 | return stream.getInfo(); 378 | } 379 | } -------------------------------------------------------------------------------- /source/engine/audio/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Audio Subsystem 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.audio; 10 | public import engine.audio.astream; 11 | public import engine.audio.music; 12 | public import engine.audio.sound; 13 | import bindbc.openal; 14 | import engine.math; 15 | 16 | /** 17 | Initializes the audio engine 18 | */ 19 | void initAudioEngine() { 20 | // Open audio device and set context 21 | ALCdevice* dev = alcOpenDevice(null); 22 | auto ctx = alcCreateContext(dev, null); 23 | alcMakeContextCurrent(ctx); 24 | } 25 | 26 | /** 27 | Set the position of the listener 28 | */ 29 | void setListenerPosition(vec3 position) { 30 | alListener3f(AL_POSITION, position.x, position.y, position.z); 31 | } 32 | /** 33 | Set the position of the listener 34 | */ 35 | vec3 getListenerPosition() { 36 | float x, y, z; 37 | alGetListener3f(AL_POSITION, &x, &y, &z); 38 | return vec3(x, y, z); 39 | } -------------------------------------------------------------------------------- /source/engine/audio/sound.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.audio.sound; 8 | import engine.audio.astream; 9 | import bindbc.openal; 10 | import engine.math; 11 | 12 | /** 13 | A sound 14 | */ 15 | class Sound { 16 | private: 17 | ALuint bufferId; 18 | ALuint sourceId; 19 | 20 | public: 21 | ~this() { 22 | alDeleteSources(1, &sourceId); 23 | alDeleteBuffers(1, &bufferId); 24 | } 25 | 26 | /** 27 | Construct a sound from a file path 28 | */ 29 | this(string file) { 30 | this(open(file)); 31 | } 32 | 33 | /** 34 | Construct a sound 35 | */ 36 | this(AudioStream stream) { 37 | 38 | // Generate buffer 39 | alGenBuffers(1, &bufferId); 40 | ubyte[] data = stream.readAll(); 41 | alBufferData(bufferId, stream.format, data.ptr, cast(int)data.length, cast(int)stream.bitrate/stream.channels); 42 | 43 | // Generate source 44 | alGenSources(1, &sourceId); 45 | alSourcei(sourceId, AL_BUFFER, bufferId); 46 | alSourcef(sourceId, AL_PITCH, 1); 47 | alSourcef(sourceId, AL_GAIN, 0.5); 48 | } 49 | 50 | /** 51 | Play sound 52 | */ 53 | void play(float gain = 0.5, float pitch = 1, vec3 position = vec3(0)) { 54 | alSource3f(sourceId, AL_POSITION, position.x, position.y, position.z); 55 | alSourcef(sourceId, AL_PITCH, pitch); 56 | alSourcef(sourceId, AL_GAIN, gain); 57 | alSourcePlay(sourceId); 58 | } 59 | 60 | void setPitch(float pitch) { 61 | alSourcef(sourceId, AL_PITCH, pitch); 62 | } 63 | 64 | void setGain(float gain) { 65 | alSourcef(sourceId, AL_GAIN, gain); 66 | } 67 | 68 | /** 69 | Stop sound 70 | */ 71 | void stop() { 72 | alSourceStop(sourceId); 73 | } 74 | } -------------------------------------------------------------------------------- /source/engine/core/astack.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.core.astack; 8 | import std.exception; 9 | 10 | /** 11 | A stack of actions performed in the game 12 | 13 | Actions can be undone and redone. 14 | Pushing a new action to the stack will overwrite actions past the current top cursor. 15 | */ 16 | class ActionStack(ActionT) { 17 | private: 18 | ActionT[] stack; 19 | size_t top; 20 | 21 | public: 22 | 23 | /** 24 | Push an action to the stack 25 | 26 | Returns the resulting top of the stack 27 | */ 28 | ActionT push(ActionT item) { 29 | 30 | // First remove any elements in the undo/redo chain after our top 31 | stack.length = top+1; 32 | 33 | // Move the top up one element 34 | top++; 35 | 36 | // Add new item 37 | stack ~= item; 38 | return stack[$-1]; 39 | } 40 | 41 | /** 42 | Get the current top of the stack 43 | */ 44 | ActionT get() { 45 | enforce(stack.length > 0, "ActionStack is empty."); 46 | return stack[top]; 47 | } 48 | 49 | /** 50 | Undo an action 51 | 52 | Returns the resulting top of the stack 53 | */ 54 | ActionT undo() { 55 | enforce(stack.length > 0, "ActionStack is empty."); 56 | if (top > 0) top--; 57 | return stack[top]; 58 | } 59 | 60 | /** 61 | Redo an action 62 | 63 | Returns the resulting top of the stack 64 | */ 65 | ActionT redo() { 66 | enforce(stack.length > 0, "ActionStack is empty."); 67 | if (top < stack.length) top++; 68 | return stack[top]; 69 | } 70 | 71 | /** 72 | Clear the action stack 73 | */ 74 | void clear() { 75 | top = 0; 76 | stack.length = 0; 77 | } 78 | 79 | /** 80 | Gets whether the action stack is empty. 81 | */ 82 | bool empty() { 83 | return stack.length == 0; 84 | } 85 | 86 | /** 87 | Returns true if there's any actions left to undo 88 | */ 89 | bool canUndo() { 90 | return top > 0; 91 | } 92 | 93 | /** 94 | Returns true if there's any actions left to redo 95 | */ 96 | bool canRedo() { 97 | return top < stack.length; 98 | } 99 | } -------------------------------------------------------------------------------- /source/engine/core/log.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.core.log; 8 | import std.stdio; 9 | import std.file; 10 | import std.format; 11 | import core.stdc.stdlib : exit; 12 | 13 | __gshared Logger AppLog; 14 | 15 | /** 16 | A logger 17 | */ 18 | class Logger { 19 | private: 20 | bool logFileOpened = false; 21 | File logFile; 22 | string logFileName; 23 | 24 | void writeLogToFile(string text) { 25 | 26 | // Open log file if need be 27 | if (!logFileOpened) this.openLogFile(); 28 | 29 | // Write to log 30 | logFile.writeln(text); 31 | logFile.flush(); 32 | } 33 | 34 | void openLogFile() { 35 | this.logFile = File(this.logFileName, "a"); 36 | } 37 | 38 | public: 39 | /** 40 | Whether to write logs to file 41 | 42 | TODO: implement 43 | */ 44 | bool logToFile; 45 | 46 | /** 47 | Creates a new logger 48 | */ 49 | this(bool logToFile = false, string logFile = "applog.log") { 50 | this.logToFile = logToFile; 51 | this.logFileName = logFile; 52 | 53 | if (logToFile) { 54 | this.openLogFile(); 55 | } 56 | } 57 | 58 | /** 59 | Closes the logger and its attachment to the log file 60 | */ 61 | ~this() { 62 | if (logFileOpened) logFile.close(); 63 | } 64 | 65 | /** 66 | Write info log to stdout 67 | */ 68 | void info(T...)(string sender, string text, T fmt) { 69 | string logText = "[%s] info: %s".format(sender, text.format(fmt)); 70 | writeln(logText); 71 | 72 | if (logToFile) { 73 | this.writeLogToFile(logText); 74 | } 75 | } 76 | 77 | /** 78 | Write warning log to stdout 79 | */ 80 | void warn(T...)(string sender, string text, T fmt) { 81 | string logText = "[%s] warning: %s".format(sender, text.format(fmt)); 82 | writeln(logText); 83 | 84 | if (logToFile) { 85 | this.writeLogToFile(logText); 86 | } 87 | } 88 | 89 | /** 90 | Writes error to stderr 91 | */ 92 | void error(T...)(string sender, string text, T fmt) { 93 | string logText = "[%s] error: %s".format(sender, text.format(fmt)); 94 | stderr.writeln(logText); 95 | 96 | if (logToFile) { 97 | this.writeLogToFile(logText); 98 | } 99 | } 100 | 101 | /** 102 | Writes a fatal error to stderr and quits the application with status -1 103 | */ 104 | void fatal(T...)(string sender, string text, T fmt) { 105 | string logText = "[%s] fatal: %s".format(sender, text.format(fmt)); 106 | stderr.writeln(logText); 107 | 108 | if (logToFile) { 109 | this.writeLogToFile(logText); 110 | } 111 | 112 | exit(-1); 113 | } 114 | } -------------------------------------------------------------------------------- /source/engine/core/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Various core functionality 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.core; 10 | public import engine.core.log; 11 | public import engine.core.window; 12 | public import engine.core.astack; 13 | public import engine.core.strings; 14 | public import engine.core.state; -------------------------------------------------------------------------------- /source/engine/core/state.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.core.state; 8 | 9 | /** 10 | Manager of game states 11 | */ 12 | class GameStateManager { 13 | private static: 14 | GameState[] states; 15 | 16 | public static: 17 | 18 | /** 19 | Update the current game state 20 | */ 21 | void update() { 22 | if (states.length == 0) return; 23 | states[$-1].update(); 24 | } 25 | 26 | /** 27 | Draw the current game state 28 | 29 | Offset sets the offset from the top of the stack to draw 30 | */ 31 | void draw(size_t offset = 1) { 32 | if (states.length == 0) return; 33 | 34 | // Handle drawing passthrough, this allows us to draw game boards with game over screen overlayed. 35 | if (states[$-offset].drawPassthrough && states.length > offset) { 36 | states[$-offset].draw(); 37 | } 38 | 39 | // Draw the current state 40 | states[$-offset].draw(); 41 | } 42 | 43 | /** 44 | Push a game state on to the stack 45 | */ 46 | void push(GameState state) { 47 | states ~= state; 48 | states[$-1].onActivate(); 49 | } 50 | 51 | /** 52 | Pop a game state from the stack 53 | */ 54 | void pop() { 55 | if (canPop) { 56 | states[$-1].onDeactivate(); 57 | states.length--; 58 | } 59 | } 60 | 61 | /** 62 | Gets whether an element can be popped from the game state stack 63 | */ 64 | bool canPop() { 65 | return states.length > 0; 66 | } 67 | 68 | /** 69 | Pop a game state from the stack 70 | */ 71 | void popAll() { 72 | while (canPop) pop(); 73 | } 74 | 75 | } 76 | 77 | /** 78 | A game state 79 | */ 80 | abstract class GameState { 81 | public: 82 | /** 83 | Wether drawing should pass-through to the previous game state (if any) 84 | This allows overlaying a gamr state over an other 85 | */ 86 | bool drawPassthrough; 87 | 88 | /** 89 | Update the game state 90 | */ 91 | abstract void update(); 92 | 93 | /** 94 | Draw the game state 95 | */ 96 | abstract void draw(); 97 | 98 | /** 99 | When a state is pushed 100 | */ 101 | void onActivate() { } 102 | 103 | /** 104 | Called when a state is popped 105 | */ 106 | void onDeactivate() { } 107 | } -------------------------------------------------------------------------------- /source/engine/core/strings.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.core.strings; 8 | import std.utf : toUTF32, toUTF16, toUTF8; 9 | 10 | /** 11 | Convert any type of string to a engine usable string 12 | */ 13 | string toDString(T)(T str) if (isString!T) { 14 | static if (is(T == string)) return str; 15 | else return toUTF8(str); 16 | } 17 | 18 | /** 19 | Convert any type of string to a engine usable string 20 | */ 21 | dstring toEngineString(T)(T str) if (isString!T) { 22 | static if (is(T == dstring)) return str; 23 | else return toUTF32(str); 24 | } 25 | 26 | /** 27 | Convert any type of string to a windows compatible UTF16 string 28 | */ 29 | wstring toWin32String(T)(T str) if (isString!T) { 30 | static if (is(T == wstring)) return str; 31 | else return toUTF16(str); 32 | } 33 | 34 | /** 35 | Is true if the specified type T is a string 36 | */ 37 | enum isString(T) = is(T == string) || is (T == wstring) || is(T == dstring); -------------------------------------------------------------------------------- /source/engine/core/window.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.core.window; 8 | import bindbc.glfw; 9 | import bindbc.opengl; 10 | import engine.render : kmViewport; 11 | 12 | /** 13 | Static instance of the game window 14 | */ 15 | static Window GameWindow; 16 | 17 | /** 18 | A Window 19 | */ 20 | class Window { 21 | private: 22 | GLFWwindow* window; 23 | string title_; 24 | int width_; 25 | int height_; 26 | 27 | int fbWidth; 28 | int fbHeight; 29 | 30 | public: 31 | 32 | /** 33 | Destructor 34 | */ 35 | ~this() { 36 | glfwDestroyWindow(window); 37 | } 38 | 39 | /** 40 | Constructor 41 | */ 42 | this(string title = "My Game", int width = 640, int height = 480) { 43 | this.title_ = title; 44 | this.width_ = width; 45 | this.height_ = height; 46 | this.fbWidth = width; 47 | this.fbHeight = height; 48 | 49 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 50 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); 51 | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); // To make macOS happy 52 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE); 53 | window = glfwCreateWindow(640, 480, this.title_.ptr, null, null); 54 | 55 | } 56 | 57 | /** 58 | Hides the window 59 | */ 60 | void hide() { 61 | glfwHideWindow(window); 62 | } 63 | 64 | /** 65 | Show window 66 | */ 67 | void show() { 68 | glfwShowWindow(window); 69 | } 70 | 71 | /** 72 | Gets the title of the window 73 | */ 74 | @property string title() { 75 | return this.title_; 76 | } 77 | 78 | /** 79 | Sets the title of the window 80 | */ 81 | @property void title(string value) { 82 | this.title_ = value; 83 | glfwSetWindowTitle(window, this.title_.ptr); 84 | } 85 | 86 | /** 87 | Gets the width of the window's framebuffer 88 | */ 89 | @property int width() { 90 | return this.fbWidth; 91 | } 92 | 93 | /** 94 | Gets the height of the window's framebuffer 95 | */ 96 | @property int height() { 97 | return this.fbHeight; 98 | } 99 | 100 | /** 101 | Resizes the window 102 | */ 103 | void resize(int width, int height) { 104 | this.width_ = width; 105 | this.height_ = height; 106 | glfwSetWindowSize(window, width, height); 107 | } 108 | 109 | /** 110 | Gets whether the window is fullscreen 111 | */ 112 | bool fullscreen() { 113 | return glfwGetWindowMonitor(window) !is null; 114 | } 115 | 116 | /** 117 | Sets the window's fullscreen state 118 | */ 119 | void fullscreen(bool value) { 120 | if (this.fullscreen == value) return; 121 | 122 | // TODO: change state 123 | // HACK: currently we're just setting the window as borderless 124 | if (value) { 125 | const(GLFWvidmode)* mode = glfwGetVideoMode(glfwGetPrimaryMonitor()); 126 | this.resize(mode.width, mode.height); 127 | glfwSetWindowAttrib(window, GLFW_DECORATED, GLFW_FALSE); 128 | } else { 129 | glfwSetWindowAttrib(window, GLFW_DECORATED, GLFW_TRUE); 130 | } 131 | } 132 | 133 | /** 134 | poll for new window events 135 | */ 136 | void update() { 137 | glfwPollEvents(); 138 | glfwGetFramebufferSize(window, &fbWidth, &fbHeight); 139 | } 140 | 141 | /** 142 | Set the close request flag 143 | */ 144 | void close() { 145 | glfwSetWindowShouldClose(window, 1); 146 | } 147 | 148 | /** 149 | Gets whether the window has requested to close (aka the game is requested to exit) 150 | */ 151 | bool isExitRequested() { 152 | return cast(bool)glfwWindowShouldClose(window); 153 | } 154 | 155 | /** 156 | Makes the OpenGL context of the window current 157 | */ 158 | void makeCurrent() { 159 | glfwMakeContextCurrent(window); 160 | } 161 | 162 | /** 163 | Swaps the OpenGL buffers for the window 164 | */ 165 | void swapBuffers() { 166 | glfwSwapBuffers(window); 167 | } 168 | 169 | /** 170 | Sets the swap interval, by default vsync 171 | */ 172 | void setSwapInterval(SwapInterval interval = SwapInterval.VSync) { 173 | glfwSwapInterval(cast(int)interval); 174 | } 175 | 176 | /** 177 | Resets the OpenGL viewport to fit the window 178 | */ 179 | void resetViewport() { 180 | kmViewport(0, 0, width, height); 181 | } 182 | 183 | /** 184 | Gets the glfw window pointer 185 | */ 186 | GLFWwindow* winPtr() { 187 | return window; 188 | } 189 | } 190 | 191 | /** 192 | A swap interval 193 | */ 194 | enum SwapInterval : int { 195 | Unlimited = 0, 196 | VSync = 1 197 | } -------------------------------------------------------------------------------- /source/engine/game.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.game; 8 | import bindbc.glfw; 9 | import engine; 10 | 11 | private double previousTime_; 12 | private double currentTime_; 13 | private double deltaTime_; 14 | 15 | /** 16 | Function run when the game is to initialize 17 | */ 18 | void function() gameInit; 19 | 20 | /** 21 | Function run when the game is to update 22 | */ 23 | void function() gameUpdate; 24 | 25 | /** 26 | Function run after the main rendering has happened, Used to draw borders for the gameplay 27 | */ 28 | void function() gameBorder; 29 | 30 | /** 31 | Function run after updates and rendering of the game 32 | */ 33 | void function() gamePostUpdate; 34 | 35 | /** 36 | Function run when the game is to clean up 37 | */ 38 | void function() gameCleanup; 39 | 40 | /** 41 | Starts the game loop 42 | 43 | viewportSize sets the desired viewport size for the framebuffer, defaults to 1080p (1920x1080) 44 | */ 45 | void startGame(vec2i viewportSize = vec2i(1920, 1080)) { 46 | gameInit(); 47 | resetTime(); 48 | 49 | Framebuffer framebuffer = new Framebuffer(GameWindow, viewportSize); 50 | while(!GameWindow.isExitRequested) { 51 | 52 | currentTime_ = glfwGetTime(); 53 | deltaTime_ = currentTime_-previousTime_; 54 | previousTime_ = currentTime_; 55 | 56 | // Bind our framebuffer 57 | framebuffer.bind(); 58 | 59 | // Clear color and depth buffers 60 | glClearColor(0, 0, 0, 1); 61 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 62 | 63 | // Update and render the game 64 | gameUpdate(); 65 | 66 | // Unbind our framebuffer 67 | framebuffer.unbind(); 68 | 69 | // Clear color and depth bits 70 | glClearColor(0, 0, 0, 0); 71 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 72 | 73 | // Draw border, framebuffer and post update content 74 | if (gameBorder !is null) gameBorder(); 75 | framebuffer.renderToFit(); 76 | if (gamePostUpdate !is null) gamePostUpdate(); 77 | 78 | // Update the mouse's state 79 | Mouse.update(); 80 | Input.update(); 81 | 82 | // Swap buffers and update the window 83 | GameWindow.swapBuffers(); 84 | GameWindow.update(); 85 | } 86 | 87 | // Pop all states 88 | GameStateManager.popAll(); 89 | 90 | // Game cleanup 91 | gameCleanup(); 92 | } 93 | 94 | /** 95 | Gets delta time 96 | */ 97 | double deltaTime() { 98 | return deltaTime_; 99 | } 100 | 101 | /** 102 | Gets delta time 103 | */ 104 | double prevTime() { 105 | return previousTime_; 106 | } 107 | 108 | /** 109 | Gets delta time 110 | */ 111 | double currTime() { 112 | return currentTime_; 113 | } 114 | 115 | /** 116 | Resets the time scale 117 | */ 118 | void resetTime() { 119 | glfwSetTime(0); 120 | previousTime_ = 0; 121 | currentTime_ = 0; 122 | } -------------------------------------------------------------------------------- /source/engine/i18n/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Internationalization Subsystem 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.i18n; 10 | 11 | string CurrentLanguage = "EN"; -------------------------------------------------------------------------------- /source/engine/input/keyboard.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.input.keyboard; 8 | import engine.input; 9 | import bindbc.glfw; 10 | import events; 11 | 12 | /** 13 | Keys 14 | */ 15 | enum Key { 16 | KeyUnknown = GLFW_KEY_UNKNOWN, 17 | KeySpace = GLFW_KEY_SPACE, 18 | KeyApostrophe = GLFW_KEY_APOSTROPHE, 19 | KeyComma = GLFW_KEY_COMMA, 20 | KeyMinus = GLFW_KEY_MINUS, 21 | KeyPeriod = GLFW_KEY_PERIOD, 22 | KeySlash = GLFW_KEY_SLASH, 23 | Key0 = GLFW_KEY_0, 24 | Key1 = GLFW_KEY_1, 25 | Key2 = GLFW_KEY_2, 26 | Key3 = GLFW_KEY_3, 27 | Key4 = GLFW_KEY_4, 28 | Key5 = GLFW_KEY_5, 29 | Key6 = GLFW_KEY_6, 30 | Key7 = GLFW_KEY_7, 31 | Key8 = GLFW_KEY_8, 32 | Key9 = GLFW_KEY_9, 33 | KeySemicolon = GLFW_KEY_SEMICOLON, 34 | KeyEqual = GLFW_KEY_EQUAL, 35 | KeyA = GLFW_KEY_A, 36 | KeyB = GLFW_KEY_B, 37 | KeyC = GLFW_KEY_C, 38 | KeyD = GLFW_KEY_D, 39 | KeyE = GLFW_KEY_E, 40 | KeyF = GLFW_KEY_F, 41 | KeyG = GLFW_KEY_G, 42 | KeyH = GLFW_KEY_H, 43 | KeyI = GLFW_KEY_I, 44 | KeyJ = GLFW_KEY_J, 45 | KeyK = GLFW_KEY_K, 46 | KeyL = GLFW_KEY_L, 47 | KeyM = GLFW_KEY_M, 48 | KeyN = GLFW_KEY_N, 49 | KeyO = GLFW_KEY_O, 50 | KeyP = GLFW_KEY_P, 51 | KeyQ = GLFW_KEY_Q, 52 | KeyR = GLFW_KEY_R, 53 | KeyS = GLFW_KEY_S, 54 | KeyT = GLFW_KEY_T, 55 | KeyU = GLFW_KEY_U, 56 | KeyV = GLFW_KEY_V, 57 | KeyW = GLFW_KEY_W, 58 | KeyX = GLFW_KEY_X, 59 | KeyY = GLFW_KEY_Y, 60 | KeyZ = GLFW_KEY_Z, 61 | KeyBracket = GLFW_KEY_LEFT_BRACKET, 62 | KeyBackslash = GLFW_KEY_BACKSLASH, 63 | KeyRightBracket = GLFW_KEY_RIGHT_BRACKET, 64 | KeyAccent = GLFW_KEY_GRAVE_ACCENT, 65 | KeyWorld1 = GLFW_KEY_WORLD_1, 66 | KeyWorld2 = GLFW_KEY_WORLD_2, 67 | 68 | KeyEscape = GLFW_KEY_ESCAPE, 69 | KeyEnter = GLFW_KEY_ENTER, 70 | KeyTab = GLFW_KEY_TAB, 71 | KeyBackspace = GLFW_KEY_BACKSPACE, 72 | KeyInsert = GLFW_KEY_INSERT, 73 | KeyDelete = GLFW_KEY_DELETE, 74 | KeyRight = GLFW_KEY_RIGHT, 75 | KeyLeft = GLFW_KEY_LEFT, 76 | KeyDown = GLFW_KEY_DOWN, 77 | KeyUp = GLFW_KEY_UP, 78 | KeyPageUp = GLFW_KEY_PAGE_UP, 79 | KeyPageDown = GLFW_KEY_PAGE_DOWN, 80 | KeyHome = GLFW_KEY_HOME, 81 | KeyEnd = GLFW_KEY_END, 82 | KeyCapsLock = GLFW_KEY_CAPS_LOCK, 83 | KeyScrollLock = GLFW_KEY_SCROLL_LOCK, 84 | KeyNumLock = GLFW_KEY_NUM_LOCK, 85 | KeyPrintScreen = GLFW_KEY_PRINT_SCREEN, 86 | KeyPause = GLFW_KEY_PAUSE, 87 | KeyF1 = GLFW_KEY_F1, 88 | KeyF2 = GLFW_KEY_F2, 89 | KeyF3 = GLFW_KEY_F3, 90 | KeyF4 = GLFW_KEY_F4, 91 | KeyF5 = GLFW_KEY_F5, 92 | KeyF6 = GLFW_KEY_F6, 93 | KeyF7 = GLFW_KEY_F7, 94 | KeyF8 = GLFW_KEY_F8, 95 | KeyF9 = GLFW_KEY_F9, 96 | KeyF10 = GLFW_KEY_F10, 97 | KeyF11 = GLFW_KEY_F11, 98 | KeyF12 = GLFW_KEY_F12, 99 | KeyF13 = GLFW_KEY_F13, 100 | KeyF14 = GLFW_KEY_F14, 101 | KeyF15 = GLFW_KEY_F15, 102 | KeyF16 = GLFW_KEY_F16, 103 | KeyF17 = GLFW_KEY_F17, 104 | KeyF18 = GLFW_KEY_F18, 105 | KeyF19 = GLFW_KEY_F19, 106 | KeyF20 = GLFW_KEY_F20, 107 | KeyF21 = GLFW_KEY_F21, 108 | KeyF22 = GLFW_KEY_F22, 109 | KeyF23 = GLFW_KEY_F23, 110 | KeyF24 = GLFW_KEY_F24, 111 | KeyF25 = GLFW_KEY_F25, 112 | Keypad0 = GLFW_KEY_KP_0, 113 | Keypad1 = GLFW_KEY_KP_1, 114 | Keypad2 = GLFW_KEY_KP_2, 115 | Keypad3 = GLFW_KEY_KP_3, 116 | Keypad4 = GLFW_KEY_KP_4, 117 | Keypad5 = GLFW_KEY_KP_5, 118 | Keypad6 = GLFW_KEY_KP_6, 119 | Keypad7 = GLFW_KEY_KP_7, 120 | Keypad8 = GLFW_KEY_KP_8, 121 | Keypad9 = GLFW_KEY_KP_9, 122 | KeypadDecimal = GLFW_KEY_KP_DECIMAL, 123 | KeypadDivide = GLFW_KEY_KP_DIVIDE, 124 | KeypadMultiply = GLFW_KEY_KP_MULTIPLY, 125 | KeypadSubtract = GLFW_KEY_KP_SUBTRACT, 126 | KeypadAdd = GLFW_KEY_KP_ADD, 127 | KeypadEnter = GLFW_KEY_KP_ENTER, 128 | KeypadEqual = GLFW_KEY_KP_EQUAL, 129 | KeyLeftShift = GLFW_KEY_LEFT_SHIFT, 130 | KeyLeftControl = GLFW_KEY_LEFT_CONTROL, 131 | KeyLeftAlt = GLFW_KEY_LEFT_ALT, 132 | KeyLeftSuper = GLFW_KEY_LEFT_SUPER, 133 | KeyRightShift = GLFW_KEY_RIGHT_SHIFT, 134 | KeyRightControl = GLFW_KEY_RIGHT_CONTROL, 135 | KeyRightAlt = GLFW_KEY_RIGHT_ALT, 136 | KeyRightSuper = GLFW_KEY_RIGHT_SUPER, 137 | KeyMenu = GLFW_KEY_MENU, 138 | KeyLast = GLFW_KEY_LAST, 139 | 140 | KeyEsc = GLFW_KEY_ESC, 141 | KeyDel = GLFW_KEY_DEL, 142 | KeyPgUp = GLFW_KEY_PAGEUP, 143 | KeyPgDown = GLFW_KEY_PAGEDOWN, 144 | KeypadNumLock = GLFW_KEY_KP_NUM_LOCK, 145 | KeyLCtrl = GLFW_KEY_LCTRL, 146 | KeyLShift = GLFW_KEY_LSHIFT, 147 | KeyLAlt = GLFW_KEY_LALT, 148 | KeyLSuper = GLFW_KEY_LSUPER, 149 | KeyRCtrl = GLFW_KEY_RCTRL, 150 | KeyRShift = GLFW_KEY_RSHIFT, 151 | KeyRAlt = GLFW_KEY_RALT, 152 | KeyRSuper = GLFW_KEY_RSUPER, 153 | } 154 | 155 | /** 156 | Keyboard 157 | */ 158 | class Keyboard { 159 | private static: 160 | GLFWwindow* window; 161 | dchar curChar; 162 | 163 | bool capsLock; 164 | bool numLock; 165 | 166 | public static: 167 | 168 | /** 169 | Event that is called every time a new text character is typed 170 | */ 171 | Event!dchar onKeyboardType; 172 | 173 | /** 174 | Event that is called every time a new ket event is emitted 175 | */ 176 | Event!Key onKeyEvent; 177 | 178 | /** 179 | Constructs the underlying data needed 180 | */ 181 | void initialize(GLFWwindow* window) { 182 | this.window = window; 183 | 184 | // Create events 185 | onKeyboardType = new Event!dchar(); 186 | onKeyEvent = new Event!Key(); 187 | 188 | // Do GLFW stuff 189 | glfwSetInputMode(window, GLFW_LOCK_KEY_MODS, GLFW_TRUE); 190 | glfwSetCharCallback(window, &onCharCallback); 191 | glfwSetKeyCallback(window, &onKeyCallback); 192 | } 193 | 194 | /** 195 | Gets the current typed text character for text input 196 | */ 197 | dchar getCurrentCharacter() { 198 | return curChar; 199 | } 200 | 201 | /** 202 | Gets whether a key is pressed 203 | */ 204 | bool isKeyPressed(Key key) { 205 | return glfwGetKey(window, key) == GLFW_PRESS; 206 | } 207 | 208 | /** 209 | Gets whether a key is pressed 210 | */ 211 | bool isKeyReleased(Key key) { 212 | return glfwGetKey(window, key) == GLFW_RELEASE; 213 | } 214 | 215 | /** 216 | Gets whether CapsLock is on 217 | */ 218 | bool isCapsLockOn() { 219 | return capsLock; 220 | } 221 | 222 | /** 223 | Gets whether NumLock is on 224 | */ 225 | bool isNumLockOn() { 226 | return numLock; 227 | } 228 | } 229 | 230 | private: 231 | void onKeyEventHandler(Key key) { 232 | Keyboard.onKeyEvent(null, key); 233 | } 234 | 235 | void onCharEventHandler(dchar key) { 236 | Keyboard.onKeyboardType(null, key); 237 | } 238 | 239 | nothrow extern(C): 240 | void onKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { 241 | Keyboard.numLock = (mods & GLFW_MOD_NUM_LOCK) > 0; 242 | Keyboard.capsLock = (mods & GLFW_MOD_CAPS_LOCK) > 0; 243 | 244 | // Cursed magic to make our exception throwing D event handler be callable from this C function 245 | (cast(void function(Key) nothrow)&onKeyEventHandler)(cast(Key)key); 246 | } 247 | 248 | void onCharCallback(GLFWwindow* window, uint codepoint) { 249 | Keyboard.curChar = cast(dchar)codepoint; 250 | 251 | // Cursed magic to make our exception throwing D event handler be callable from this C function 252 | (cast(void function(dchar) nothrow)&onCharEventHandler)(cast(dchar)codepoint); 253 | } 254 | -------------------------------------------------------------------------------- /source/engine/input/mouse.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.input.mouse; 8 | import engine.input; 9 | import bindbc.glfw; 10 | import gl3n.linalg; 11 | 12 | /** 13 | The buttons on a mouse 14 | */ 15 | enum MouseButton { 16 | Left = GLFW_MOUSE_BUTTON_LEFT, 17 | Middle = GLFW_MOUSE_BUTTON_MIDDLE, 18 | Right = GLFW_MOUSE_BUTTON_RIGHT 19 | } 20 | 21 | /** 22 | Mouse 23 | */ 24 | class Mouse { 25 | private static: 26 | GLFWwindow* window; 27 | 28 | bool[MouseButton] lastState; 29 | 30 | public static: 31 | 32 | /** 33 | Constructs the underlying data needed 34 | */ 35 | void initialize(GLFWwindow* window) { 36 | this.window = window; 37 | } 38 | 39 | /** 40 | Gets mouse position 41 | */ 42 | vec2 position() { 43 | double x, y; 44 | glfwGetCursorPos(window, &x, &y); 45 | return vec2(cast(float)x, cast(float)y); 46 | } 47 | 48 | /** 49 | Gets if a mouse button is pressed 50 | */ 51 | bool isButtonPressed(MouseButton button) { 52 | return glfwGetMouseButton(window, button) == GLFW_PRESS; 53 | } 54 | 55 | /** 56 | Gets if a mouse button is released 57 | */ 58 | bool isButtonReleased(MouseButton button) { 59 | return glfwGetMouseButton(window, button) == GLFW_RELEASE; 60 | } 61 | 62 | /** 63 | Gets whether the button was clicked 64 | */ 65 | bool isButtonClicked(MouseButton button) { 66 | return !lastState[button] && isButtonPressed(button); 67 | } 68 | 69 | /** 70 | Gets whether the button was clicked 71 | */ 72 | bool isButtonUnclicked(MouseButton button) { 73 | return lastState[button] && isButtonReleased(button); 74 | } 75 | 76 | /** 77 | Updates the mouse state for single-clicking 78 | */ 79 | void update() { 80 | lastState[MouseButton.Left] = glfwGetMouseButton(window, MouseButton.Left) == GLFW_PRESS; 81 | lastState[MouseButton.Middle] = glfwGetMouseButton(window, MouseButton.Middle) == GLFW_PRESS; 82 | lastState[MouseButton.Right] = glfwGetMouseButton(window, MouseButton.Right) == GLFW_PRESS; 83 | } 84 | } -------------------------------------------------------------------------------- /source/engine/input/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Input Handling Subsystem 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.input; 10 | import bindbc.glfw; 11 | 12 | public import engine.input.keyboard; 13 | public import engine.input.mouse; 14 | 15 | /** 16 | Initializes input system 17 | */ 18 | void initInput(GLFWwindow* window) { 19 | 20 | // Initialize keyboard 21 | Keyboard.initialize(window); 22 | 23 | // Initialize mouse 24 | Mouse.initialize(window); 25 | } 26 | 27 | private struct Keybinding { 28 | Key key; 29 | 30 | bool lstate; 31 | bool state; 32 | } 33 | 34 | class Input { 35 | private static: 36 | Keybinding*[string] keybindings; 37 | 38 | public static: 39 | 40 | /** 41 | Register a key and a default binding for a keybinding 42 | */ 43 | void registerKey(string name, Key binding) { 44 | keybindings[name] = new Keybinding(binding, false, false); 45 | } 46 | 47 | /** 48 | Load keybindings from a list of bindings 49 | */ 50 | void loadBindings(Key[string] bindings) { 51 | foreach(name, binding; bindings) { 52 | registerKey(name, binding); 53 | } 54 | } 55 | 56 | /** 57 | Gets the key attached to a keybinding 58 | */ 59 | Key getKeyFor(string name) { 60 | return keybindings[name].key; 61 | } 62 | 63 | /** 64 | Whether a user pressed the specified binding button 65 | */ 66 | bool isPressed(string name) { 67 | return keybindings[name].state && keybindings[name].state != keybindings[name].lstate; 68 | } 69 | 70 | /** 71 | Whether a user pressed the specified binding button the last frame 72 | */ 73 | bool wasPressed(string name) { 74 | return !keybindings[name].state && keybindings[name].state != keybindings[name].lstate; 75 | } 76 | 77 | /** 78 | Whether a user pressed the specified binding button 79 | */ 80 | bool isDown(string name) { 81 | return keybindings[name].state; 82 | } 83 | 84 | /** 85 | Whether a user pressed the specified binding button 86 | */ 87 | bool isUp(string name) { 88 | return !keybindings[name].state; 89 | } 90 | 91 | /** 92 | Updates the keybinding states 93 | */ 94 | void update() { 95 | 96 | // Update keybindings 97 | foreach(binding; keybindings) { 98 | binding.lstate = binding.state; 99 | binding.state = Keyboard.isKeyPressed(binding.key); 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /source/engine/math/camera.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.math.camera; 8 | import gl3n.linalg; 9 | import engine; 10 | 11 | /** 12 | Camera for 3D space 13 | */ 14 | class Camera { 15 | private: 16 | 17 | mat4 g_matrix() { 18 | return mat4.perspective(cameraTargetWidth, cameraTargetHeight, fov, 0.1, 50) * transform.matrix; 19 | } 20 | 21 | public: 22 | 23 | this() { 24 | 25 | } 26 | 27 | float fov = 90; 28 | 29 | /** 30 | Transform of the position of the camera 31 | */ 32 | Transform transform; 33 | 34 | /** 35 | Create 3D camera 36 | */ 37 | this(Transform transform = null) { 38 | this.transform = transform is null ? new Transform() : transform; 39 | } 40 | 41 | /** 42 | The matrix for the camera 43 | */ 44 | mat4 matrix() { 45 | return g_matrix(); 46 | } 47 | } 48 | 49 | /** 50 | Orthographic camera for rendering UI 51 | 52 | TODO: extend camera as needed 53 | */ 54 | class Camera2D { 55 | private: 56 | mat4 projection; 57 | 58 | public: 59 | 60 | this() { 61 | position = vec2(0, 0); 62 | } 63 | 64 | /** 65 | Position of camera 66 | */ 67 | vec2 position; 68 | 69 | /** 70 | Matrix for this camera 71 | */ 72 | mat4 matrix() { 73 | int largestSize = max(kmViewportWidth, kmViewportHeight); 74 | return 75 | mat4.orthographic(0f, cast(float)cameraTargetWidth, cast(float)cameraTargetHeight, 0, 0, largestSize) * 76 | mat4.translation(position.x, position.y, -10); 77 | } 78 | } 79 | 80 | private int cameraTargetWidth; 81 | private int cameraTargetHeight; 82 | 83 | void kmSetCameraTargetSize(int width, int height) { 84 | cameraTargetWidth = width; 85 | cameraTargetHeight = height; 86 | } 87 | 88 | int kmCameraViewWidth() { 89 | return cameraTargetWidth; 90 | } 91 | 92 | int kmCameraViewHeight() { 93 | return cameraTargetHeight; 94 | } -------------------------------------------------------------------------------- /source/engine/math/obb.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.math.obb; 8 | import gl3n.linalg; 9 | 10 | /** 11 | Oriented bounding box 12 | */ 13 | struct OBB { 14 | public: 15 | /** 16 | Position of the OBB 17 | */ 18 | vec3 position; 19 | 20 | /** 21 | Rotation of the OBB 22 | */ 23 | quat rotation; 24 | 25 | /** 26 | Extents of the OBB 27 | */ 28 | vec3 size; 29 | 30 | /** 31 | Minimum extent of the OBB 32 | */ 33 | vec3 min() { 34 | return vec3(rotation.to_matrix!(3, 3) * vec3(position.x-size.x, position.y-size.y, position.z-size.z)); 35 | } 36 | 37 | /** 38 | Maximum extent of the OBB 39 | */ 40 | vec3 max() { 41 | return vec3(rotation.to_matrix!(3, 3) * vec3(position.x+size.x, position.y+size.y, position.z+size.z)); 42 | } 43 | 44 | /** 45 | Gets this OBB as a matrix 46 | */ 47 | mat3 asMatrix() { 48 | return mat3.scaling(size.x, size.y, size.z) * rotation.to_matrix!(3, 3) * mat3.translation(position); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /source/engine/math/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Math Subsystem 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.math; 10 | public import engine.math.camera; 11 | public import engine.math.transform; 12 | public import engine.math.obb; 13 | public import std.math; 14 | public import gl3n.math; 15 | public import gl3n.linalg; 16 | public import gl3n.aabb; 17 | public import gl3n.interpolate; 18 | import engine.core.log; 19 | 20 | /** 21 | Smoothly dampens from a position to a target 22 | */ 23 | vec3 dampen(vec3 pos, vec3 target, float delta, float speed = 1) { 24 | return (pos - target) * pow(1e-4f, delta*speed) + target; 25 | } 26 | 27 | /** 28 | A basic ray 29 | */ 30 | struct Ray { 31 | /** 32 | Origin of the ray 33 | */ 34 | vec3 origin; 35 | 36 | /** 37 | Direction of the ray (unit vector) 38 | */ 39 | vec3 direction; 40 | 41 | string toString() { 42 | import std.format : format; 43 | return "origin=<%s, %s, %s> dir=<%s, %s, %s>".format(origin.x, origin.y, origin.z, direction.x, direction.y, direction.z); 44 | } 45 | } 46 | /** 47 | Casts a screen space ray from the mouse position 48 | */ 49 | Ray castScreenSpaceRay(vec2 mouse, vec2 viewSize, mat4 vp) { 50 | 51 | // mouse to homogenus coordinates 52 | double x = mouse.x / (viewSize.x * 0.5) - 1.0; 53 | double y = mouse.y / (viewSize.y * 0.5) - 1.0; 54 | 55 | x = clamp(x, -1, 1); 56 | y = clamp(y, -1, 1); 57 | 58 | vec4 rayStart = vec4( 59 | x, 60 | -y, 61 | 0, 62 | 1 63 | ); 64 | 65 | vec4 rayEnd = vec4( 66 | x, 67 | -y, 68 | 1, 69 | 1 70 | ); 71 | 72 | // Get inverse vp matrix 73 | immutable(mat4) vpInverse = vp.inverse; 74 | vec4 rayStartWorld = vpInverse * rayStart; rayStartWorld /= rayStartWorld.w; 75 | vec4 rayEndWorld = vpInverse * rayEnd; rayEndWorld /= rayEndWorld.w; 76 | 77 | // Get the ray direction 78 | vec3 rayDir = vec3(rayEndWorld-rayStartWorld).normalized; 79 | return Ray(rayStartWorld.xyz, rayDir); 80 | } 81 | 82 | /** 83 | Gets whether a ray is intersecting a bounding box 84 | */ 85 | bool isRayIntersecting(OBB boundingBox, Ray ray, mat4 modelmatrix, ref float iDist) { 86 | float min = 0f; 87 | float max = 100_000f; 88 | 89 | vec3 oobWorldspace = vec3(modelmatrix[3][0], modelmatrix[3][1], modelmatrix[3][2]); 90 | vec3 delta = oobWorldspace-ray.origin; 91 | 92 | { 93 | vec3 xaxis = vec3(modelmatrix[0][0], modelmatrix[0][1], modelmatrix[0][2]); 94 | float e = dot(xaxis, delta); 95 | float f = dot(ray.direction, xaxis); 96 | 97 | if (fabs(f) > 0.0001f) { 98 | float t1 = (e+boundingBox.min.x)/f; 99 | float t2 = (e+boundingBox.max.x)/f; 100 | 101 | // Swap if t1 is larger than t2 102 | if (t1 > t2) { 103 | float w = t1; 104 | t1 = t2; 105 | t2 = w; 106 | } 107 | 108 | if (t2 < max) max = t2; 109 | if (t1 > min) min = t1; 110 | 111 | if (max < min) return false; 112 | } 113 | } 114 | 115 | { 116 | vec3 yaxis = vec3(modelmatrix[1][0], modelmatrix[1][1], modelmatrix[1][2]); 117 | float e = dot(yaxis, delta); 118 | float f = dot(ray.direction, yaxis); 119 | 120 | if (fabs(f) > 0.0001f) { 121 | float t1 = (e+boundingBox.min.y)/f; 122 | float t2 = (e+boundingBox.max.y)/f; 123 | 124 | // Swap if t1 is larger than t2 125 | if (t1 > t2) { 126 | float w = t1; 127 | t1 = t2; 128 | t2 = w; 129 | } 130 | 131 | if (t2 < max) max = t2; 132 | if (t1 > min) min = t1; 133 | 134 | if (max < min) return false; 135 | } 136 | } 137 | 138 | { 139 | vec3 zaxis = vec3(modelmatrix[2][0], modelmatrix[2][1], modelmatrix[2][2]); 140 | float e = dot(zaxis, delta); 141 | float f = dot(ray.direction, zaxis); 142 | 143 | if (fabs(f) > 0.0001f) { 144 | float t1 = (e+boundingBox.min.z)/f; 145 | float t2 = (e+boundingBox.max.z)/f; 146 | 147 | // Swap if t1 is larger than t2 148 | if (t1 > t2) { 149 | float w = t1; 150 | t1 = t2; 151 | t2 = w; 152 | } 153 | 154 | if (t2 < max) max = t2; 155 | if (t1 > min) min = t1; 156 | 157 | if (max < min) return false; 158 | } 159 | } 160 | 161 | iDist = min; 162 | return true; 163 | } -------------------------------------------------------------------------------- /source/engine/math/transform.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.math.transform; 8 | import gl3n.linalg; 9 | 10 | /** 11 | A 3D transform 12 | */ 13 | class Transform { 14 | private: 15 | Transform parent; 16 | 17 | // Generated matrix 18 | mat4 g_matrix() { 19 | return mat4.translation(position) * rotation.to_matrix!(4, 4) * mat4.scaling(scale.x, scale.y, scale.z); 20 | } 21 | 22 | // Generated matrix 23 | mat4 g_matrix_ns() { 24 | return mat4.translation(position) * rotation.to_matrix!(4, 4); 25 | } 26 | 27 | public: 28 | 29 | /** 30 | Create a new 3D transform 31 | */ 32 | this(Transform parent = null) { 33 | this(vec3(0, 0, 0), vec3(1, 1, 1), quat.identity, parent); 34 | } 35 | 36 | /** 37 | Create a new 3D transform 38 | */ 39 | this(vec3 position, Transform parent = null) { 40 | this(position, vec3(1, 1, 1), quat.identity, parent); 41 | } 42 | 43 | /** 44 | Create a new 3D transform 45 | */ 46 | this(vec3 position, vec3 scale, Transform parent = null) { 47 | this(position, scale, quat.identity, parent); 48 | } 49 | 50 | /** 51 | Create a new 3D transform 52 | */ 53 | this(vec3 position, vec3 scale, quat rotation, Transform parent = null) { 54 | this.parent = parent; 55 | this.position = position; 56 | this.scale = scale; 57 | this.rotation = rotation; 58 | } 59 | 60 | /** 61 | Position of transform 62 | */ 63 | vec3 position; 64 | 65 | /** 66 | Origin of transform 67 | */ 68 | vec3 origin; 69 | 70 | /** 71 | Scale of transform 72 | */ 73 | vec3 scale; 74 | 75 | /** 76 | Rotation of transform 77 | */ 78 | quat rotation; 79 | 80 | /** 81 | Changes the transform's parent 82 | */ 83 | void changeParent(Transform parent) { 84 | this.parent = parent; 85 | } 86 | 87 | /** 88 | Gets the calculated matrix for this transform 89 | */ 90 | mat4 matrix() { 91 | if (parent is null) return g_matrix; 92 | return g_matrix*parent.matrix; 93 | } 94 | 95 | /** 96 | Gets the calculated matrix for this transform without any scaling applied 97 | */ 98 | mat4 matrixUnscaled() { 99 | if (parent is null) return g_matrix_ns; 100 | return g_matrix_ns*parent.matrixUnscaled; 101 | } 102 | } 103 | 104 | /** 105 | A 2D transform 106 | */ 107 | class Transform2D { 108 | private: 109 | Transform2D parent; 110 | 111 | // Generated matrix 112 | mat4 g_matrix() { 113 | return 114 | mat4.zrotation(rotation) * 115 | mat4.scaling(scale.x, scale.y, 1) * 116 | mat4.translation(position.x, position.y, 0) * 117 | mat4.translation(origin.x, origin.y, 0); 118 | } 119 | 120 | public: 121 | 122 | /** 123 | Create a new 2D transform 124 | */ 125 | this(Transform2D parent = null) { 126 | this(vec2(0, 0), vec2(0, 0), vec2(1, 1), 0, parent); 127 | } 128 | 129 | /** 130 | Create a new 2D transform 131 | */ 132 | this(vec2 position, vec2 origin = vec2(0, 0), vec2 scale = vec2(1, 1), float rotation = 0, Transform2D parent = null) { 133 | this.position = position; 134 | this.origin = origin; 135 | this.scale = scale; 136 | this.rotation = rotation; 137 | this.parent = parent; 138 | } 139 | 140 | /** 141 | Position of transform 142 | */ 143 | vec2 position; 144 | 145 | /** 146 | Position of the transform origin 147 | */ 148 | vec2 origin; 149 | 150 | /** 151 | Scale of transform 152 | */ 153 | vec2 scale; 154 | 155 | /** 156 | Rotation of transform 157 | */ 158 | float rotation; 159 | 160 | /** 161 | Changes the transform's parent 162 | */ 163 | void changeParent(Transform2D parent) { 164 | this.parent = parent; 165 | } 166 | 167 | /** 168 | Gets the calculated matrix for this transform 169 | */ 170 | mat4 matrix() { 171 | if (parent is null) return g_matrix; 172 | return g_matrix*parent.matrix; 173 | } 174 | } -------------------------------------------------------------------------------- /source/engine/net/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Networking Subsystem 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.net; 10 | 11 | /* 12 | TODO: Implement core net-code which the game can utilize here. 13 | */ -------------------------------------------------------------------------------- /source/engine/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine; 8 | public import engine.core; 9 | public import engine.input; 10 | public import engine.render; 11 | public import engine.math; 12 | public import engine.audio; 13 | public import engine.net; 14 | public import engine.ui; 15 | public import engine.game; 16 | public import engine.i18n; 17 | 18 | import bindbc.glfw; 19 | import bindbc.openal; 20 | import bindbc.freetype; 21 | 22 | /** 23 | Initialize the game engine 24 | */ 25 | void initEngine() { 26 | // Initialize logger if needed 27 | if (AppLog is null) AppLog = new Logger(); 28 | 29 | // Initialize GLFW 30 | initGLFW(); 31 | glfwInit(); 32 | AppLog.info("Engine", "GLFW initialized..."); 33 | 34 | // Initialize OpenAL 35 | initOAL(); 36 | initAudioEngine(); 37 | AppLog.info("Engine", "Audio Engine initialized..."); 38 | 39 | // Create window 40 | GameWindow = new Window(); 41 | GameWindow.makeCurrent(); 42 | AppLog.info("Engine", "Window initialized..."); 43 | 44 | // Initialize OpenGL and make context current 45 | initOGL(); 46 | initRender(); 47 | AppLog.info("Engine", "Renderer initialized..."); 48 | 49 | // Initialize Font system 50 | initFT(); 51 | initFontSystem(); 52 | AppLog.info("Engine", "Font system initialized..."); 53 | 54 | // Initialize input 55 | initInput(GameWindow.winPtr); 56 | AppLog.info("Engine", "Input system initialized..."); 57 | 58 | // Initialize atlasser 59 | GameAtlas = new AtlasCollection(); 60 | AppLog.info("Engine", "Texture atlassing initialized..."); 61 | 62 | // Initialize subsystems 63 | initTileMesh(); 64 | AppLog.info("Engine", "Intialized internal state for renderer..."); 65 | 66 | initPlaylist(); 67 | AppLog.info("Engine", "Initialized smaller subsystems..."); 68 | } 69 | 70 | /** 71 | Closes the engine and relases libraries, etc. 72 | */ 73 | void closeEngine() { 74 | import core.memory : GC; 75 | destroy(GamePlaylist); 76 | destroy(GameWindow); 77 | destroy(AppLog); 78 | 79 | // Collect the stuff before we terminate all this other stuff 80 | // We let OpenGL, OpenAL and GLFW be terminated by the closing of the program 81 | GC.collect(); 82 | } 83 | 84 | private void initOAL() { 85 | auto support = loadOpenAL(); 86 | if (support == ALSupport.badLibrary) { 87 | AppLog.fatal("Engine", "Could not load OpenAL, bad library!"); 88 | } else if (support == ALSupport.noLibrary) { 89 | AppLog.fatal("Engine", "Could not load OpenAL, no library found!"); 90 | } 91 | } 92 | 93 | private void initGLFW() { 94 | auto support = loadGLFW(); 95 | if (support == GLFWSupport.badLibrary) { 96 | AppLog.fatal("Engine", "Could not load GLFW, bad library!"); 97 | } else if (support == GLFWSupport.noLibrary) { 98 | AppLog.fatal("Engine", "Could not load GLFW, no library found!"); 99 | } 100 | } 101 | 102 | private void initOGL() { 103 | auto support = loadOpenGL(); 104 | if (support == GLSupport.badLibrary) { 105 | AppLog.fatal("Engine", "Could not load OpenGL, bad library!"); 106 | } else if (support == GLSupport.noLibrary) { 107 | AppLog.fatal("Engine", "Could not load OpenGL, no library found!"); 108 | } else if (support == GLSupport.noContext) { 109 | AppLog.fatal("Engine", "OpenGL context was not created before loading OpenGL."); 110 | } 111 | } 112 | 113 | private void initFT() { 114 | auto support = loadFreeType(); 115 | if (support == FTSupport.badLibrary) { 116 | AppLog.fatal("Engine", "Could not load FreeType, bad library!"); 117 | } else if (support == FTSupport.noLibrary) { 118 | AppLog.fatal("Engine", "Could not load FreeType, no library found!"); 119 | } 120 | } -------------------------------------------------------------------------------- /source/engine/render/batcher.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.render.batcher; 8 | import engine; 9 | 10 | private { 11 | 12 | /// How many entries in a SpriteBatch 13 | enum EntryCount = 10_000; 14 | 15 | // Various variables that make it easier to reference sizes 16 | enum VecSize = 2; 17 | enum UVSize = 2; 18 | enum ColorSize = 4; 19 | enum VertsCount = 6; 20 | enum DataLength = VecSize+UVSize+ColorSize; 21 | enum DataSize = DataLength*VertsCount; 22 | 23 | Shader spriteBatchShader; 24 | Camera2D batchCamera; 25 | 26 | vec2 transformVerts(vec2 position, mat4 matrix) { 27 | return vec2(matrix*vec4(position.x, position.y, 0, 1)); 28 | } 29 | } 30 | 31 | /** 32 | Global game sprite batcher 33 | */ 34 | static SpriteBatch GameBatch; 35 | 36 | /** 37 | Sprite flipping 38 | */ 39 | enum SpriteFlip { 40 | None = 0, 41 | Horizontal = 1, 42 | Vertical = 2 43 | } 44 | 45 | /** 46 | Batches Texture objects for 2D drawing 47 | */ 48 | class SpriteBatch { 49 | private: 50 | float[DataSize*EntryCount] data; 51 | size_t dataOffset; 52 | size_t tris; 53 | 54 | GLuint vao; 55 | GLuint buffer; 56 | GLint vp; 57 | 58 | Texture currentTexture; 59 | Framebuffer currentFboTex; 60 | 61 | void addVertexData(vec2 position, vec2 uvs, vec4 color) { 62 | data[dataOffset..dataOffset+DataLength] = [position.x, position.y, uvs.x, uvs.y, color.x, color.y, color.z, color.w]; 63 | dataOffset += DataLength; 64 | } 65 | 66 | public: 67 | 68 | /** 69 | Constructor 70 | */ 71 | this() { 72 | data = new float[DataSize*EntryCount]; 73 | 74 | glGenVertexArrays(1, &vao); 75 | glBindVertexArray(vao); 76 | 77 | glGenBuffers(1, &buffer); 78 | glBindBuffer(GL_ARRAY_BUFFER, buffer); 79 | glBufferData(GL_ARRAY_BUFFER, float.sizeof*data.length, data.ptr, GL_DYNAMIC_DRAW); 80 | 81 | batchCamera = new Camera2D(); 82 | spriteBatchShader = new Shader(import("shaders/batch.vert"), import("shaders/batch.frag")); 83 | vp = spriteBatchShader.getUniformLocation("vp"); 84 | } 85 | 86 | /** 87 | Draws texture from atlas 88 | */ 89 | void draw(string item, vec4 position, vec4 cutout = vec4.init, vec2 origin = vec2(0, 0), float rotation = 0f, SpriteFlip flip = SpriteFlip.None, vec4 color = vec4(1, 1, 1, 1)) { 90 | auto index = GameAtlas[item]; 91 | draw(index, position, cutout, origin, rotation, flip, color); 92 | } 93 | 94 | /** 95 | Draws cached atlas index 96 | */ 97 | void draw(AtlasIndex index, vec4 position, vec4 cutout = vec4.init, vec2 origin = vec2(0, 0), float rotation = 0f, SpriteFlip flip = SpriteFlip.None, vec4 color = vec4(1)) { 98 | 99 | vec4 fCutout = index.area; 100 | if (cutout.isFinite) { 101 | 102 | // Clamp the cutout to fit within the texture's area 103 | vec4 cutoutClamped = vec4( 104 | clamp(cutout.x, 0, index.area.z), 105 | clamp(cutout.y, 0, index.area.w), 106 | clamp(cutout.z, 0, index.area.z), 107 | clamp(cutout.w, 0, index.area.w), 108 | ); 109 | 110 | // Cut in to the area 111 | fCutout = vec4( 112 | index.area.x+cutoutClamped.x, 113 | index.area.y+cutoutClamped.y, 114 | cutoutClamped.z, 115 | cutoutClamped.w, 116 | ); 117 | } 118 | 119 | draw(index.texture, position, fCutout, origin, rotation, flip, color); 120 | } 121 | 122 | /** 123 | Draws the texture 124 | 125 | Remember to call flush after drawing all the textures you want 126 | 127 | Flush will automatically be called if your draws exceed the max count 128 | Flush will automatically be called if you queue an other texture 129 | */ 130 | void draw(Texture texture, vec4 position, vec4 cutout = vec4.init, vec2 origin = vec2(0, 0), float rotation = 0f, SpriteFlip flip = SpriteFlip.None, vec4 color = vec4(1)) { 131 | 132 | // Flush if neccesary 133 | if (dataOffset == DataSize*EntryCount) flush(); 134 | if (texture != currentTexture) flush(); 135 | 136 | // Update current texture 137 | currentTexture = texture; 138 | 139 | // Calculate rotation, position and scaling. 140 | mat4 transform = 141 | mat4.translation(-origin.x, -origin.y, 0) * 142 | mat4.translation(position.x, position.y, 0) * 143 | mat4.translation(origin.x, origin.y, 0) * 144 | mat4.zrotation(rotation) * 145 | mat4.translation(-origin.x, -origin.y, 0) * 146 | mat4.scaling(position.z, position.w, 0); 147 | 148 | // If cutout has not been set (all values are NaN or infinity) we set it to use the entire texture 149 | if (!cutout.isFinite) { 150 | cutout = vec4(0, 0, texture.width, texture.height); 151 | } 152 | 153 | // Get the area of the texture with a tiny bit cut off to avoid textures bleeding in to each other 154 | // TODO: add a 1x1 px transparent border around textures instead? 155 | enum cutoffOffset = 0.8; 156 | enum cutoffAmount = cutoffOffset*2; 157 | 158 | vec4 uvArea = vec4( 159 | (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x+cutout.z)-cutoffAmount : (cutout.x)+cutoffOffset, 160 | (flip & SpriteFlip.Vertical) > 0 ? (cutout.y+cutout.w)-cutoffAmount : (cutout.y)+cutoffOffset, 161 | (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x)+cutoffOffset : (cutout.x+cutout.z)-cutoffAmount, 162 | (flip & SpriteFlip.Vertical) > 0 ? (cutout.y)+cutoffOffset : (cutout.y+cutout.w)-cutoffAmount, 163 | ); 164 | 165 | // Triangle 1 166 | addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color); 167 | addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color); 168 | addVertexData(vec2(0, 0).transformVerts(transform), vec2(uvArea.x, uvArea.y), color); 169 | 170 | // Triangle 2 171 | addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color); 172 | addVertexData(vec2(1, 1).transformVerts(transform), vec2(uvArea.z, uvArea.w), color); 173 | addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color); 174 | 175 | tris += 2; 176 | } 177 | 178 | /** 179 | Draws a framebuffer texture 180 | 181 | Automatically flushes after draw 182 | */ 183 | void draw(Framebuffer fbo, vec4 position, vec4 cutout = vec4.init, vec2 origin = vec2(0, 0), float rotation = 0f, SpriteFlip flip = SpriteFlip.None, vec4 color = vec4(1, 1, 1, 1)) { 184 | 185 | // Flush if neccesary 186 | if (dataOffset == DataSize*EntryCount) flush(); 187 | if (currentTexture !is null) flush(); 188 | 189 | // Update current texture 190 | currentFboTex = fbo; 191 | 192 | // Calculate rotation, position and scaling. 193 | mat4 transform = 194 | mat4.translation(-origin.x, -origin.y, 0) * 195 | mat4.translation(position.x, position.y, 0) * 196 | mat4.translation(origin.x, origin.y, 0) * 197 | mat4.zrotation(rotation) * 198 | mat4.translation(-origin.x, -origin.y, 0) * 199 | mat4.scaling(position.z, position.w, 0); 200 | 201 | // If cutout has not been set (all values are NaN or infinity) we set it to use the entire texture 202 | if (!cutout.isFinite) { 203 | cutout = vec4(0, fbo.realHeight, fbo.realWidth, -fbo.realHeight); 204 | } 205 | 206 | // Get the area of the texture with a tiny bit cut off to avoid textures bleeding in to each other 207 | // TODO: add a 1x1 px transparent border around textures instead? 208 | enum cutoffOffset = 0.8; 209 | enum cutoffAmount = cutoffOffset*2; 210 | 211 | vec4 uvArea = vec4( 212 | (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x+cutout.z)-cutoffAmount : (cutout.x)+cutoffOffset, 213 | (flip & SpriteFlip.Vertical) > 0 ? (cutout.y+cutout.w)-cutoffAmount : (cutout.y)+cutoffOffset, 214 | (flip & SpriteFlip.Horizontal) > 0 ? (cutout.x)+cutoffOffset : (cutout.x+cutout.z)-cutoffAmount, 215 | (flip & SpriteFlip.Vertical) > 0 ? (cutout.y)+cutoffOffset : (cutout.y+cutout.w)-cutoffAmount, 216 | ); 217 | 218 | // Triangle 1 219 | addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color); 220 | addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color); 221 | addVertexData(vec2(0, 0).transformVerts(transform), vec2(uvArea.x, uvArea.y), color); 222 | 223 | // Triangle 2 224 | addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color); 225 | addVertexData(vec2(1, 1).transformVerts(transform), vec2(uvArea.z, uvArea.w), color); 226 | addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color); 227 | 228 | tris += 2; 229 | 230 | // Auto flush 231 | this.flush!true(); 232 | } 233 | 234 | /** 235 | Flush the buffer 236 | */ 237 | void flush(bool isFbo=false)() { 238 | 239 | // Disable depth testing for the batcher 240 | glDisable(GL_DEPTH_TEST); 241 | 242 | // Don't draw empty textures 243 | static if (!isFbo) { 244 | if (currentTexture is null) return; 245 | } 246 | 247 | // Bind VAO 248 | glBindVertexArray(vao); 249 | 250 | // Bind just in case some shennanigans happen 251 | glBindBuffer(GL_ARRAY_BUFFER, buffer); 252 | 253 | // Update with this draw round's data 254 | glBufferSubData(GL_ARRAY_BUFFER, 0, dataOffset*float.sizeof, data.ptr); 255 | 256 | // Bind the texture 257 | static if (!isFbo) { 258 | currentTexture.bind(); 259 | } else { 260 | glActiveTexture(GL_TEXTURE0); 261 | glBindTexture(GL_TEXTURE_2D, currentFboTex.getTexId()); 262 | } 263 | 264 | // Use our sprite batcher shader and bind our camera matrix 265 | spriteBatchShader.use(); 266 | spriteBatchShader.setUniform(vp, batchCamera.matrix); 267 | 268 | // Vertex buffer 269 | glEnableVertexAttribArray(0); 270 | glVertexAttribPointer( 271 | 0, 272 | VecSize, 273 | GL_FLOAT, 274 | GL_FALSE, 275 | DataLength*GLfloat.sizeof, 276 | null, 277 | ); 278 | 279 | // UV buffer 280 | glEnableVertexAttribArray(1); 281 | glVertexAttribPointer( 282 | 1, 283 | UVSize, 284 | GL_FLOAT, 285 | GL_FALSE, 286 | DataLength*GLfloat.sizeof, 287 | cast(GLvoid*)(UVSize*GLfloat.sizeof), 288 | ); 289 | 290 | // Color buffer 291 | glEnableVertexAttribArray(2); 292 | glVertexAttribPointer( 293 | 2, 294 | ColorSize, 295 | GL_FLOAT, 296 | GL_FALSE, 297 | DataLength*GLfloat.sizeof, 298 | cast(GLvoid*)((VecSize+UVSize)*GLfloat.sizeof), 299 | ); 300 | 301 | // Draw the triangles 302 | glDrawArrays(GL_TRIANGLES, 0, cast(int)(tris*3)); 303 | 304 | // Reset the batcher's state 305 | glDisableVertexAttribArray(0); 306 | glDisableVertexAttribArray(1); 307 | glDisableVertexAttribArray(2); 308 | currentTexture = null; 309 | dataOffset = 0; 310 | tris = 0; 311 | 312 | // Re-enable depth testing for 3D rendering 313 | glEnable(GL_DEPTH_TEST); 314 | } 315 | } -------------------------------------------------------------------------------- /source/engine/render/fbo.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.render.fbo; 8 | import engine.render; 9 | import engine.math; 10 | import engine.core.window; 11 | 12 | /** 13 | A framebuffer 14 | */ 15 | class Framebuffer { 16 | private: 17 | Window window; 18 | 19 | vec2i size; 20 | vec2i realsize; 21 | 22 | GLuint fbo; 23 | GLuint color; 24 | GLuint depth; 25 | 26 | package(engine): 27 | 28 | /** 29 | Gets the texture ID associated with the fbo 30 | */ 31 | GLuint getTexId() { 32 | return color; 33 | } 34 | 35 | public: 36 | 37 | /** 38 | The constructor 39 | */ 40 | this(Window window, vec2i size) { 41 | this.window = window; 42 | this.size = size; 43 | this.realsize = size*2; 44 | 45 | // Bind FBO 46 | glGenFramebuffers(1, &fbo); 47 | glBindFramebuffer(GL_FRAMEBUFFER, fbo); 48 | 49 | // Enable blending, etc. 50 | glEnable(GL_BLEND); 51 | glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); 52 | glEnable(GL_CULL_FACE); 53 | glEnable(GL_DEPTH_TEST); 54 | glDepthFunc(GL_LESS); 55 | 56 | // Generate texture 57 | glGenTextures(1, &color); 58 | glBindTexture(GL_TEXTURE_2D, color); 59 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, realsize.x, realsize.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, null); 60 | 61 | // Set up texture parameters 62 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 63 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 64 | 65 | // Generate depth buffer 66 | glGenRenderbuffers(1, &depth); 67 | glBindRenderbuffer(GL_RENDERBUFFER, depth); 68 | glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, realsize.x, realsize.y); 69 | 70 | // Configure framebuffer 71 | glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth); 72 | glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0); 73 | } 74 | 75 | /** 76 | Bind the framebuffer 77 | */ 78 | void bind() { 79 | glBindFramebuffer(GL_FRAMEBUFFER, fbo); 80 | kmViewport(0, 0, realsize.x, realsize.y); 81 | kmSetCameraTargetSize(size.x, size.y); 82 | } 83 | 84 | /** 85 | Unbind the framebuffer 86 | */ 87 | void unbind() { 88 | glBindFramebuffer(GL_FRAMEBUFFER, 0); 89 | kmViewport(0, 0, window.width, window.height); 90 | } 91 | 92 | /** 93 | Renders the framebuffer to fit in the current viewport 94 | */ 95 | void renderToFit() { 96 | double widthScale = cast(double)kmViewportWidth / cast(double)realWidth; 97 | double heightScale = cast(double)kmViewportHeight / cast(double)realHeight; 98 | double scale = min(widthScale, heightScale); 99 | 100 | vec4 bounds = vec4( 101 | 0, 102 | 0, 103 | realWidth*scale, 104 | realHeight*scale 105 | ); 106 | 107 | if (widthScale > heightScale) bounds.x = (kmViewportWidth-bounds.z)/2; 108 | else if (heightScale > widthScale) bounds.y = (kmViewportHeight-bounds.w)/2; 109 | 110 | GameBatch.draw(this, bounds); 111 | } 112 | 113 | /** 114 | Width of framebuffer 115 | */ 116 | int width() { 117 | return size.x; 118 | } 119 | 120 | /** 121 | Height of framebuffer 122 | */ 123 | int height() { 124 | return size.y; 125 | } 126 | 127 | /** 128 | Real width of framebuffer 129 | */ 130 | int realWidth() { 131 | return realsize.x; 132 | } 133 | 134 | /** 135 | Real height of framebuffer 136 | */ 137 | int realHeight() { 138 | return realsize.y; 139 | } 140 | } -------------------------------------------------------------------------------- /source/engine/render/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Rendering Subsystem 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.render; 10 | public import bindbc.opengl; 11 | public import engine.render.shader; 12 | public import engine.render.tile; 13 | public import engine.render.texture; 14 | public import engine.render.batcher; 15 | public import engine.render.fbo; 16 | 17 | void initRender() { 18 | 19 | // OpenGL prep stuff 20 | glEnable(GL_BLEND); 21 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 22 | 23 | glEnable(GL_CULL_FACE); 24 | glEnable(GL_DEPTH_TEST); 25 | glDepthFunc(GL_LESS); 26 | 27 | GameBatch = new SpriteBatch(); 28 | } 29 | 30 | private int viewportX; 31 | private int viewportY; 32 | private int viewportW; 33 | private int viewportH; 34 | 35 | /** 36 | Sets the viewport 37 | */ 38 | void kmViewport(int x, int y, int width, int height) { 39 | import engine.math.camera : kmSetCameraTargetSize; 40 | 41 | viewportX = x; 42 | viewportY = y; 43 | viewportW = width; 44 | viewportH = height; 45 | glViewport(x, y, width, height); 46 | kmSetCameraTargetSize(width, height); 47 | } 48 | 49 | /** 50 | Returns the viewport X 51 | */ 52 | int kmViewportX() { 53 | return viewportX; 54 | } 55 | 56 | /** 57 | Returns the viewport Y 58 | */ 59 | int kmViewportY() { 60 | return viewportY; 61 | } 62 | 63 | /** 64 | Returns the viewport width 65 | */ 66 | int kmViewportWidth() { 67 | return viewportW; 68 | } 69 | 70 | /** 71 | Returns the viewport height 72 | */ 73 | int kmViewportHeight() { 74 | return viewportH; 75 | } -------------------------------------------------------------------------------- /source/engine/render/shader.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.render.shader; 8 | import engine; 9 | import std.string; 10 | import gl3n.linalg; 11 | 12 | /** 13 | A shader 14 | */ 15 | class Shader { 16 | private: 17 | GLuint shaderProgram; 18 | GLuint fragShader; 19 | GLuint vertShader; 20 | 21 | void compileShaders(string vertex, string fragment) { 22 | 23 | // Compile vertex shader 24 | vertShader = glCreateShader(GL_VERTEX_SHADER); 25 | auto c_vert = vertex.toStringz; 26 | glShaderSource(vertShader, 1, &c_vert, null); 27 | glCompileShader(vertShader); 28 | verifyShader(vertShader); 29 | 30 | // Compile fragment shader 31 | fragShader = glCreateShader(GL_FRAGMENT_SHADER); 32 | auto c_frag = fragment.toStringz; 33 | glShaderSource(fragShader, 1, &c_frag, null); 34 | glCompileShader(fragShader); 35 | verifyShader(fragShader); 36 | 37 | // Attach and link them 38 | shaderProgram = glCreateProgram(); 39 | glAttachShader(shaderProgram, vertShader); 40 | glAttachShader(shaderProgram, fragShader); 41 | glLinkProgram(shaderProgram); 42 | verifyProgram(); 43 | } 44 | 45 | void verifyShader(GLuint shader) { 46 | 47 | // Get the length of the error log 48 | GLint logLength; 49 | glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength); 50 | if (logLength > 0) { 51 | 52 | // Fetch the error log 53 | char[] log = new char[logLength]; 54 | glGetShaderInfoLog(shader, logLength, null, log.ptr); 55 | 56 | AppLog.fatal("Engine::Shader", cast(string)log); 57 | } 58 | } 59 | 60 | void verifyProgram() { 61 | 62 | // Get the length of the error log 63 | GLint logLength; 64 | glGetProgramiv(shaderProgram, GL_INFO_LOG_LENGTH, &logLength); 65 | if (logLength > 0) { 66 | 67 | // Fetch the error log 68 | char[] log = new char[logLength]; 69 | glGetProgramInfoLog(shaderProgram, logLength, null, log.ptr); 70 | 71 | AppLog.fatal("Engine::Shader", cast(string)log); 72 | } 73 | } 74 | 75 | public: 76 | 77 | /** 78 | Destructor 79 | */ 80 | ~this() { 81 | glDetachShader(shaderProgram, vertShader); 82 | glDetachShader(shaderProgram, fragShader); 83 | glDeleteProgram(shaderProgram); 84 | 85 | glDeleteShader(fragShader); 86 | glDeleteShader(vertShader); 87 | } 88 | 89 | /** 90 | Creates a new shader object from source 91 | */ 92 | this(string vertex, string fragment) { 93 | compileShaders(vertex, fragment); 94 | } 95 | 96 | /** 97 | Use the shader 98 | */ 99 | void use() { 100 | glUseProgram(shaderProgram); 101 | } 102 | 103 | GLint getUniformLocation(string name) { 104 | return glGetUniformLocation(shaderProgram, name.ptr); 105 | } 106 | 107 | void setUniform(GLint uniform, bool value) { 108 | glUniform1i(uniform, cast(int)value); 109 | } 110 | 111 | void setUniform(GLint uniform, int value) { 112 | glUniform1i(uniform, value); 113 | } 114 | 115 | void setUniform(GLint uniform, float value) { 116 | glUniform1f(uniform, value); 117 | } 118 | 119 | void setUniform(GLint uniform, vec2 value) { 120 | glUniform2f(uniform, value.x, value.y); 121 | } 122 | 123 | void setUniform(GLint uniform, vec3 value) { 124 | glUniform3f(uniform, value.x, value.y, value.z); 125 | } 126 | 127 | void setUniform(GLint uniform, vec4 value) { 128 | glUniform4f(uniform, value.x, value.y, value.z, value.w); 129 | } 130 | 131 | void setUniform(GLint uniform, mat4 value) { 132 | glUniformMatrix4fv(uniform, 1, GL_TRUE, value.value_ptr); 133 | } 134 | } -------------------------------------------------------------------------------- /source/engine/render/texture/atlas.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.render.texture.atlas; 8 | import engine.render.texture; 9 | import gl3n.linalg; 10 | import std.exception; 11 | import std.format; 12 | import engine; 13 | 14 | /** 15 | The game's static texture atlas collection 16 | */ 17 | static AtlasCollection GameAtlas; 18 | 19 | /** 20 | An index in the atlas collection 21 | 22 | It is safe to keep this value around for caching 23 | */ 24 | struct AtlasIndex { 25 | private: 26 | TextureAtlas parentAtlas; 27 | 28 | package(engine.render): 29 | Texture texture() { 30 | return parentAtlas.texture; 31 | } 32 | 33 | public: 34 | /** 35 | The UV points of the texture 36 | */ 37 | vec4 uv; 38 | 39 | /** 40 | The area of the texture in the atlas 41 | */ 42 | vec4 area; 43 | 44 | /** 45 | Bind the atlas texture 46 | */ 47 | void bind(uint unit = 0) { 48 | parentAtlas.bind(unit); 49 | } 50 | } 51 | 52 | /***/ 53 | class AtlasCollection { 54 | private: 55 | TextureAtlas[] atlasses; 56 | AtlasIndex[string] texTable; 57 | Filtering defaultFilter = Filtering.Point; 58 | 59 | public: 60 | /** 61 | Gets the uv and atlas pointer for the index 62 | */ 63 | AtlasIndex opIndex(string name) { 64 | return texTable[name]; 65 | } 66 | 67 | /** 68 | Gets whether the atlas has the specified name 69 | */ 70 | bool has(string name) { 71 | return (name in texTable) !is null; 72 | } 73 | 74 | /** 75 | Add texture to the atlas from a file 76 | */ 77 | void add(string name, string file, size_t atlas=0) { 78 | add(name, ShallowTexture(file), atlas); 79 | } 80 | 81 | /** 82 | Sets the filtering mode for the collection 83 | */ 84 | void setFiltering(Filtering filtering) { 85 | defaultFilter = filtering; 86 | foreach(atlas; atlasses) { 87 | atlas.setFiltering(filtering); 88 | } 89 | } 90 | 91 | /** 92 | Add texture to the atlas collection 93 | */ 94 | void add(string name, ShallowTexture shallowTexture, size_t atlas=0) { 95 | enforce(name !in texTable, "Texture with name '%s' is already in the atlas collection".format(name)); 96 | 97 | // Add new atlas 98 | if (atlas >= atlasses.length) { 99 | AppLog.info("AtlasCollection", "All atlases were out of space, creating new atlas %s...", atlasses.length); 100 | atlasses ~= new TextureAtlas(vec2i(4096, 4096)); 101 | atlasses[$-1].setFiltering(defaultFilter); 102 | } 103 | 104 | // Add to atlas and get uvs 105 | AtlasArea area = atlasses[atlas].add(name, shallowTexture); 106 | 107 | // Height is 0 if it couldn't fit 108 | if (!area.area.isFinite) { 109 | 110 | // Try the next atlas 111 | add(name, shallowTexture, atlas+1); 112 | return; 113 | } 114 | 115 | // Put the texture and its uvs in to the table 116 | texTable[name] = AtlasIndex(atlasses[atlas], area.uv, area.area); 117 | 118 | } 119 | 120 | /** 121 | Remove named texture 122 | */ 123 | void remove(string name) { 124 | if (name in texTable) { 125 | texTable[name].parentAtlas.remove(name); 126 | } 127 | } 128 | } 129 | 130 | /** 131 | An area in a texture atlas 132 | */ 133 | struct AtlasArea { 134 | /** 135 | The area in pixels 136 | */ 137 | vec4 area; 138 | 139 | /** 140 | The UV coordinates 141 | */ 142 | vec4 uv; 143 | } 144 | 145 | /** 146 | A texture atlas 147 | */ 148 | class TextureAtlas { 149 | package(engine.render): 150 | Texture texture; 151 | 152 | private: 153 | TexturePacker packer; 154 | AtlasArea[string] entries; 155 | 156 | public: 157 | 158 | /** 159 | Creates a new texture atlas 160 | */ 161 | this(vec2i textureSize) { 162 | texture = new Texture(textureSize.x, textureSize.y); 163 | packer = new TexturePacker(textureSize); 164 | } 165 | 166 | /** 167 | Gets the UV points for the index 168 | */ 169 | AtlasArea opIndex(string name) { 170 | return entries[name]; 171 | } 172 | 173 | /** 174 | Bind the atlas texture 175 | */ 176 | void bind(uint unit = 0) { 177 | texture.bind(unit); 178 | } 179 | 180 | /** 181 | Set filtering used for the texture 182 | */ 183 | void setFiltering(Filtering filtering) { 184 | texture.setFiltering(filtering); 185 | } 186 | 187 | /** 188 | Add texture to the atlas from a file 189 | */ 190 | AtlasArea add(string name, string file) { 191 | return add(name, ShallowTexture(file)); 192 | } 193 | 194 | /** 195 | Add texture to the atlas 196 | */ 197 | AtlasArea add(string name, ShallowTexture shallowTexture) { 198 | enforce(name !in entries, "Texture with name '%s' is already in the atlas".format(name)); 199 | 200 | // Get packing position of texture 201 | vec4i texpos = packer.packTexture(vec2i(shallowTexture.width, shallowTexture.height)); 202 | 203 | // Texture does not fit in this atlas. 204 | if (!texpos.isFinite) return AtlasArea(vec4.init, vec4.init); 205 | 206 | // Put it in to the texture and set its entry 207 | texture.setDataRegion(shallowTexture.data, texpos.x, texpos.y, shallowTexture.width, shallowTexture.height); 208 | 209 | debug { 210 | AppLog.info("debug", "Packed texture %s in to region (%s, %s, %s, %s)", name, texpos.x, texpos.y, shallowTexture.width, shallowTexture.height); 211 | } 212 | 213 | // Calculate UV coordinates and put them in to the table 214 | vec2 texSize = vec2(cast(float)texture.width, cast(float)texture.height); 215 | vec4 texArea = vec4( 216 | texpos.x, 217 | texpos.y, 218 | shallowTexture.width, 219 | shallowTexture.height 220 | ); 221 | 222 | vec4 uvPoints = vec4( 223 | texArea.x/texSize.x, 224 | texArea.y/texSize.y, 225 | (texArea.x+texArea.z)/texSize.x, 226 | (texArea.y+texArea.w)/texSize.y 227 | ); 228 | 229 | entries[name] = AtlasArea(texArea, uvPoints); 230 | return entries[name]; 231 | } 232 | 233 | /** 234 | Remove an entry from the atlas 235 | */ 236 | void remove(string name) { 237 | packer.remove(vec4i( 238 | cast(int)entries[name].area.x, 239 | cast(int)entries[name].area.y, 240 | cast(int)entries[name].area.z, 241 | cast(int)entries[name].area.w 242 | ) 243 | ); 244 | entries.remove(name); 245 | } 246 | 247 | /** 248 | Clears the texture atlas 249 | */ 250 | void clear() { 251 | packer.clear(); 252 | entries.clear(); 253 | } 254 | } -------------------------------------------------------------------------------- /source/engine/render/texture/font.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.render.texture.font; 8 | import bindbc.freetype; 9 | import std.exception; 10 | import std.conv : text; 11 | import std.string; 12 | import std.format; 13 | import engine; 14 | 15 | private { 16 | FT_Library lib; 17 | Shader fontShader; 18 | Camera2D fontCamera; 19 | 20 | // VAO and VP can be reused 21 | GLuint vao; 22 | GLint vp; 23 | 24 | /// How many entries in a Font batch 25 | enum EntryCount = 10_000; 26 | 27 | // Various variables that make it easier to reference sizes 28 | enum VecSize = 2; 29 | enum UVSize = 2; 30 | enum ColorSize = 4; 31 | enum VertsCount = 6; 32 | enum DataLength = VecSize+UVSize+ColorSize; 33 | enum DataSize = DataLength*VertsCount; 34 | 35 | vec2 transformVerts(vec2 position, mat4 matrix) { 36 | return vec2(matrix*vec4(position.x, position.y, 0, 1)); 37 | } 38 | 39 | vec2 transformVertsR(vec2 position, mat4 matrix) { 40 | return vec2(vec4(position.x, position.y, 0, 1)*matrix); 41 | } 42 | } 43 | 44 | /** 45 | The game's public font 46 | */ 47 | Font GameFont; 48 | 49 | /** 50 | Initialize the font subsystem 51 | */ 52 | void initFontSystem() { 53 | int err = FT_Init_FreeType(&lib); 54 | if (err != FT_Err_Ok) { 55 | AppLog.fatal("Font System", "FreeType returned error %s", err); 56 | } 57 | 58 | // Set up batching 59 | glGenVertexArrays(1, &vao); 60 | 61 | fontCamera = new Camera2D(); 62 | fontShader = new Shader(import("shaders/font.vert"), import("shaders/font.frag")); 63 | vp = fontShader.getUniformLocation("vp"); 64 | 65 | // The game's font 66 | GameFont = new Font(cast(ubyte[])import("fonts/KosugiMaru.ttf"), 24); 67 | } 68 | 69 | /** 70 | A font 71 | */ 72 | class Font { 73 | private: 74 | // 75 | // Glyph managment 76 | // 77 | struct Glyph { 78 | vec4i area; 79 | vec2 advance; 80 | vec2 bearing; 81 | } 82 | 83 | struct GlyphIndex { 84 | dchar c; 85 | int size; 86 | } 87 | 88 | vec2 metrics; 89 | 90 | ubyte[] faceMemory; 91 | 92 | FT_Face fontFace; 93 | TexturePacker fontPacker; 94 | Texture fontTexture; 95 | Glyph[GlyphIndex] glyphs; 96 | int size; 97 | 98 | // Generates glyph for the specified character 99 | bool genGlyphFor(dchar c) { 100 | 101 | // Find the character's index in the font 102 | uint index = FT_Get_Char_Index(fontFace, c); 103 | 104 | // Load it if possible, otherwise tell the outside code 105 | FT_Load_Glyph(fontFace, index, FT_LOAD_RENDER); 106 | if (fontFace.glyph is null) return false; 107 | 108 | // Get width/height of character 109 | immutable(uint) width = fontFace.glyph.bitmap.width; 110 | immutable(uint) height = fontFace.glyph.bitmap.rows; 111 | 112 | // Get length of pixel data, then get it in to a slice 113 | size_t dataLength = fontFace.glyph.bitmap.pitch*fontFace.glyph.bitmap.rows; 114 | ubyte[] pixData = fontFace.glyph.bitmap.buffer[0..dataLength]; 115 | 116 | // Find space in the texture and pack the glyph in there 117 | vec4i area = fontPacker.packTexture(vec2i(width, height)); 118 | fontTexture.setDataRegion(pixData, area.x, area.y, width, height); 119 | 120 | // Add glyph to listing 121 | glyphs[GlyphIndex(c, size)] = Glyph( 122 | // Area (in texture) 123 | area, 124 | 125 | // Advance 126 | vec2(fontFace.glyph.advance.x >> 6, fontFace.glyph.advance.y >> 6), 127 | 128 | // Bearing 129 | vec2(fontFace.glyph.bitmap_left, fontFace.glyph.bitmap_top) 130 | ); 131 | return true; 132 | } 133 | 134 | // 135 | // Font Batching 136 | // 137 | float[DataSize*EntryCount] data; 138 | size_t dataOffset; 139 | size_t tris; 140 | 141 | GLuint buffer; 142 | 143 | void addVertexData(vec2 position, vec2 uvs, vec4 color) { 144 | data[dataOffset..dataOffset+DataLength] = [position.x, position.y, uvs.x, uvs.y, color.x, color.y, color.z, color.w]; 145 | dataOffset += DataLength; 146 | } 147 | 148 | void init_(int canvasSize) { 149 | 150 | // Select unicode so we can render i18n text 151 | FT_Select_Charmap(fontFace, FT_ENCODING_UNICODE); 152 | 153 | // Create the texture 154 | fontTexture = new Texture(canvasSize, canvasSize, GL_RED, 1); 155 | fontPacker = new TexturePacker(vec2i(canvasSize, canvasSize)); 156 | 157 | glBindVertexArray(vao); 158 | glGenBuffers(1, &buffer); 159 | glBindBuffer(GL_ARRAY_BUFFER, buffer); 160 | glBufferData(GL_ARRAY_BUFFER, float.sizeof*data.length, data.ptr, GL_DYNAMIC_DRAW); 161 | } 162 | 163 | public: 164 | 165 | /** 166 | Gets the font metrics 167 | */ 168 | vec2 getMetrics() { 169 | return metrics; 170 | } 171 | 172 | /** 173 | Destroys the font 174 | */ 175 | ~this() { 176 | destroy(fontTexture); 177 | FT_Done_Face(fontFace); 178 | glDeleteBuffers(1, &buffer); 179 | 180 | // Destroy face memory if we loaded it from memory 181 | if (faceMemory !is null) { 182 | destroy(faceMemory); 183 | } 184 | } 185 | 186 | /** 187 | Constructs a new font 188 | 189 | canvasSize specifies how big the texture for the font will be. 190 | */ 191 | this(string file, int size, int canvasSize = 4096) { 192 | int err = FT_New_Face(lib, file.toStringz, 0, &fontFace); 193 | 194 | enforce(err != FT_Err_Unknown_File_Format, "Unknown file format for %s".format(file)); 195 | enforce(err == FT_Err_Ok, "Error %s while loading font file".format(err)); 196 | 197 | // Change size of text 198 | this.changeSize(size); 199 | 200 | // Initializes the texture 201 | this.init_(canvasSize); 202 | } 203 | 204 | /** 205 | Constructs a new font 206 | 207 | canvasSize specifies how big the texture for the font will be. 208 | */ 209 | this(ubyte[] memFace, int size, int canvasSize = 4096) { 210 | 211 | // Copy data from memFace 212 | import std.algorithm.mutation : copy; 213 | faceMemory = new ubyte[memFace.length]; 214 | copy(memFace, faceMemory); 215 | 216 | AppLog.info("FontLoader", "Loading font from memory with size %s", memFace.length); 217 | int err = FT_New_Memory_Face(lib, faceMemory.ptr, faceMemory.length, 0, &fontFace); 218 | 219 | enforce(err != FT_Err_Unknown_File_Format, "Unknown file format"); 220 | enforce(err == FT_Err_Ok, "Error %s while loading font file".format(err)); 221 | 222 | // Change size of text 223 | this.changeSize(size); 224 | 225 | // Initializes the texture 226 | this.init_(canvasSize); 227 | } 228 | 229 | 230 | // 231 | // Glyph managment 232 | // 233 | 234 | /** 235 | Changes size of font 236 | */ 237 | final void changeSize(int size) { 238 | 239 | // Don't try to change size when it's the same 240 | if (size == this.size) return; 241 | 242 | // Set the size of the font 243 | FT_Set_Pixel_Sizes(fontFace, 0, size); 244 | metrics = vec2(size, fontFace.size.metrics.height >> 6); 245 | this.size = size; 246 | } 247 | 248 | /** 249 | Gets the advance of a glyph 250 | */ 251 | vec2 advance(dchar glyph) { 252 | 253 | // Newline is special 254 | if (glyph == '\n') return vec2(0); 255 | 256 | auto idx = GlyphIndex(glyph, size); 257 | 258 | // Make sure glyph is present 259 | if (idx !in glyphs) enforce(genGlyphFor(glyph), "Could not find glyph for character %s".format(glyph)); 260 | 261 | // Return the advance of the glyphs 262 | return glyphs[idx].advance; 263 | } 264 | 265 | /** 266 | Measure size (width, height) of rectangle needed to contain the specified text 267 | */ 268 | vec2 measure(dstring text) { 269 | 270 | int lines = 1; 271 | float curLineLen = 0; 272 | vec2 size = vec2(0); 273 | 274 | foreach(dchar c; text) { 275 | if (c == '\n') { 276 | lines++; 277 | curLineLen = 0; 278 | continue; 279 | } 280 | 281 | auto idx = GlyphIndex(c, this.size); 282 | 283 | // Try to generate glyph if not present 284 | if (idx !in glyphs) { 285 | genGlyphFor(c); 286 | 287 | // At this point if the glyph does not exist, skip it 288 | if (idx !in glyphs) continue; 289 | } 290 | 291 | // Bitshift the X advance to make it be in pixels. 292 | curLineLen += glyphs[idx].advance.x; 293 | 294 | // Update the bounding box if the length extends 295 | if (curLineLen > size.x) size.x = curLineLen; 296 | } 297 | size.y = metrics.y*lines; 298 | return size; 299 | } 300 | 301 | 302 | // 303 | // Font Batching 304 | // 305 | 306 | /** 307 | Basic string draw function 308 | */ 309 | void draw(dstring text, vec2 position, vec4 color=vec4(1)) { 310 | vec2 next = position; 311 | size_t line; 312 | 313 | foreach(dchar c; text) { 314 | 315 | // Skip newline 316 | if (c == '\n') { 317 | line++; 318 | next.x = position.x; 319 | next.y += metrics.y; 320 | continue; 321 | } 322 | 323 | auto idx = GlyphIndex(c, size); 324 | 325 | // Load character if neccesary 326 | if (idx !in glyphs) { 327 | genGlyphFor(c); 328 | 329 | // At this point if the glyph does not exist, skip it 330 | if (idx !in glyphs) continue; 331 | } 332 | 333 | draw(c, next, vec2(0), 0, color); 334 | next.x += glyphs[idx].advance.x; 335 | } 336 | } 337 | 338 | /** 339 | draws a character 340 | */ 341 | void draw(dchar c, vec2 position, vec2 origin=vec2(0), float rotation=0, vec4 color=vec4(1)) { 342 | 343 | auto idx = GlyphIndex(c, size); 344 | 345 | // Load character if neccesary 346 | if (idx !in glyphs) { 347 | genGlyphFor(c); 348 | 349 | // At this point if the glyph does not exist, skip it 350 | if (idx !in glyphs) return; 351 | } 352 | 353 | // Flush if neccesary 354 | if (dataOffset == DataSize*EntryCount) flush(); 355 | vec4 area = glyphs[idx].area; 356 | vec2 bearing = glyphs[idx].bearing; 357 | 358 | vec2 pos = vec2( 359 | position.x + bearing.x, 360 | (position.y - bearing.y)+metrics.y 361 | ); 362 | 363 | mat4 transform = 364 | mat4.translation(-origin.x, -origin.y, 0) * 365 | mat4.translation(pos.x, pos.y, 0) * 366 | mat4.translation(origin.x, origin.y, 0) * 367 | mat4.zrotation(rotation) * 368 | mat4.translation(-origin.x, -origin.y, 0) * 369 | mat4.scaling(area.z, area.w, 0); 370 | 371 | 372 | vec4 uvArea = vec4( 373 | (area.x)+0.25, 374 | (area.y)+0.25, 375 | (area.x+area.z)-0.25, 376 | (area.y+area.w)-0.25, 377 | ); 378 | 379 | // Triangle 1 380 | addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color); 381 | addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color); 382 | addVertexData(vec2(0, 0).transformVerts(transform), vec2(uvArea.x, uvArea.y), color); 383 | 384 | // Triangle 2 385 | addVertexData(vec2(0, 1).transformVerts(transform), vec2(uvArea.x, uvArea.w), color); 386 | addVertexData(vec2(1, 1).transformVerts(transform), vec2(uvArea.z, uvArea.w), color); 387 | addVertexData(vec2(1, 0).transformVerts(transform), vec2(uvArea.z, uvArea.y), color); 388 | 389 | tris += 2; 390 | } 391 | 392 | /** 393 | Flush font rendering 394 | */ 395 | void flush() { 396 | 397 | // Disable depth testing for the batcher 398 | glDisable(GL_DEPTH_TEST); 399 | // Bind VAO 400 | glBindVertexArray(vao); 401 | 402 | // Bind just in case some shennanigans happen 403 | glBindBuffer(GL_ARRAY_BUFFER, buffer); 404 | 405 | // Update with this draw round's data 406 | glBufferSubData(GL_ARRAY_BUFFER, 0, dataOffset*float.sizeof, data.ptr); 407 | 408 | // Bind the texture 409 | fontTexture.bind(); 410 | 411 | // Use our sprite batcher shader 412 | fontShader.use(); 413 | fontShader.setUniform(vp, fontCamera.matrix); 414 | 415 | // Vertex Buffer 416 | glEnableVertexAttribArray(0); 417 | glVertexAttribPointer( 418 | 0, 419 | VecSize, 420 | GL_FLOAT, 421 | GL_FALSE, 422 | DataLength*GLfloat.sizeof, 423 | null, 424 | ); 425 | 426 | // UV Buffer 427 | glEnableVertexAttribArray(1); 428 | glVertexAttribPointer( 429 | 1, 430 | UVSize, 431 | GL_FLOAT, 432 | GL_FALSE, 433 | DataLength*GLfloat.sizeof, 434 | cast(GLvoid*)(UVSize*GLfloat.sizeof), 435 | ); 436 | 437 | // UV Buffer 438 | glEnableVertexAttribArray(2); 439 | glVertexAttribPointer( 440 | 2, 441 | ColorSize, 442 | GL_FLOAT, 443 | GL_FALSE, 444 | DataLength*GLfloat.sizeof, 445 | cast(GLvoid*)((VecSize+UVSize)*GLfloat.sizeof), 446 | ); 447 | 448 | // Draw the triangles 449 | glDrawArrays(GL_TRIANGLES, 0, cast(int)(tris*3)); 450 | 451 | // Reset the batcher's state 452 | glDisableVertexAttribArray(0); 453 | glDisableVertexAttribArray(1); 454 | glDisableVertexAttribArray(2); 455 | dataOffset = 0; 456 | tris = 0; 457 | 458 | // Re-enable depth test Clear depth buffer 459 | glEnable(GL_DEPTH_TEST); 460 | } 461 | 462 | } -------------------------------------------------------------------------------- /source/engine/render/texture/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.render.texture; 8 | public import engine.render.texture.packer; 9 | public import engine.render.texture.atlas; 10 | public import engine.render.texture.font; 11 | import bindbc.opengl; 12 | import std.exception; 13 | import imagefmt; 14 | import std.format; 15 | import engine; 16 | 17 | /** 18 | Filtering mode for texture 19 | */ 20 | enum Filtering { 21 | /** 22 | Linear filtering will try to smooth out textures 23 | */ 24 | Linear = GL_LINEAR, 25 | 26 | /** 27 | Point filtering will try to preserve pixel edges. 28 | Due to texture sampling being float based this is imprecise. 29 | */ 30 | Point = GL_POINT 31 | } 32 | 33 | /** 34 | Texture wrapping modes 35 | */ 36 | enum Wrapping { 37 | /** 38 | Clamp texture sampling to be within the texture 39 | */ 40 | Clamp = GL_CLAMP_TO_BORDER, 41 | 42 | /** 43 | Wrap the texture in every direction idefinitely 44 | */ 45 | Repeat = GL_REPEAT, 46 | 47 | /** 48 | Wrap the texture mirrored in every direction indefinitely 49 | */ 50 | Mirror = GL_MIRRORED_REPEAT 51 | } 52 | 53 | /** 54 | A texture which is not bound to an OpenGL context 55 | 56 | Used for texture atlassing 57 | */ 58 | struct ShallowTexture { 59 | public: 60 | /** 61 | 8-bit RGBA color data 62 | */ 63 | ubyte[] data; 64 | 65 | /** 66 | Width of texture 67 | */ 68 | int width; 69 | 70 | /** 71 | Height of texture 72 | */ 73 | int height; 74 | 75 | /** 76 | Loads a shallow texture from image file 77 | Supported file types: 78 | * PNG 8-bit 79 | * BMP 8-bit 80 | * TGA 8-bit non-palleted 81 | * JPEG baseline 82 | */ 83 | this(string file) { 84 | 85 | // Load image from disk, as RGBA 8-bit 86 | IFImage image = read_image(file, 4, 8); 87 | enforce( image.e == 0, "%s: %s".format(IF_ERROR[image.e], file)); 88 | scope(exit) image.free(); 89 | 90 | // Copy data from IFImage to this ShallowTexture 91 | this.data = new ubyte[image.buf8.length]; 92 | this.data[] = image.buf8; 93 | 94 | // Set the width/height data 95 | this.width = image.w; 96 | this.height = image.h; 97 | } 98 | } 99 | 100 | /** 101 | A texture, only format supported is unsigned 8 bit RGBA 102 | */ 103 | class Texture { 104 | private: 105 | GLuint id; 106 | int width_; 107 | int height_; 108 | 109 | GLuint colorMode; 110 | int alignment; 111 | 112 | public: 113 | 114 | /** 115 | Loads texture from image file 116 | Supported file types: 117 | * PNG 8-bit 118 | * BMP 8-bit 119 | * TGA 8-bit non-palleted 120 | * JPEG baseline 121 | */ 122 | this(string file) { 123 | 124 | // Load image from disk, as RGBA 8-bit 125 | IFImage image = read_image(file, 4, 8); 126 | enforce( image.e == 0, "%s: %s".format(IF_ERROR[image.e], file)); 127 | scope(exit) image.free(); 128 | 129 | // Load in image data to OpenGL 130 | this(image.buf8, image.w, image.h); 131 | } 132 | 133 | /** 134 | Creates a texture from a ShallowTexture 135 | */ 136 | this(ShallowTexture shallow) { 137 | this(shallow.data, shallow.width, shallow.height); 138 | } 139 | 140 | /** 141 | Creates a new empty texture 142 | */ 143 | this(int width, int height, GLuint mode = GL_RGBA, int alignment = 4) { 144 | 145 | // Create an empty texture array with no data 146 | ubyte[] empty = new ubyte[width_*height_*alignment]; 147 | 148 | // Pass it on to the other texturing 149 | this(empty, width, height, mode, alignment); 150 | } 151 | 152 | /** 153 | Creates a new texture from specified data 154 | */ 155 | this(ubyte[] data, int width, int height, GLuint mode = GL_RGBA, int alignment = 4) { 156 | this.colorMode = mode; 157 | this.alignment = alignment; 158 | this.width_ = width; 159 | this.height_ = height; 160 | 161 | // Generate OpenGL texture 162 | glGenTextures(1, &id); 163 | this.setData(data); 164 | 165 | // Set default filtering and wrapping 166 | this.setFiltering(Filtering.Linear); 167 | this.setWrapping(Wrapping.Clamp); 168 | } 169 | 170 | /** 171 | Width of texture 172 | */ 173 | int width() { 174 | return width_; 175 | } 176 | 177 | /** 178 | Height of texture 179 | */ 180 | int height() { 181 | return height_; 182 | } 183 | 184 | /** 185 | Set the filtering mode used for the texture 186 | */ 187 | void setFiltering(Filtering filtering) { 188 | this.bind(); 189 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filtering); 190 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filtering); 191 | } 192 | 193 | /** 194 | Set the wrapping mode used for the texture 195 | */ 196 | void setWrapping(Wrapping wrapping) { 197 | this.bind(); 198 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapping); 199 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapping); 200 | } 201 | 202 | /** 203 | Sets the data of the texture 204 | */ 205 | void setData(ubyte[] data) { 206 | this.bind(); 207 | glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); 208 | glTexImage2D(GL_TEXTURE_2D, 0, colorMode, width_, height_, 0, colorMode, GL_UNSIGNED_BYTE, data.ptr); 209 | glGenerateMipmap(GL_TEXTURE_2D); 210 | } 211 | 212 | /** 213 | Sets a region of a texture to new data 214 | */ 215 | void setDataRegion(ubyte[] data, int x, int y, int width, int height) { 216 | this.bind(); 217 | 218 | // Make sure we don't try to change the texture in an out of bounds area. 219 | enforce( x >= 0 && x+width <= this.width_, "x offset is out of bounds (xoffset=%s, xbound=%s)".format(x+width, this.width_)); 220 | enforce( y >= 0 && y+height <= this.height_, "y offset is out of bounds (yoffset=%s, ybound=%s)".format(y+height, this.height_)); 221 | 222 | // Update the texture 223 | glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); 224 | glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, this.colorMode, GL_UNSIGNED_BYTE, data.ptr); 225 | glGenerateMipmap(GL_TEXTURE_2D); 226 | } 227 | 228 | /** 229 | Bind this texture 230 | 231 | Notes 232 | - In release mode the unit value is clamped to 31 (The max OpenGL texture unit value) 233 | - In debug mode unit values over 31 will assert. 234 | */ 235 | void bind(uint unit = 0) { 236 | assert(unit <= 31u, "Outside maximum OpenGL texture unit value"); 237 | glActiveTexture(GL_TEXTURE0+(unit <= 31u ? unit : 31u)); 238 | glBindTexture(GL_TEXTURE_2D, id); 239 | } 240 | } -------------------------------------------------------------------------------- /source/engine/render/texture/packer.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.render.texture.packer; 8 | import gl3n.linalg; 9 | import gl3n.math; 10 | import std.exception; 11 | import engine.core.log; 12 | import std.format; 13 | import std.algorithm.mutation : remove; 14 | 15 | private bool contains(vec4i a, vec4i b) { 16 | return a.x >= b.x && 17 | a.y >= b.y && 18 | a.x+a.z <= b.x+b.z && 19 | a.y+a.w <= b.y+b.w; 20 | } 21 | 22 | /** 23 | A bin 24 | */ 25 | private struct Bin { 26 | private: 27 | vec2i size; 28 | vec4i[] usedRectangles; 29 | vec4i[] freeRectangles; 30 | 31 | vec4i scoreRect(vec2i size, out int score1, out int score2) { 32 | vec4i newNode; 33 | 34 | // Find the best place to put the rectangle 35 | score1 = int.max; 36 | score2 = int.max; 37 | newNode = findNewNodeFit(size, score1, score2); 38 | 39 | // reset score 40 | if (newNode.w == 0) { 41 | score1 = int.max; 42 | score2 = int.max; 43 | } 44 | return newNode; 45 | } 46 | 47 | vec4i scoreRect(vec2i size) { 48 | vec4i newNode; 49 | 50 | // Find the best place to put the rectangle 51 | int score1 = int.max; 52 | int score2 = int.max; 53 | newNode = findNewNodeFit(size, score1, score2); 54 | 55 | return newNode; 56 | } 57 | 58 | void place(ref vec4i newNode) { 59 | 60 | // Rectangles to process 61 | size_t rectanglesToProcess = freeRectangles.length; 62 | 63 | // Run through all rectangles 64 | for(int i; i < rectanglesToProcess; ++i) { 65 | 66 | // Try splitting them up 67 | if (splitFree(freeRectangles[i], newNode)) { 68 | freeRectangles.remove(i); 69 | --i; 70 | --rectanglesToProcess; 71 | } 72 | } 73 | 74 | prune(); 75 | usedRectangles ~= newNode; 76 | } 77 | 78 | vec4i findNewNodeFit(vec2i size, int score1, int score2) { 79 | vec4i bestNode = vec4i.init; 80 | 81 | int bestShortFit = int.max; 82 | int bestLongFit = int.max; 83 | 84 | foreach(freeRect; freeRectangles) { 85 | 86 | // Try placing the rectangle in upright orientation 87 | if (freeRect.z >= size.x && freeRect.w >= size.y) { 88 | int leftoverH = abs(freeRect.z - size.x); 89 | int leftoverV = abs(freeRect.w - size.y); 90 | int shortSideFit = min(leftoverH, leftoverV); 91 | int longSideFit = max(leftoverH, leftoverV); 92 | 93 | if (shortSideFit < bestShortFit || (shortSideFit == bestShortFit && longSideFit < bestLongFit)) { 94 | bestNode.x = freeRect.x; 95 | bestNode.y = freeRect.y; 96 | bestNode.z = size.x; 97 | bestNode.w = size.y; 98 | bestShortFit = shortSideFit; 99 | bestLongFit = longSideFit; 100 | } 101 | } 102 | } 103 | 104 | return bestNode; 105 | } 106 | 107 | bool splitFree(vec4i freeNode, ref vec4i usedNode) { 108 | if (usedNode.x >= freeNode.x + freeNode.z || usedNode.x + usedNode.z <= freeNode.x || 109 | usedNode.y >= freeNode.y + freeNode.w || usedNode.y + usedNode.w <= freeNode.y) 110 | return false; 111 | 112 | // Vertical Splitting 113 | if (usedNode.x < freeNode.x + freeNode.z && usedNode.x + usedNode.z > freeNode.x) { 114 | 115 | // New node at top of used 116 | if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.w) { 117 | vec4i newNode = freeNode; 118 | newNode.w = usedNode.y - newNode.y; 119 | freeRectangles ~= newNode; 120 | } 121 | 122 | // New node at bottom of used 123 | if (usedNode.y + usedNode.w < freeNode.y + freeNode.w) { 124 | vec4i newNode = freeNode; 125 | newNode.y = usedNode.y + usedNode.w; 126 | newNode.w = freeNode.y + freeNode.w - (usedNode.y + usedNode.w); 127 | freeRectangles ~= newNode; 128 | } 129 | } 130 | 131 | // Horizontal Splitting 132 | if (usedNode.y < freeNode.y + freeNode.w && usedNode.y + usedNode.w > freeNode.y) { 133 | 134 | // New node at left of used 135 | if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.z) { 136 | vec4i newNode = freeNode; 137 | newNode.z = usedNode.x - newNode.x; 138 | freeRectangles ~= newNode; 139 | } 140 | 141 | // New node at right of used 142 | if (usedNode.x + usedNode.z < freeNode.x + freeNode.z) { 143 | vec4i newNode = freeNode; 144 | newNode.x = usedNode.x + usedNode.z; 145 | newNode.z = freeNode.x + freeNode.z - (usedNode.x + usedNode.z); 146 | freeRectangles ~= newNode; 147 | } 148 | } 149 | return true; 150 | } 151 | 152 | void prune() { 153 | for(int i; i < freeRectangles.length; ++i) { 154 | for(int j = i+1; j < freeRectangles.length; ++j) { 155 | 156 | 157 | if (freeRectangles[i].contains(freeRectangles[j])) { 158 | freeRectangles = freeRectangles.remove(i); 159 | --i; 160 | break; 161 | } 162 | 163 | if (freeRectangles[j].contains(freeRectangles[i])) { 164 | freeRectangles = freeRectangles.remove(j); 165 | --j; 166 | } 167 | } 168 | } 169 | } 170 | 171 | public: 172 | this(vec2i size) { 173 | this.size = size; 174 | freeRectangles = [vec4i(0, 0, size.x, size.y)]; 175 | } 176 | 177 | /** 178 | Inserts a new rectangle in to the bin 179 | */ 180 | vec4i insert(vec2i size) { 181 | int score1; 182 | int score2; 183 | vec4i newNode = scoreRect(size, score1, score2); 184 | 185 | // Place rectangle in to bin 186 | place(newNode); 187 | return newNode; 188 | } 189 | 190 | /** 191 | Removes the area from the packing 192 | */ 193 | void remove(vec4i area) { 194 | foreach(i, rect; usedRectangles) { 195 | if (rect == area) { 196 | usedRectangles = usedRectangles.remove(i); 197 | break; 198 | } 199 | } 200 | freeRectangles ~= area; 201 | } 202 | 203 | void clear() { 204 | freeRectangles = [vec4i(0, 0, size.x, size.y)]; 205 | usedRectangles = []; 206 | } 207 | 208 | /** 209 | Gets ratio of surface area used 210 | */ 211 | float occupancy() { 212 | ulong surfaceArea = 0; 213 | foreach(rect; usedRectangles) { 214 | surfaceArea += rect.z*rect.w; 215 | } 216 | return surfaceArea / (size.x*size.y); 217 | } 218 | } 219 | 220 | 221 | class TexturePacker { 222 | private: 223 | Bin bin; 224 | 225 | public: 226 | 227 | /** 228 | Max size of texture packer 229 | */ 230 | this(vec2i textureSize = vec2i(1024, 1024)) { 231 | bin = Bin(textureSize); 232 | } 233 | 234 | /** 235 | Packs a texture in to the bin 236 | 237 | Returns a vec4i(0, 0, 0, 0) on packing failure 238 | */ 239 | vec4i packTexture(vec2i size) { 240 | return bin.insert(size); 241 | } 242 | 243 | /** 244 | Remove an area from the texture packer 245 | */ 246 | void remove(vec4i area) { 247 | bin.remove(area); 248 | } 249 | 250 | /** 251 | Clear the texture packer 252 | */ 253 | void clear() { 254 | bin.clear(); 255 | } 256 | } -------------------------------------------------------------------------------- /source/engine/render/tile.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.render.tile; 8 | import engine; 9 | import std.conv; 10 | import std.exception; 11 | import std.format : format; 12 | 13 | private { 14 | static Shader TileShader; 15 | static GLint TileShaderMVP; 16 | static GLint TileAvailability; 17 | } 18 | 19 | /** 20 | Initializes the tile mesh system 21 | */ 22 | void initTileMesh() { 23 | 24 | // Load tile shader if needed 25 | if (TileShader is null) { 26 | TileShader = new Shader(import("shaders/tile.vert"), import("shaders/tile.frag")); 27 | TileShader.use(); 28 | TileShaderMVP = TileShader.getUniformLocation("mvp"); 29 | TileAvailability = TileShader.getUniformLocation("available"); 30 | } 31 | } 32 | 33 | /** 34 | The side of the tile mesh 35 | */ 36 | enum TileMeshSide { 37 | /// Font of the tile mesh 38 | Front, 39 | 40 | /// Back of the tile mesh 41 | Back, 42 | 43 | // Left/right sides of the tile mesh 44 | Side, 45 | 46 | // Top/bottom sides of the tile mesh 47 | Cap 48 | } 49 | 50 | private static GLuint vao; 51 | 52 | /** 53 | A mesh for a tile 54 | */ 55 | class TileMesh { 56 | private: 57 | TextureAtlas atlas; 58 | 59 | 60 | vec3 size; 61 | float[12*3*3] verts; 62 | float[12*2*3] uvs; 63 | GLuint uvId; 64 | GLuint vdId; 65 | 66 | void genVerts() { 67 | 68 | // We want to generate the model so that origin is in the center 69 | immutable(float) relWidth = size.x/2; 70 | immutable(float) relHeight = size.y/2; 71 | immutable(float) relLength = size.z/2; 72 | 73 | // Populate verticies 74 | verts = [ 75 | // Front Face Tri 1 76 | -relWidth, -relHeight, relLength, 77 | relWidth, -relHeight, relLength, 78 | -relWidth, relHeight, relLength, 79 | 80 | // Front Face Tri 2 81 | relWidth, -relHeight, relLength, 82 | relWidth, relHeight, relLength, 83 | -relWidth, relHeight, relLength, 84 | 85 | // Top Face Tri 1 86 | relWidth, relHeight, -relLength, 87 | -relWidth, relHeight, -relLength, 88 | relWidth, relHeight, relLength, 89 | 90 | // Top Face Tri 2 91 | -relWidth, relHeight, -relLength, 92 | -relWidth, relHeight, relLength, 93 | relWidth, relHeight, relLength, 94 | 95 | // Back Face Tri 1 96 | relWidth, -relHeight, -relLength, 97 | -relWidth, -relHeight, -relLength, 98 | relWidth, relHeight, -relLength, 99 | 100 | // Back Face Tri 2 101 | -relWidth, -relHeight, -relLength, 102 | -relWidth, relHeight, -relLength, 103 | relWidth, relHeight, -relLength, 104 | 105 | // Bottom Face Tri 1 106 | -relWidth, -relHeight, -relLength, 107 | relWidth, -relHeight, -relLength, 108 | -relWidth, -relHeight, relLength, 109 | 110 | // Bottom Face Tri 2 111 | relWidth, -relHeight, -relLength, 112 | relWidth, -relHeight, relLength, 113 | -relWidth, -relHeight, relLength, 114 | 115 | // Left Face Tri 1 116 | -relWidth, relHeight, -relLength, 117 | -relWidth, -relHeight, -relLength, 118 | -relWidth, relHeight, relLength, 119 | 120 | // Left Face Tri 2 121 | -relWidth, -relHeight, -relLength, 122 | -relWidth, -relHeight, relLength, 123 | -relWidth, relHeight, relLength, 124 | 125 | // Right Face Tri 1 126 | relWidth, -relHeight, -relLength, 127 | relWidth, relHeight, -relLength, 128 | relWidth, -relHeight, relLength, 129 | 130 | // Right Face Tri 2 131 | relWidth, relHeight, -relLength, 132 | relWidth, relHeight, relLength, 133 | relWidth, -relHeight, relLength, 134 | ]; 135 | 136 | } 137 | 138 | void genUV(TileMeshSide side, AtlasArea area) { 139 | immutable(vec4) faceUV = area.uv; 140 | switch(side) { 141 | case TileMeshSide.Front: 142 | uvs[0..12] = [ 143 | faceUV.x, faceUV.w, 144 | faceUV.z, faceUV.w, 145 | faceUV.x, faceUV.y, 146 | 147 | faceUV.z, faceUV.w, 148 | faceUV.z, faceUV.y, 149 | faceUV.x, faceUV.y, 150 | ]; 151 | break; 152 | 153 | case TileMeshSide.Back: 154 | uvs[24..36] = [ 155 | faceUV.x, faceUV.w, 156 | faceUV.z, faceUV.w, 157 | faceUV.x, faceUV.y, 158 | 159 | faceUV.z, faceUV.w, 160 | faceUV.z, faceUV.y, 161 | faceUV.x, faceUV.y, 162 | ]; 163 | 164 | break; 165 | 166 | case TileMeshSide.Side: 167 | uvs[48..60] = [ 168 | faceUV.x, faceUV.w, 169 | faceUV.z, faceUV.w, 170 | faceUV.x, faceUV.y, 171 | 172 | faceUV.z, faceUV.w, 173 | faceUV.z, faceUV.y, 174 | faceUV.x, faceUV.y, 175 | ]; 176 | uvs[60..72] = [ 177 | faceUV.x, faceUV.w, 178 | faceUV.z, faceUV.w, 179 | faceUV.x, faceUV.y, 180 | 181 | faceUV.z, faceUV.w, 182 | faceUV.z, faceUV.y, 183 | faceUV.x, faceUV.y, 184 | ]; 185 | break; 186 | 187 | case TileMeshSide.Cap: 188 | uvs[12..24] = [ 189 | faceUV.x, faceUV.w, 190 | faceUV.z, faceUV.w, 191 | faceUV.x, faceUV.y, 192 | 193 | faceUV.z, faceUV.w, 194 | faceUV.z, faceUV.y, 195 | faceUV.x, faceUV.y, 196 | ]; 197 | uvs[36..48] = [ 198 | faceUV.x, faceUV.w, 199 | faceUV.z, faceUV.w, 200 | faceUV.x, faceUV.y, 201 | 202 | faceUV.z, faceUV.w, 203 | faceUV.z, faceUV.y, 204 | faceUV.x, faceUV.y, 205 | ]; 206 | break; 207 | default: break; 208 | } 209 | } 210 | 211 | void initTile(string front, string back, string side, string cap) { 212 | 213 | // Gen new vao if needed 214 | if (vao == 0) glGenVertexArrays(1, &vao); 215 | glBindVertexArray(vao); 216 | 217 | glGenBuffers(1, &vdId); 218 | glGenBuffers(1, &uvId); 219 | 220 | genVerts(); 221 | glBindBuffer(GL_ARRAY_BUFFER, vdId); 222 | glBufferData(GL_ARRAY_BUFFER, float.sizeof*verts.length, verts.ptr, GL_STATIC_DRAW); 223 | 224 | genUV(TileMeshSide.Front, atlas[front]); 225 | genUV(TileMeshSide.Back, atlas[back]); 226 | genUV(TileMeshSide.Side, atlas[side]); 227 | genUV(TileMeshSide.Cap, atlas[cap]); 228 | glBindBuffer(GL_ARRAY_BUFFER, uvId); 229 | glBufferData(GL_ARRAY_BUFFER, float.sizeof*uvs.length, uvs.ptr, GL_STATIC_DRAW); 230 | } 231 | 232 | void bind() { 233 | 234 | // Set vertex attributes 235 | glEnableVertexAttribArray(0); 236 | glEnableVertexAttribArray(1); 237 | glBindBuffer(GL_ARRAY_BUFFER, vdId); 238 | glVertexAttribPointer( 239 | 0, 240 | 3, 241 | GL_FLOAT, 242 | GL_FALSE, 243 | 0, 244 | null 245 | ); 246 | 247 | glBindBuffer(GL_ARRAY_BUFFER, uvId); 248 | glVertexAttribPointer( 249 | 1, 250 | 2, 251 | GL_FLOAT, 252 | GL_FALSE, 253 | 0, 254 | null 255 | ); 256 | } 257 | 258 | public: 259 | 260 | /** 261 | Availability 262 | */ 263 | bool available = true; 264 | 265 | /** 266 | Destructor 267 | */ 268 | ~this() { 269 | glDeleteBuffers(1, &uvId); 270 | glDeleteBuffers(1, &vdId); 271 | } 272 | 273 | /** 274 | Construct a new tile 275 | */ 276 | this(vec3 size, TextureAtlas atlas, string frontTexture, string backTexture, string sideTexture, string capTexture) { 277 | this.atlas = atlas; 278 | this.size = size; 279 | initTile(frontTexture, backTexture, sideTexture, capTexture); 280 | } 281 | 282 | /** 283 | Changes the texture of a side of the tile 284 | */ 285 | void setTexture(TileMeshSide side, AtlasArea tex) { 286 | genUV(side, tex); 287 | glBindBuffer(GL_ARRAY_BUFFER, uvId); 288 | glBufferData(GL_ARRAY_BUFFER, float.sizeof*uvs.length, uvs.ptr, GL_STATIC_DRAW); 289 | } 290 | 291 | /** 292 | Begin rendering tiles 293 | */ 294 | static void begin() { 295 | 296 | // Bind the vao 297 | glBindVertexArray(vao); 298 | } 299 | 300 | /** 301 | End rendering tiles 302 | */ 303 | static void end() { 304 | glDisableVertexAttribArray(0); 305 | glDisableVertexAttribArray(1); 306 | } 307 | 308 | /** 309 | Draws the tile 310 | */ 311 | void draw(Camera camera, mat4 transform) { 312 | 313 | bind(); 314 | TileShader.use(); 315 | atlas.bind(); 316 | TileShader.setUniform(TileShaderMVP, camera.matrix*transform); 317 | TileShader.setUniform(TileAvailability, available); 318 | glDrawArrays(GL_TRIANGLES, 0, cast(int)verts.length); 319 | } 320 | 321 | /** 322 | Draws the tile on a 2D plane 323 | */ 324 | void draw2d(Camera2D camera, vec2 position, float scale = 1, quat rotation = quat.identity) { 325 | 326 | bind(); 327 | TileShader.use(); 328 | atlas.bind(); 329 | TileShader.setUniform(TileShaderMVP, 330 | camera.matrix * 331 | mat4.translation(position.x, position.y, -(size.z*scale)) * 332 | rotation.to_matrix!(4, 4) * 333 | mat4.scaling(scale, -scale, scale) 334 | ); 335 | 336 | glDrawArrays(GL_TRIANGLES, 0, cast(int)verts.length); 337 | } 338 | } -------------------------------------------------------------------------------- /source/engine/ui/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | UI Subsystem 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.ui; 10 | public import engine.ui.widget; 11 | import engine; 12 | import bindbc.opengl; 13 | import core.memory; 14 | 15 | /** 16 | Base stuff for UI rendering 17 | */ 18 | class UI { 19 | public: 20 | 21 | /** 22 | Sets up state for UI rendering 23 | */ 24 | static void begin() { 25 | glEnable(GL_SCISSOR_TEST); 26 | } 27 | 28 | /** 29 | Set the UI scissor area 30 | */ 31 | static void setScissor(vec4i scissor) { 32 | glScissor(scissor.x, (GameWindow.height-scissor.y)-scissor.w, cast(uint)scissor.z, cast(uint)scissor.w); 33 | } 34 | 35 | /** 36 | Finishes up UI rendering state 37 | */ 38 | static void end() { 39 | glDisable(GL_SCISSOR_TEST); 40 | } 41 | } -------------------------------------------------------------------------------- /source/engine/ui/widget.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.ui.widget; 8 | import engine; 9 | import std.algorithm.mutation : remove; 10 | import bindbc.opengl; 11 | import std.exception; 12 | import std.format; 13 | 14 | /** 15 | A widget 16 | */ 17 | abstract class Widget { 18 | private: 19 | string typeName; 20 | Widget parent_; 21 | Widget[] children; 22 | 23 | ptrdiff_t findSelfInParent() { 24 | 25 | // Can't find self if we don't have a parent 26 | if (parent_ is null) return -1; 27 | 28 | // Iterate through parent's children and find our instance 29 | foreach(i, widget; parent_.children) { 30 | if (widget == this) return i; 31 | } 32 | 33 | // We couldn't find ourselves? 34 | AppLog.warn("UI", "Widget %s could not find self in parent", this); 35 | return -1; 36 | } 37 | 38 | protected: 39 | /** 40 | The parent widget 41 | */ 42 | Widget parent() { 43 | return parent_; 44 | } 45 | 46 | /** 47 | Base widget instance requires type name 48 | */ 49 | this(string type) { 50 | this.typeName = type; 51 | } 52 | 53 | /** 54 | Code run when updating the widget 55 | */ 56 | abstract void onUpdate(); 57 | 58 | /** 59 | Code run when drawing 60 | */ 61 | abstract void onDraw(); 62 | 63 | public: 64 | 65 | /** 66 | Whether the widget is visible 67 | */ 68 | bool visible = true; 69 | 70 | /** 71 | The position of the widget 72 | */ 73 | vec2 position = vec2(0); 74 | 75 | /** 76 | Changes the parent of a widget to the specified other widget. 77 | */ 78 | void changeParent(Widget newParent) { 79 | 80 | // Once we're done we need to update our bounds 81 | // We'll skip this if we threw an exception earlier 82 | scope(success) update(); 83 | 84 | // Remove ourselves from our current parent if we have any 85 | if (parent_ !is null) { 86 | 87 | // Find ourselves in our parent 88 | ptrdiff_t self = findSelfInParent(); 89 | enforce(self >= 0, "Invalid parent widget"); 90 | 91 | // Remove ourselves from our current parent 92 | parent_.children.remove(self); 93 | } 94 | 95 | // If our new parent is null we'll end early 96 | if (newParent is null) { 97 | this.parent_ = null; 98 | return; 99 | } 100 | 101 | // Set our parent to our new parent and add ourselves to our new parent's list 102 | this.parent_ = newParent; 103 | this.parent_.children ~= this; 104 | } 105 | 106 | /** 107 | Update the widget 108 | 109 | Automatically updates all the children first 110 | */ 111 | void update() { 112 | 113 | // Update all our children 114 | foreach(child; children) { 115 | child.update(); 116 | } 117 | 118 | // Update ourselves 119 | this.onUpdate(); 120 | } 121 | 122 | /** 123 | Draw the widget 124 | 125 | For widget implementing: override onDraw 126 | */ 127 | final void draw() { 128 | 129 | // Don't draw this widget or its children if we're invisible 130 | if (!visible) return; 131 | 132 | if (parent_ is null) UI.setScissor(vec4i(0, 0, GameWindow.width, GameWindow.height)); 133 | 134 | // Draw ourselves first 135 | this.onDraw(); 136 | 137 | // We set our scissor rectangle to our rendering area 138 | UI.setScissor(scissorArea); 139 | 140 | // Draw all the children 141 | foreach(child; children) { 142 | child.draw(); 143 | } 144 | } 145 | 146 | /** 147 | Gets the calculated position of the widget 148 | */ 149 | final vec2 calculatedPosition() { 150 | return parent_ !is null ? parent_.calculatedPosition+position : position; 151 | } 152 | 153 | abstract { 154 | 155 | /** 156 | Area of the widget 157 | */ 158 | vec4 area(); 159 | 160 | /** 161 | Area in which the widget cuts out child widgets 162 | */ 163 | vec4i scissorArea(); 164 | } 165 | 166 | override { 167 | 168 | /** 169 | Gets this widget as a string 170 | 171 | This returns the tree for this instance of the widget ordered by type name. 172 | */ 173 | final string toString() const { 174 | return parent_ is null ? typeName : "%s->%s".format(parent_.toString, typeName); 175 | } 176 | } 177 | } -------------------------------------------------------------------------------- /source/engine/ui/widgets/label.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.ui.widgets.label; 8 | import engine.ui; 9 | import engine; 10 | 11 | /** 12 | A label 13 | */ 14 | class Label : Widget { 15 | private: 16 | 17 | protected: 18 | override { 19 | /** 20 | Code run when updating the widget 21 | */ 22 | void onUpdate() { } 23 | 24 | /** 25 | Code run when drawing 26 | */ 27 | void onDraw() { 28 | GameFont.changeSize(size); 29 | GameFont.draw(this.text, vec2(area.x, area.y)); 30 | GameFont.flush(); 31 | } 32 | } 33 | 34 | public: 35 | 36 | /** 37 | The text buffer for the label 38 | */ 39 | dstring text; 40 | 41 | /** 42 | The font size 43 | */ 44 | int size = 24; 45 | 46 | this(T)(T text, int size = 24) if (isString!T) { 47 | super("Label"); 48 | 49 | this.text = text.toEngineString(); 50 | this.size = size; 51 | } 52 | 53 | /** 54 | Set the text of the label 55 | This function supports UTF8 text 56 | */ 57 | void setText(T)(T text) if (isString!T) { 58 | this.text = text.toEngineString(); 59 | } 60 | 61 | override: 62 | 63 | /** 64 | Area of the widget 65 | */ 66 | vec4 area() { 67 | vec4 sArea = scissorArea(); 68 | return vec4(sArea.x-4, sArea.y-4, sArea.z+8, sArea.w+8); 69 | } 70 | 71 | /** 72 | Area in which the widget cuts out child widgets 73 | */ 74 | vec4i scissorArea() { 75 | GameFont.changeSize(size); 76 | vec2 measure = GameFont.measure(text); 77 | vec2 pos = this.calculatedPosition(); 78 | return vec4i( 79 | cast(int)pos.x, 80 | cast(int)pos.y, 81 | cast(int)measure.x, 82 | cast(int)measure.y 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /source/engine/vn/character.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.vn.character; 8 | import engine.vn.render; 9 | import engine; 10 | import std.format; 11 | 12 | /** 13 | Character declaration 14 | */ 15 | struct CharDecl { 16 | /** 17 | The symbolic name for the character 18 | */ 19 | string name; 20 | 21 | /** 22 | Name of character 23 | */ 24 | dstring[string] names; 25 | 26 | /** 27 | Textures for character 28 | */ 29 | string[string] textures; 30 | } 31 | 32 | /** 33 | Character registry 34 | */ 35 | class CharacterRegistry { 36 | private static: 37 | CharDecl[string] registry; 38 | 39 | public static: 40 | /** 41 | Register a character 42 | */ 43 | public void register(string name, CharDecl declaration) { 44 | registry[name] = declaration; 45 | } 46 | 47 | /** 48 | Create a character from its name 49 | */ 50 | public Character create(string name) { 51 | return new Character(registry[name]); 52 | } 53 | } 54 | 55 | /** 56 | A character in the visual novel 57 | */ 58 | class Character { 59 | private: 60 | 61 | /** 62 | The system name of the character 63 | */ 64 | string name; 65 | 66 | /** 67 | All the names the character can have 68 | */ 69 | dstring[string] dnames; 70 | 71 | /** 72 | Named textures for the character 73 | */ 74 | AtlasIndex[string] textures; 75 | 76 | /** 77 | The texture of the character 78 | */ 79 | string texture; 80 | 81 | /** 82 | Cached texture size 83 | */ 84 | vec2 textureSize; 85 | 86 | /** 87 | How visible the character is 88 | */ 89 | float visibility = 0; 90 | 91 | public: 92 | 93 | dstring displayName() { 94 | return dnames[CurrentLanguage]; 95 | } 96 | 97 | /** 98 | Whether to show the character 99 | */ 100 | bool shown; 101 | 102 | /** 103 | Position of the character on the screen 104 | */ 105 | vec2 position; 106 | 107 | /** 108 | Whether the character's texture is flipped vertically 109 | */ 110 | bool isFlipped; 111 | 112 | /** 113 | The current texture 114 | */ 115 | AtlasIndex* currentTexture() { 116 | return texture in textures; 117 | } 118 | 119 | /** 120 | Sets the character's texture 121 | */ 122 | void setTexture(string texture) { 123 | this.texture = texture; 124 | this.textureSize = currentTexture.area.zw; 125 | } 126 | 127 | /** 128 | Creates a character from a definition 129 | */ 130 | this(CharDecl decl) { 131 | this.name = decl.name; 132 | this.dnames = decl.names; 133 | foreach (texName, texture; decl.textures) { 134 | AppLog.info("VN Engine", "Added character texture %s", texName); 135 | GameAtlas.add(genTexName(name, texName), texture); 136 | textures[texName] = GameAtlas[genTexName(name, texName)]; 137 | } 138 | 139 | // Set texture to neutral if any was given for the character 140 | if (decl.textures.length > 0) this.setTexture("neutral"); 141 | } 142 | 143 | /** 144 | Clean up textures between scene switches. 145 | */ 146 | ~this() { 147 | 148 | // Remove unused textures from the atlas 149 | foreach(texName, _; textures) { 150 | GameAtlas.remove(genTexName(name, texName)); 151 | } 152 | } 153 | 154 | /** 155 | Activates the character causing them to jump if visible 156 | */ 157 | private float activationTimer = 0.0; 158 | private float jump = 0; 159 | void activate() { 160 | activationTimer = 1; 161 | } 162 | 163 | void update() { 164 | 165 | // Handle visibility fade 166 | if (shown) { 167 | if (visibility < 1) visibility += 10*deltaTime(); 168 | if (visibility > 1) visibility = 1; 169 | } else { 170 | if (visibility > 0) visibility -= 10*deltaTime(); 171 | if (visibility < 0) visibility = 0; 172 | } 173 | 174 | // TODO: use the animation system for this 175 | 176 | // Handle activation timer countdown 177 | if (activationTimer > 0) activationTimer -= 5*deltaTime(); 178 | if (activationTimer < 0) activationTimer = 0; 179 | 180 | if (activationTimer > 0.5) { 181 | float t = (0.5-activationTimer)*2; 182 | jump = lerp!float(-12, 0, t); 183 | } else { 184 | float t = (0.5-activationTimer)*2; 185 | jump = lerp!float(0, -12, t); 186 | } 187 | } 188 | 189 | void draw() { 190 | if (!shown && visibility == 0) return; 191 | if (currentTexture is null) return; 192 | GameBatch.draw( 193 | *currentTexture, 194 | vec4(position.x, position.y-jump, textureSize.x, textureSize.y), 195 | vec4.init, 196 | vec2(textureSize.x/2, textureSize.y), 197 | 0, 198 | isFlipped ? SpriteFlip.Horizontal : SpriteFlip.None, 199 | vec4(1, 1, 1, visibility) 200 | ); 201 | } 202 | } 203 | 204 | private string genTexName(string charName, string texName) { 205 | return "%s/%s".format(charName, texName); 206 | } -------------------------------------------------------------------------------- /source/engine/vn/dialg.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.vn.dialg; 8 | import engine.vn.render.dialg; 9 | import engine.vn.log; 10 | import engine.vn; 11 | import engine; 12 | 13 | /** 14 | Manager for dialogue 15 | */ 16 | class DialogueManager { 17 | private: 18 | DialogueRenderer renderer; 19 | string speaker; 20 | dstring dspeaker; 21 | VNLog log; 22 | VNState state; 23 | 24 | public: 25 | this(VNState state) { 26 | this.state = state; 27 | 28 | log = new VNLog(); 29 | renderer = new DialogueRenderer(); 30 | if (!GameAtlas.has("ui_vnbar")) { 31 | GameAtlas.add("ui_vnbar", "assets/textures/ui/vn-bar.png"); 32 | } 33 | } 34 | 35 | /** 36 | Whether the hide the dialogue box 37 | */ 38 | bool hide = false; 39 | 40 | /** 41 | Whether the dialogue requested to automatically advance 42 | */ 43 | bool autoNext = false; 44 | 45 | /** 46 | Whether the text is done scrolling 47 | */ 48 | bool done() { 49 | return renderer.done; 50 | } 51 | 52 | /** 53 | Skip to end of text 54 | */ 55 | void skip() { 56 | renderer.skip(); 57 | } 58 | 59 | /** 60 | Update the dialogue manager 61 | */ 62 | void update() { 63 | renderer.update(); 64 | } 65 | 66 | /** 67 | Push dialogue/action to the renderer 68 | Automatically pu 69 | */ 70 | void push(dstring dialogue, dstring origin = null) { 71 | speaker = origin.toDString; 72 | dspeaker = origin; 73 | renderer.pushText(dialogue); 74 | log.add(origin, dialogue); 75 | } 76 | 77 | /** 78 | Draw dialogue box 79 | */ 80 | void draw() { 81 | if (hide) return; 82 | 83 | // Set font size 84 | GameFont.changeSize(48); 85 | 86 | // Draw the bar 87 | GameBatch.draw("ui_vnbar", vec4(0, 1080-292, 1920, 292)); 88 | GameBatch.flush(); 89 | 90 | // Draw name tag 91 | if (speaker.length > 0) { 92 | GameFont.draw( 93 | speaker in state.characters ? 94 | state.characters[speaker].displayName : 95 | dspeaker, 96 | vec2(32, 804) 97 | ); 98 | } 99 | GameFont.flush(); 100 | 101 | // Slow type the dialogue 102 | renderer.draw(vec2(48, 900)); 103 | } 104 | } -------------------------------------------------------------------------------- /source/engine/vn/log.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.vn.log; 8 | import std.format; 9 | 10 | class VNLog { 11 | public: 12 | /** 13 | The log 14 | */ 15 | dstring[] log; 16 | 17 | /** 18 | Adds action to log 19 | */ 20 | void add(dstring action) { 21 | log ~= action; 22 | } 23 | 24 | /** 25 | Adds character saying something to log 26 | */ 27 | void add(dstring c, dstring text) { 28 | 29 | // Just add action if we have no origin 30 | if (c.length == 0) { 31 | this.add(text); 32 | return; 33 | } 34 | 35 | // Add our dialogue 36 | log ~= "[&cl1,0.5,0.5]%s[&clclear]: %s"d.format(c, text); 37 | } 38 | } -------------------------------------------------------------------------------- /source/engine/vn/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Visual Novel Subsystem 3 | 4 | Copyright © 2020, Luna Nielsen 5 | Distributed under the 2-Clause BSD License, see LICENSE file. 6 | 7 | Authors: Luna Nielsen 8 | */ 9 | module engine.vn; 10 | import engine.vn.render; 11 | import engine.vn.script; 12 | public import engine.vn.character; 13 | public import engine.vn.dialg; 14 | public import engine.vn.script; 15 | import engine; 16 | 17 | /** 18 | Holds the current state of the Visual Novel system 19 | */ 20 | class VNState { 21 | public: 22 | this() { 23 | dialogue = new DialogueManager(this); 24 | } 25 | 26 | /** 27 | Music being played 28 | */ 29 | Music music; 30 | 31 | /** 32 | Ambience loops being played 33 | */ 34 | Music[] ambience; 35 | 36 | /** 37 | The background/cg texture 38 | */ 39 | Texture cg; 40 | 41 | /** 42 | The list of the characters currently in the scene 43 | */ 44 | Character[string] characters; 45 | 46 | /** 47 | The dialogue manager 48 | */ 49 | DialogueManager dialogue; 50 | 51 | /** 52 | The current script being executed by the engine 53 | */ 54 | Script script; 55 | 56 | /** 57 | Changes the currently active scene 58 | */ 59 | void changeScene(string scene=null) { 60 | 61 | // Clear all the old characters out 62 | this.characters.clear(); 63 | 64 | // Change the scene 65 | script.changeScene(scene); 66 | } 67 | 68 | /** 69 | Cleanup procedure before closing VN state 70 | */ 71 | void cleanup() { 72 | import core.memory : GC; 73 | 74 | // Clear all the old characters out 75 | foreach(name, character; characters) { 76 | debug AppLog.info("VN Memory Managment", "Destroying %s...", name); 77 | destroy(character); 78 | } 79 | 80 | // Clear all the old characters out 81 | this.characters.clear(); 82 | 83 | destroy(cg); 84 | destroy(dialogue); 85 | destroy(script); 86 | 87 | // Force GC to collect everything 88 | GC.collect(); 89 | } 90 | 91 | /** 92 | Sets dialogue auto next flag 93 | */ 94 | void markAutoNext() { 95 | dialogue.autoNext = true; 96 | } 97 | 98 | /** 99 | Loads the characters defined in a list 100 | */ 101 | void loadCharacters(string[] characters) { 102 | 103 | // Load characters from the character registry 104 | foreach(character; characters) { 105 | this.characters[character] = CharacterRegistry.create(character); 106 | } 107 | } 108 | 109 | /** 110 | Loads a script 111 | */ 112 | void loadScript(Scene[string] manuscript) { 113 | this.script = new Script(this, manuscript); 114 | script.runNext(); 115 | } 116 | 117 | /** 118 | Updates the visual novel 119 | */ 120 | void update() { 121 | 122 | // No need to update when there's no manuscript loaded 123 | if (script is null) return; 124 | 125 | // If the "Confirm" binding or left mouse button was pressed, advance dialogue 126 | if (Input.wasPressed("Confirm") || Mouse.isButtonClicked(MouseButton.Left)) { 127 | 128 | // AutoNext dialogue cannot be skipped 129 | // Note: AutoNext dialogue should be brief 130 | if (!dialogue.autoNext) { 131 | if (!dialogue.done) { 132 | dialogue.skip(); 133 | } else { 134 | script.runNext(); 135 | } 136 | } 137 | } 138 | 139 | // Handle AutoNext dialogue 140 | if (dialogue.autoNext) { 141 | if (dialogue.done()) { 142 | dialogue.autoNext = false; 143 | script.runNext(); 144 | } 145 | } 146 | 147 | // Update the characters 148 | foreach(character; characters) { 149 | character.update(); 150 | } 151 | 152 | // Update the dialogue manager 153 | dialogue.update(); 154 | 155 | // Cleanup 156 | foreach(amb; ambience) { 157 | if (!amb.isPlaying) { 158 | destroy(amb); 159 | } 160 | } 161 | } 162 | 163 | /** 164 | Draw's the visual novel 165 | */ 166 | void draw() { 167 | 168 | // If there's no manuscript then we don't need to try to draw stuff 169 | if (script is null) { 170 | GameFont.changeSize(48); 171 | GameFont.draw("No manuscripts are loaded!"d, vec2(16, 16)); 172 | GameFont.flush(); 173 | return; 174 | } 175 | 176 | // First draw background/cg 177 | if (cg !is null) { 178 | GameBatch.draw(cg, vec4(0, 0, kmCameraViewWidth, kmCameraViewHeight)); 179 | GameBatch.flush(); 180 | } 181 | 182 | // Draw the characters 183 | foreach(character; characters) { 184 | character.draw(); 185 | } 186 | 187 | // Draw dialogue 188 | dialogue.draw(); 189 | } 190 | } 191 | 192 | shared static this() { 193 | Input.registerKey("Confirm", Key.KeySpace); 194 | } -------------------------------------------------------------------------------- /source/engine/vn/render/dialg.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.vn.render.dialg; 8 | import engine; 9 | import std.conv; 10 | import std.string : split; 11 | import std.random; 12 | 13 | private struct textInstr { 14 | dstring instr; 15 | dstring value; 16 | } 17 | 18 | private textInstr parseInstr(dstring txt) { 19 | textInstr instr; 20 | instr.instr = txt[0..2]; 21 | instr.value = txt[2..$]; 22 | return instr; 23 | } 24 | 25 | /** 26 | Renderer for visual novel dialogue, supports slow-typing 27 | */ 28 | class DialogueRenderer { 29 | private: 30 | enum DEFAULT_TIMEOUT = 0.045; 31 | 32 | dstring current; 33 | size_t rendOffset = 0; 34 | 35 | double accum = 0; 36 | double timeout = DEFAULT_TIMEOUT; 37 | 38 | double sleep = 0; 39 | 40 | bool requestHide = false; 41 | 42 | public: 43 | 44 | /** 45 | Resets text speed 46 | */ 47 | void resetSpeed() { 48 | timeout = DEFAULT_TIMEOUT; 49 | } 50 | 51 | /** 52 | Gets the current text buffer being rendered 53 | */ 54 | dstring currentTextBuffer() { 55 | return current; 56 | } 57 | 58 | /** 59 | Whether the text requested for the dialogue to be hidden 60 | */ 61 | bool isHidingRequested() { 62 | return requestHide; 63 | } 64 | 65 | /** 66 | Skip to end of dialogue 67 | */ 68 | void skip() { 69 | if (!done) { 70 | rendOffset = current.length; 71 | resetSpeed(); 72 | } 73 | } 74 | 75 | /** 76 | Gets whether the renderer is empty 77 | */ 78 | bool empty() { 79 | return current.length == 0; 80 | } 81 | 82 | /** 83 | Gets whether this line is done 84 | */ 85 | bool done() { 86 | return rendOffset == current.length; 87 | } 88 | 89 | /** 90 | Push text for rendering 91 | */ 92 | void pushText(dstring text) { 93 | current = text; 94 | rendOffset = 0; 95 | accum = 0; 96 | } 97 | 98 | void update() { 99 | 100 | 101 | // Don't try to uupdate empty text 102 | if (empty) return; 103 | 104 | // Accumulator 105 | accum += 1*deltaTime; 106 | 107 | // Handle sleeping 108 | if (sleep > 0) { 109 | if (accum >= sleep) { 110 | sleep = 0; 111 | accum = 0; 112 | while(!done && current[rendOffset] != ']') rendOffset++; 113 | rendOffset++; 114 | } 115 | else return; 116 | } 117 | 118 | // If we're past the end for some reason, fix it 119 | 120 | // Handle slowtyping text 121 | if (accum >= timeout) { 122 | accum -= timeout; 123 | if (!done) { 124 | if (current[rendOffset] == '\n') rendOffset++; 125 | 126 | size_t i = rendOffset; 127 | 128 | // Parse time changes 129 | while (i < current.length-3 && current[i] == '[' && current[i+1] == '&') { 130 | i += 2; 131 | 132 | while(i < current.length && current[i] != ']') i++; 133 | i++; 134 | 135 | // Get the instruction 136 | textInstr instr = parseInstr(current[rendOffset+2..i-1]); 137 | switch(instr.instr) { 138 | case "tm"d: 139 | if (instr.value == "clear") { 140 | timeout = DEFAULT_TIMEOUT; 141 | break; 142 | } 143 | timeout = parse!double(instr.value); 144 | break; 145 | case "sl"d: 146 | sleep = parse!double(instr.value); 147 | return; 148 | case "rh"d: 149 | requestHide = true; 150 | break; 151 | case "rs"d: 152 | requestHide = false; 153 | break; 154 | default: break; 155 | } 156 | } 157 | 158 | // Extra increment 159 | if (i < current.length) i++; 160 | rendOffset = i; 161 | } 162 | } 163 | } 164 | 165 | void draw(vec2 at, int size = 48) { 166 | 167 | // Don't try to render empty text 168 | if (empty) return; 169 | 170 | // Always flush drawing when done 171 | scope(exit) GameFont.flush(); 172 | 173 | // Setup 174 | GameFont.changeSize(size); 175 | vec2 metrics = GameFont.getMetrics(); 176 | at += vec2(metrics.x/2, metrics.y/2);; 177 | vec2 cursor = at; 178 | int shake = 0; 179 | int wave = 0; 180 | int waveSpeed = 5; 181 | bool waveRot = false; 182 | vec4 color = vec4(1, 1, 1, 1); 183 | for(int i = 0; i < rendOffset; i++) { 184 | 185 | // Values for this iteration 186 | dchar c = current[i]; 187 | vec2 advance = GameFont.advance(c); 188 | 189 | // Parse mode changes 190 | if (i < current.length-3) { 191 | while (current[i] == '[' && current[i+1] == '&') { 192 | i += 2; 193 | 194 | // Go till end of instruction 195 | size_t startOffset = i; 196 | while(i != current.length-1 && current[i] != ']') i++; 197 | i++; 198 | 199 | // Get the instruction 200 | textInstr instr = parseInstr(current[startOffset..i-1]); 201 | switch(instr.instr) { 202 | case "cl"d: 203 | 204 | // Handle clear 205 | if (instr.value == "clear") { 206 | color = vec4(1, 1, 1, 1); 207 | break; 208 | } 209 | 210 | // Try to figure out color 211 | dstring[] vals = instr.value.split(','); 212 | if (vals.length != 3) break; 213 | 214 | // Parse colors 215 | color.r = parse!float(vals[0]); 216 | color.g = parse!float(vals[1]); 217 | color.b = parse!float(vals[2]); 218 | break; 219 | 220 | case "wv"d: 221 | // Handle clear 222 | if (instr.value == "clear") { 223 | wave = 0; 224 | break; 225 | } 226 | 227 | shake = 0; 228 | wave = parse!int(instr.value); 229 | break; 230 | 231 | case "wr"d: 232 | waveRot = parse!bool(instr.value); 233 | break; 234 | 235 | case "ws"d: 236 | // Handle clear 237 | if (instr.value == "clear") { 238 | waveSpeed = 5; 239 | break; 240 | } 241 | 242 | waveSpeed = parse!int(instr.value); 243 | break; 244 | 245 | case "sh"d: 246 | // Handle clear 247 | if (instr.value == "clear") { 248 | shake = 0; 249 | break; 250 | } 251 | 252 | wave = 0; 253 | shake = parse!int(instr.value); 254 | break; 255 | 256 | default: break; 257 | } 258 | 259 | if (i < current.length) { 260 | c = current[i]; 261 | advance = GameFont.advance(c); 262 | } else return; 263 | } 264 | } 265 | 266 | // Handle newlines 267 | if (c == '\n') { 268 | cursor.x = at.x; 269 | cursor.y += metrics.y; 270 | continue; 271 | } 272 | 273 | // Skip all the parsing stuff for whitespace 274 | if (c == ' ') { 275 | cursor.x += advance.x; 276 | continue; 277 | } 278 | 279 | vec2 lCursor = cursor; 280 | float rot = 0; 281 | 282 | if (wave > 0) { 283 | cursor.y += sin((currTime()+cast(float)i)*waveSpeed)*wave; 284 | if (waveRot) rot += sin((currTime()+cast(float)i)*waveSpeed)*(0.015*wave); 285 | } 286 | if (shake > 0) { 287 | cursor.x += ((uniform01()*2)-1)*shake; 288 | cursor.y += ((uniform01()*2)-1)*shake; 289 | } 290 | 291 | 292 | // Draw font 293 | GameFont.draw(c, cursor, vec2(advance.x/2, metrics.y/2), rot, color); 294 | cursor = lCursor; 295 | cursor.x += advance.x; 296 | } 297 | } 298 | } -------------------------------------------------------------------------------- /source/engine/vn/render/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.vn.render; -------------------------------------------------------------------------------- /source/engine/vn/script/instr.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.vn.script.instr; 8 | import engine.vn.script; 9 | import engine; 10 | 11 | /** 12 | Instruction that shows a character 13 | */ 14 | class CGInstr : IScriptInstr { 15 | public: 16 | string cgfile; 17 | 18 | this(string cgfile = null) { 19 | this.cgfile = cgfile; 20 | } 21 | 22 | bool execute(Script script) { 23 | script.state.cg = cgfile is null ? null : new Texture("assets/textures/backgrounds/"~cgfile); 24 | return false; 25 | } 26 | 27 | bool isBlocking() { 28 | return false; 29 | } 30 | } 31 | 32 | /** 33 | Instruction that plays a sound effect 34 | */ 35 | class SFXInstr : IScriptInstr { 36 | public: 37 | string sfxfile; 38 | 39 | this(string sfxfile) { 40 | this.sfxfile = sfxfile; 41 | } 42 | 43 | bool execute(Script script) { 44 | (new Sound(sfxfile)).play(); 45 | return false; 46 | } 47 | 48 | bool isBlocking() { 49 | return false; 50 | } 51 | } 52 | 53 | /** 54 | Instruction that sets the scene's music 55 | */ 56 | class MusicInstr : IScriptInstr { 57 | public: 58 | string musicFile; 59 | 60 | this(string musicFile) { 61 | this.musicFile = musicFile; 62 | } 63 | 64 | bool execute(Script script) { 65 | if (script.state.music !is null) { 66 | script.state.music.stop(); 67 | } 68 | script.state.music = new Music(musicFile); 69 | script.state.music.setLooping(true); 70 | script.state.music.play(); 71 | return false; 72 | } 73 | 74 | bool isBlocking() { 75 | return false; 76 | } 77 | } 78 | 79 | /** 80 | Instruction that shows a character 81 | */ 82 | class ShowInstr : IScriptInstr { 83 | public: 84 | bool side; 85 | string texture; 86 | string character; 87 | 88 | this(string character, string texture=null, bool side=false) { 89 | this.character = character; 90 | this.texture = texture; 91 | this.side = side; 92 | } 93 | 94 | bool execute(Script script) { 95 | if (character in script.state.characters) { 96 | script.state.characters[character].shown = true; 97 | 98 | if (texture !is null) { 99 | script.state.characters[character].setTexture(texture); 100 | } 101 | 102 | script.state.characters[character].position = !side ? 103 | vec2(256, 1080) : vec2(1920-256, 1080); 104 | script.state.characters[character].isFlipped = side; 105 | } 106 | return false; 107 | } 108 | 109 | bool isBlocking() { 110 | return false; 111 | } 112 | } 113 | 114 | /** 115 | Instruction that hides a character 116 | */ 117 | class HideInstr : IScriptInstr { 118 | public: 119 | string character; 120 | 121 | this(string character) { 122 | this.character = character; 123 | } 124 | 125 | bool execute(Script script) { 126 | if (character in script.state.characters) { 127 | script.state.characters[character].shown = false; 128 | } 129 | return false; 130 | } 131 | 132 | bool isBlocking() { 133 | return false; 134 | } 135 | } 136 | 137 | /** 138 | Instruction that causes a character to say something 139 | */ 140 | class SayInstr : IScriptInstr { 141 | public: 142 | bool show; 143 | string origin; 144 | string text; 145 | 146 | this(string text, string origin=null, bool show=false) { 147 | this.origin = origin; 148 | this.text = text; 149 | this.show = show; 150 | } 151 | 152 | /** 153 | Executes the instruction 154 | */ 155 | bool execute(Script script) { 156 | script.state.dialogue.push(text.toEngineString(), origin.toEngineString()); 157 | 158 | // If show is enabled show the character 159 | if (origin in script.state.characters) { 160 | 161 | // Activates the character 162 | script.state.characters[origin].activate(); 163 | 164 | if (show) { 165 | script.state.characters[origin].shown = true; 166 | } 167 | } 168 | return true; 169 | } 170 | 171 | bool isBlocking() { 172 | return true; 173 | } 174 | } 175 | /** 176 | Instruction that causes a character to say something, this is nonblocking 177 | */ 178 | class NSayInstr : SayInstr { 179 | public: 180 | bool show; 181 | string origin; 182 | string text; 183 | 184 | this(string text, string origin=null, bool show=false) { 185 | super(text, origin, show); 186 | } 187 | 188 | /** 189 | Executes the instruction 190 | */ 191 | override bool execute(Script script) { 192 | script.state.markAutoNext(); 193 | return super.execute(script); 194 | } 195 | 196 | override bool isBlocking() { 197 | return super.isBlocking(); 198 | } 199 | } 200 | 201 | /** 202 | Instruction that executes D code 203 | */ 204 | class CodeInstr : IScriptInstr { 205 | public: 206 | void delegate(Script script) instr; 207 | 208 | this(void delegate(Script script) instr) { 209 | this.instr = instr; 210 | } 211 | 212 | /** 213 | Executes the instruction 214 | */ 215 | bool execute(Script script) { 216 | instr(script); 217 | return false; 218 | } 219 | 220 | bool isBlocking() { 221 | return false; 222 | } 223 | } -------------------------------------------------------------------------------- /source/engine/vn/script/package.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module engine.vn.script; 8 | import engine.vn; 9 | public import engine.vn.script.instr; 10 | 11 | /** 12 | A bookmark to where in the script to continue from (for saving) 13 | */ 14 | struct Bookmark { 15 | /** 16 | The scene current being shown 17 | */ 18 | string scene; 19 | 20 | /** 21 | The last blocking line + 1 22 | */ 23 | int line; 24 | } 25 | 26 | /** 27 | A visual novel script 28 | */ 29 | class Script { 30 | private: 31 | /** 32 | Manuscript holder 33 | */ 34 | Scene[string] manuscripts; 35 | 36 | /** 37 | Execution offset. 38 | */ 39 | int next; 40 | 41 | /** 42 | Current instruction 43 | */ 44 | int current() { return next-1; } 45 | 46 | /** 47 | The current scene 48 | */ 49 | string currentScene; 50 | 51 | /** 52 | Returns the current instruction list 53 | */ 54 | IScriptInstr[] instructions() { return manuscripts[currentScene].instructions; } 55 | 56 | public: 57 | 58 | /** 59 | The state of the visual novel 60 | */ 61 | VNState state; 62 | 63 | /** 64 | Destructor 65 | */ 66 | ~this() { 67 | next = -1; 68 | manuscripts.clear(); 69 | } 70 | 71 | /** 72 | Constructs a new script 73 | Always runs the "main" scene 74 | */ 75 | this(VNState state, Scene[string] manuscript) { 76 | this.state = state; 77 | manuscripts = manuscript; 78 | this.changeScene("main"); 79 | } 80 | 81 | /** 82 | Changes the scene 83 | */ 84 | void changeScene(string scene) { 85 | currentScene = scene; 86 | next = 0; 87 | state.loadCharacters(manuscripts[currentScene].characters); 88 | } 89 | 90 | /** 91 | Run the next instruction(s) 92 | Execution continues until the next non-blocking instruction 93 | */ 94 | void runNext() { 95 | while(!instructions[next].execute(this)) { 96 | next++; 97 | 98 | // Loop if we're at the end of the instructions 99 | if (next >= instructions.length) { 100 | next = 0; 101 | runNext(); 102 | return; 103 | } 104 | } 105 | next++; 106 | 107 | // Loop if we're at the end of the instructions 108 | if (next >= instructions.length) { 109 | next = 0; 110 | return; 111 | } 112 | } 113 | 114 | /** 115 | Gets a bookmark 116 | */ 117 | Bookmark bookmark() { 118 | 119 | // Count back instructions to the last blocking instruction 120 | int c = current(); 121 | if (c >= 1) { 122 | while (!instructions[c].isBlocking) { 123 | 124 | // Make sure we don't go *too* far back 125 | if (c >= 1) c--; 126 | else break; 127 | } 128 | } 129 | 130 | // Cap to first instruction if we went under instruction 0 131 | if (c < 0) c = 0; 132 | 133 | return Bookmark( 134 | currentScene, 135 | c+1 // We want to return the instruction *after* the last blocking one 136 | ); 137 | } 138 | } 139 | 140 | /** 141 | A scene in the VN 142 | */ 143 | class Scene { 144 | public: 145 | 146 | /** 147 | Creates a new scene 148 | */ 149 | this(string[] characters, IScriptInstr[] instructions) { 150 | this.characters = characters; 151 | this.instructions = instructions; 152 | } 153 | 154 | /** 155 | The characters to be loaded in this scene 156 | */ 157 | string[] characters; 158 | 159 | /** 160 | The instructions associated with a scene 161 | */ 162 | IScriptInstr[] instructions; 163 | } 164 | 165 | /** 166 | A script instruction 167 | */ 168 | interface IScriptInstr { 169 | 170 | /** 171 | Execution function that runs the instruction 172 | */ 173 | bool execute(Script script); 174 | 175 | /** 176 | Gets whether the instruction is blocking 177 | */ 178 | bool isBlocking(); 179 | } -------------------------------------------------------------------------------- /source/vorbisfile.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020, Luna Nielsen 3 | Distributed under the 2-Clause BSD License, see LICENSE file. 4 | 5 | Authors: Luna Nielsen 6 | */ 7 | module vorbisfile; 8 | import core.stdc.config; 9 | 10 | alias ogg_int64_t = long; 11 | alias ogg_uint64_t = ulong; 12 | alias ogg_int32_t = int; 13 | alias ogg_uint32_t = uint; 14 | alias ogg_int16_t = short; 15 | alias ogg_uint16_t = ushort; 16 | 17 | struct vorbis_info { 18 | int _version; // Renamed from "version", since that's a keyword in D 19 | int channels; 20 | int rate; 21 | c_long bitrate_upper; 22 | c_long bitrate_nominal; 23 | c_long bitrate_lower; 24 | c_long bitrate_window; 25 | 26 | void *codec_setup; 27 | } 28 | 29 | struct vorbis_dsp_state { 30 | int analysisp; 31 | vorbis_info* vi; 32 | float** pcm; 33 | float** pcmret; 34 | int pcm_storage; 35 | int pcm_current; 36 | int pcm_returned; 37 | int preextrapolate; 38 | int eofflag; 39 | c_long lW; 40 | c_long W; 41 | c_long nW; 42 | c_long centerW; 43 | ogg_int64_t granulepos; 44 | ogg_int64_t sequence; 45 | ogg_int64_t glue_bits; 46 | ogg_int64_t time_bits; 47 | ogg_int64_t floor_bits; 48 | ogg_int64_t res_bits; 49 | void* backend_state; 50 | } 51 | 52 | struct vorbis_comment { 53 | char** user_comments; 54 | int* comment_lengths; 55 | int comments; 56 | char* vendor; 57 | } 58 | 59 | struct alloc_chain { 60 | void* ptr; 61 | alloc_chain* next; 62 | } 63 | 64 | struct vorbis_block { 65 | float** pcm; 66 | oggpack_buffer opb; 67 | c_long lW; 68 | c_long W; 69 | c_long nW; 70 | int pcmend; 71 | int mode; 72 | int eofflag; 73 | ogg_int64_t granulepos; 74 | ogg_int64_t sequence; 75 | vorbis_dsp_state* vd; 76 | void* localstore; 77 | c_long localtop; 78 | c_long localalloc; 79 | c_long totaluse; 80 | alloc_chain* reap; 81 | c_long glue_bits; 82 | c_long time_bits; 83 | c_long floor_bits; 84 | c_long res_bits; 85 | void* internal; 86 | } 87 | 88 | 89 | struct ogg_iovec_t { 90 | void* iov_base; 91 | size_t iov_len; 92 | } 93 | 94 | struct oggpack_buffer { 95 | c_long endbyte; 96 | int endbit; 97 | ubyte* buffer; 98 | ubyte* ptr; 99 | c_long storage; 100 | } 101 | 102 | struct ogg_page { 103 | ubyte* header; 104 | c_long header_len; 105 | ubyte* _body; // originally named "body", but that's a keyword in D. 106 | c_long body_len; 107 | } 108 | 109 | struct ogg_stream_state { 110 | ubyte* body_data; 111 | c_long body_storage; 112 | c_long body_fill; 113 | c_long body_returned; 114 | int* lacing_vals; 115 | ogg_int64_t* granule_vals; 116 | c_long lacing_storage; 117 | c_long lacing_fill; 118 | c_long lacing_packet; 119 | c_long lacing_returned; 120 | ubyte[282] header; 121 | int header_fill; 122 | int e_o_s; 123 | int b_o_s; 124 | c_long serialno; 125 | c_long pageno; 126 | ogg_int64_t packetno; 127 | ogg_int64_t granulepos; 128 | } 129 | 130 | struct ogg_packet { 131 | ubyte* packet; 132 | c_long bytes; 133 | c_long b_o_s; 134 | c_long e_o_s; 135 | ogg_int64_t granulepos; 136 | ogg_int64_t packetno; 137 | } 138 | 139 | struct ogg_sync_state { 140 | ubyte* data; 141 | int storage; 142 | int fill; 143 | int returned; 144 | 145 | int unsynced; 146 | int headerbytes; 147 | int bodybytes; 148 | } 149 | 150 | struct ov_callbacks { 151 | extern(C) nothrow { 152 | size_t function(void*, size_t, size_t, void*) read_func; 153 | int function(void*, ogg_int64_t, int) seek_func; 154 | int function(void*) close_func; 155 | c_long function(void*) tell_func; 156 | } 157 | } 158 | 159 | enum { 160 | NOTOPEN =0, 161 | PARTOPEN =1, 162 | OPENED =2, 163 | STREAMSET =3, 164 | INITSET =4, 165 | 166 | OV_FALSE = -1, 167 | OV_EOF = -2, 168 | OV_HOLE = -3, 169 | OV_EREAD = -128, 170 | OV_EFAULT = -129, 171 | OV_EIMPL = -130, 172 | OV_EINVAL = -131, 173 | OV_ENOTVORBIS = -132, 174 | OV_EBADHEADER = -133, 175 | OV_EVERSION = -134, 176 | OV_ENOTAUDIO = -135, 177 | OV_EBADPACKET = -136, 178 | OV_EBADLINK = -137, 179 | OV_ENOSEEK = -138, 180 | } 181 | 182 | struct OggVorbis_File { 183 | void* datasource; 184 | int seekable; 185 | ogg_int64_t offset; 186 | ogg_int64_t end; 187 | ogg_sync_state oy; 188 | int links; 189 | ogg_int64_t* offsets; 190 | ogg_int64_t* dataoffsets; 191 | c_long* serialnos; 192 | ogg_int64_t* pcmlengths; 193 | vorbis_info* vi; 194 | vorbis_comment* vc; 195 | ogg_int64_t pcm_offset; 196 | int ready_state; 197 | c_long current_serialno; 198 | int current_link; 199 | double bittrack; 200 | double samptrack; 201 | ogg_stream_state os; 202 | vorbis_dsp_state vd; 203 | vorbis_block vb; 204 | ov_callbacks callbacks; 205 | } 206 | 207 | extern(C) @nogc nothrow __gshared: 208 | int ov_clear(OggVorbis_File*); 209 | int ov_fopen(const(char)*, OggVorbis_File*); 210 | int ov_open_callbacks(void* datasource, OggVorbis_File*, const(char)*, c_long, ov_callbacks); 211 | int ov_test_callbacks(void*, OggVorbis_File*, const(char)*, c_long, ov_callbacks); 212 | int ov_test_open(OggVorbis_File*); 213 | c_long ov_bitrate(OggVorbis_File*, int); 214 | c_long ov_bitrate_instant(OggVorbis_File*); 215 | c_long ov_streams(OggVorbis_File*); 216 | c_long ov_seekable(OggVorbis_File*); 217 | c_long ov_serialnumber(OggVorbis_File*, int); 218 | ogg_int64_t ov_raw_total(OggVorbis_File*, int); 219 | ogg_int64_t ov_pcm_total(OggVorbis_File*, int); 220 | double ov_time_total(OggVorbis_File*, int); 221 | int ov_raw_seek(OggVorbis_File*, ogg_int64_t); 222 | int ov_pcm_seek(OggVorbis_File*, ogg_int64_t); 223 | int ov_pcm_seek_page(OggVorbis_File*, ogg_int64_t); 224 | int ov_time_seek(OggVorbis_File*, double); 225 | int ov_time_seek_page(OggVorbis_File*, double); 226 | int ov_raw_seek_lap(OggVorbis_File*, ogg_int64_t); 227 | int ov_pcm_seek_lap(OggVorbis_File*, ogg_int64_t); 228 | int ov_pcm_seek_page_lap(OggVorbis_File*, ogg_int64_t); 229 | int ov_time_seek_lap(OggVorbis_File*, double); 230 | int ov_time_seek_page_lap(OggVorbis_File*, double); 231 | ogg_int64_t ov_raw_tell(OggVorbis_File*); 232 | ogg_int64_t ov_pcm_tell(OggVorbis_File*); 233 | double ov_time_tell(OggVorbis_File*); 234 | vorbis_info* ov_info(OggVorbis_File*, int); 235 | vorbis_comment* ov_comment(OggVorbis_File*, int); 236 | c_long ov_read_float(OggVorbis_File*, float***, int, int*); 237 | c_long ov_read(OggVorbis_File*, byte*, int, int, int, int, int*); 238 | int ov_crosslap(OggVorbis_File*, OggVorbis_File*); 239 | int ov_halfrate(OggVorbis_File*, int); 240 | int ov_halfrate_p(OggVorbis_File*); --------------------------------------------------------------------------------