├── .gitignore ├── .gitmodules ├── CHANGELOG ├── COMPILING.md ├── CONTRIBUTORS.md ├── Dockerfile ├── INSTALLING.txt ├── LICENSE.rst ├── OVERVIEW.md ├── README.md ├── devel ├── df-assemble.sh ├── df-launch.sh ├── dfhack.init └── package-buildmaster.sh ├── dist └── shared │ ├── data │ └── init │ │ ├── bans.txt │ │ └── dfplex.txt │ └── hack │ ├── lua │ └── plugins │ │ └── dfplex.lua │ └── scripts │ └── dfplex_restrict_z.lua ├── package.sh ├── server ├── CMakeLists.txt ├── Client.hpp ├── README.md ├── callbacks.cpp ├── callbacks.hpp ├── chat.cpp ├── chat.hpp ├── command.cpp ├── command.hpp ├── config.cpp ├── config.hpp ├── dfplex.cpp ├── dfplex.hpp ├── hackutil.cpp ├── hackutil.hpp ├── input.cpp ├── input.hpp ├── keymap.cpp ├── keymap.hpp ├── lua.cpp ├── parse_config.cpp ├── parse_config.hpp ├── screenbuf.cpp ├── screenbuf.hpp ├── server.cpp ├── server.hpp ├── serverlog.cpp ├── serverlog.hpp ├── state.cpp ├── state.hpp ├── state_cb.cpp ├── state_cb.hpp ├── staticserver.cpp └── staticserver.hpp └── static ├── README.md ├── art ├── CONTRIBUTORS.md ├── Curses.png ├── Ironhand.png ├── Ironhand_Square.png ├── Mayday.png ├── Obsidian.png ├── Phoebus.png ├── ShizzleClean.png ├── SimpleMood.png ├── Spacefox.png ├── Spacefox_16x16.png ├── curses_640x300.png ├── curses_800x600.png ├── curses_square_16x16.png ├── t_Anno.png ├── t_Bisasam.png ├── t_Cheepicus.png ├── t_Cheepicus8bit.png ├── t_Phoebus.png ├── t_Phssthpok.png ├── t_ShizzleClean.png ├── t_Spacefox.png ├── t_wanderlust.png └── wanderlust.png ├── colors ├── CONTRIBUTORS.md ├── CowThing.json ├── RedGreenColorblind.json ├── VheridAsh.json ├── VheridBone.json ├── VheridDarkSand.json ├── VheridDefault.json ├── VheridDefaultPlus.json ├── VheridDusk.json ├── VheridFallen.json ├── VheridFields.json ├── VheridHeat.json ├── VheridJade.json ├── VheridLaser.json ├── VheridMatrix.json ├── VheridMishka.json ├── VheridMud.json ├── VheridNatural.json ├── VheridSorrow.json ├── VheridWarm.json ├── chroma.json ├── curses.json └── default.json ├── config.js ├── dfplex.css ├── dfplex.html ├── favicon.ico ├── js ├── dfplex.js ├── keycode.js ├── params.js └── stats.min.js └── online.png /.gitignore: -------------------------------------------------------------------------------- 1 | /server/df_linux 2 | /server/dfhack 3 | /server/build 4 | /server/deps 5 | /static/config-local.js 6 | /package/ 7 | *.zip 8 | df.tar 9 | df/ 10 | save* 11 | release/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "server/websocketpp"] 2 | path = server/websocketpp 3 | url = https://github.com/zaphoyd/websocketpp.git 4 | [submodule "server/cpp-httplib"] 5 | path = server/cpp-httplib 6 | url = https://github.com/yhirose/cpp-httplib 7 | [submodule "server/IXWebSocket"] 8 | path = server/IXWebSocket 9 | url = https://github.com/machinezone/IXWebSocket 10 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | dfplex 2 | ====== 3 | * multiplayer 4 | 5 | v2.0.0 (webfort) 6 | ====== 7 | * New maintainer 8 | * Changed socket library to websocketpp 9 | * Moved build system to CMake 10 | * Removed queuing system 11 | * Removed idle timer 12 | * Added environment vars for basic configuration 13 | * Changed from host-side resizing to a fixed game size 14 | * Added letterboxing to client-side views 15 | * Added IRC sidepane 16 | * Added user count, time left, and player nicknames to UI 17 | * Added configurable client parameters 18 | * Moved from fixed-delay to capped framerate 19 | * Added version negotation 20 | 21 | v1.10 (webfort) 22 | ===== 23 | * Initial release 24 | -------------------------------------------------------------------------------- /COMPILING.md: -------------------------------------------------------------------------------- 1 | ## HOW TO COMPILE ## 2 | 3 | Dwarfplex currently compiles as a submodule of DFHack, much like 4 | stonesense. Please clone the dfhack repo from 5 | the SAME github namespace as you found this repository in and 6 | follow its build instructions. 7 | 8 | Linux -- After cloning dfhack, You can use `devel/df-assemble.sh` 9 | in this repository as a shortcut. -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | Here is a non-exhaustive list of everyone whose work went into creating 4 | DFplex and its ancestor, Webfortress. If you'd like to see someone's name on here, please contact an author or send a PR. 5 | 6 | ### DFPlex ### 7 | ``` 8 | white-rabbit 9 | BenLubar 10 | flbr 11 | Paingouin 12 | DaKnig 13 | ``` 14 | 15 | ### Webfortress ### 16 | ``` 17 | mifki 18 | alloyed 19 | Dragoon209 20 | ``` 21 | 22 | ### Special Thanks ### 23 | ``` 24 | Lethosar, who provided much advice. 25 | Hexa, for setting up the first public server. 26 | ``` 27 | 28 | 29 | DFplex also uses tilesets/colorschemes created by the wider DF 30 | community, their names can be found in the static/art and static/colors 31 | folders respectively -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | # Pre install 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | RUN apt-get update 6 | 7 | RUN apt-get -y install --no-install-recommends build-essential libsdl1.2debian libsdl-image1.2 libsdl-ttf2.0-0 libopenal1 libsndfile1 libgtk2.0-0 libncursesw5 libglu1 unzip curl libboost-all-dev bzip2 8 | 9 | ENV DEBIAN_FRONTEND=dialog 10 | 11 | # doing build here would be ideal to date. 12 | ADD http://www.bay12games.com/dwarves/df_47_04_linux.tar.bz2 df.tar.bz2 13 | ADD https://github.com/white-rabbit-dfplex/dfplex/releases/download/v0.2.1-dfplex/dfplex-v0.2.1-linux64.zip dfplex.zip 14 | ADD https://github.com/DFHack/dfhack/releases/download/0.47.04-r1/dfhack-0.47.04-r1-Linux-64bit-gcc-4.8.tar.bz2 dfhack.tar.bz2 15 | 16 | 17 | RUN tar xfj df.tar.bz2 && rm df.tar.bz2 18 | RUN tar xfj dfhack.tar.bz2 -C /df_linux && rm dfhack.tar.bz2 19 | RUN unzip dfplex.zip && rm dfplex.zip && cp -r dfplex-v0.2.1-Linux64/* df_linux/ && rm -rf dfplex-v0.2.1-Linux64/ 20 | RUN echo "\n[PRINT_MODE:TEXT]\n[INTRO:NO]\n[TRUETYPE:NO]\n[SOUND:NO]" >> /df_linux/data/init/init.txt 21 | RUN echo "\n[BIRTH_CITIZEN:A_D:D_D]\n[MOOD_BUILDING_CLAIMED:A_D:D_D]\n[ARTIFACT_BEGUN:A_D:D_D]" >> /df_linux/data/init/announcements.txt 22 | EXPOSE 8000 23 | EXPOSE 5000 24 | EXPOSE 1234 25 | 26 | ENTRYPOINT ["df_linux/dfhack"] 27 | -------------------------------------------------------------------------------- /INSTALLING.txt: -------------------------------------------------------------------------------- 1 | ## INSTALLING ## 2 | 3 | 1. Installing the plugin. 4 | 5 | DFPlex is the dfhack plugin that gets added directly to your Dwarf 6 | Fortress install. Just follow the dfhack installation instructions. If you're building 7 | from source, see the build instructions in the README. 8 | 9 | 2. Launch dfhack 10 | 11 | Run dfhack. In the DFHack Console, you should see a line that says: 12 | 13 | [DFPLEX] [2014-11-10 06:11:06] [application] DFPlex started on port 1234 14 | 15 | If you see something like that, then DFPlex has been properly installed. However, 16 | the port mentioned in the console here is not the one to connect to. 17 | 18 | 3. Port Forwarding 19 | 20 | For external users to connect to your webfort, they will need to access port 21 | 1234, and port 8000. This is typically done via port 22 | forwarding. If you haven't done this before, see 23 | 24 | http://portforward.com/ 25 | 26 | for a list of guides for common routers. 27 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | ######## 4 | Licenses 5 | ######## 6 | 7 | DFHack is distributed under the Zlib license, with some MIT- 8 | and BSD-licensed components. These licenses protect your right 9 | to use DFHack for any purpose, distribute copies, and so on. 10 | 11 | The core, plugins, scripts, and other DFHack code all use the 12 | ZLib license unless noted otherwise. By contributing to DFHack, 13 | authors release the contributed work under this license. 14 | 15 | DFHack also draws on several external packages. 16 | Their licenses are summarised here and reproduced below. 17 | 18 | =============== ============= ================================================= 19 | Component License Copyright 20 | =============== ============= ================================================= 21 | DFHack_ Zlib \(c\) 2009-2012, Petr Mrรกzek 22 | clsocket_ BSD 3-clause \(c\) 2007-2009, CarrierLabs, LLC. 23 | dirent_ MIT \(c\) 2006, Toni Ronkko 24 | JSON.lua_ CC-BY-SA_ \(c\) 2010-2014, Jeffrey Friedl 25 | jsoncpp_ MIT \(c\) 2007-2010, Baptiste Lepilleur 26 | linenoise_ BSD 2-clause \(c\) 2010, Salvatore Sanfilippo & Pieter Noordhuis 27 | lua_ MIT \(c\) 1994-2008, Lua.org, PUC-Rio. 28 | luafilesystem_ MIT \(c\) 2003-2014, Kepler Project 29 | lua-profiler_ MIT \(c\) 2002,2003,2004 Pepperfish 30 | protobuf_ BSD 3-clause \(c\) 2008, Google Inc. 31 | tinythread_ Zlib \(c\) 2010, Marcus Geelnard 32 | tinyxml_ Zlib \(c\) 2000-2006, Lee Thomason 33 | UTF-8-decoder_ MIT \(c\) 2008-2010, Bjoern Hoehrmann 34 | webfort_ ISC \(c\) 2014, Vitaly Pronkin, Kyle Mclamb 35 | cpp-httplib_ MIT \(c\) 2017, yhirose 36 | websocketpp_ BSD 3-clause \(c\) 2014, Peter Thorson 37 | ixwebsockets_ BSD 3-clause \(c\) 2018, Machine Zone 38 | =============== ============= ================================================= 39 | 40 | .. _DFHack: https://github.com/DFHack/dfhack 41 | .. _clsocket: https://github.com/DFHack/clsocket 42 | .. _dirent: https://github.com/tronkko/dirent 43 | .. _JSON.lua: http://regex.info/blog/lua/json 44 | .. _jsoncpp: https://github.com/open-source-parsers/jsoncpp 45 | .. _linenoise: http://github.com/antirez/linenoise 46 | .. _lua: http://www.lua.org 47 | .. _luafilesystem: https://github.com/keplerproject/luafilesystem 48 | .. _protobuf: https://github.com/google/protobuf 49 | .. _tinythread: http://tinythreadpp.bitsnbites.eu/ 50 | .. _tinyxml: http://www.sourceforge.net/projects/tinyxml 51 | .. _UTF-8-decoder: http://bjoern.hoehrmann.de/utf-8/decoder/dfa 52 | .. _lua-profiler: http://lua-users.org/wiki/PepperfishProfiler 53 | .. _webfort: https://github.com/Ankoku/df-webfort 54 | .. _cpp-httplib: https://github.com/yhirose/cpp-httplib 55 | .. _websocketpp: https://github.com/zaphoyd/websocketpp 56 | .. _ixwebsockets: https://github.com/machinezone/IXWebSocket 57 | .. _CC-BY-SA: http://creativecommons.org/licenses/by/3.0/deed.en_US 58 | 59 | 60 | Zlib License 61 | ============ 62 | See https://en.wikipedia.org/wiki/Zlib_License 63 | :: 64 | 65 | This software is provided 'as-is', without any express or implied 66 | warranty. In no event will the authors be held liable for any 67 | damages arising from the use of this software. 68 | 69 | Permission is granted to anyone to use this software for any 70 | purpose, including commercial applications, and to alter it and 71 | redistribute it freely, subject to the following restrictions: 72 | 73 | 1. The origin of this software must not be misrepresented; you must 74 | not claim that you wrote the original software. If you use this 75 | software in a product, an acknowledgment in the product 76 | documentation would be appreciated but is not required. 77 | 78 | 2. Altered source versions must be plainly marked as such, and 79 | must not be misrepresented as being the original software. 80 | 81 | 3. This notice may not be removed or altered from any source 82 | distribution. 83 | 84 | MIT License 85 | =========== 86 | See https://en.wikipedia.org/wiki/MIT_License 87 | :: 88 | 89 | Permission is hereby granted, free of charge, to any person obtaining 90 | a copy of this software and associated documentation files (the 91 | "Software"), to deal in the Software without restriction, including 92 | without limitation the rights to use, copy, modify, merge, publish, 93 | distribute, sublicense, and/or sell copies of the Software, and to 94 | permit persons to whom the Software is furnished to do so, subject to 95 | the following conditions: 96 | 97 | The above copyright notice and this permission notice shall be included 98 | in all copies or substantial portions of the Software. 99 | 100 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 101 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 102 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 103 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 104 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 105 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 106 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 107 | 108 | ISC License 109 | =========== 110 | See https://en.wikipedia.org/wiki/ISC_license 111 | :: 112 | 113 | Permission to use, copy, modify, and/or distribute this software for any 114 | purpose with or without fee is hereby granted, provided that the above 115 | copyright notice and this permission notice appear in all copies. 116 | 117 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 118 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 119 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 120 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 121 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 122 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 123 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 124 | 125 | ISC License 126 | ============ 127 | See https://en.wikipedia.org/wiki/ISC_license 128 | :: 129 | 130 | Permission to use, copy, modify, and/or distribute this software for any 131 | purpose with or without fee is hereby granted, provided that the above 132 | copyright notice and this permission notice appear in all copies. 133 | 134 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 135 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 136 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 137 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 138 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 139 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 140 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 141 | 142 | BSD Licenses 143 | ============ 144 | See https://en.wikipedia.org/wiki/BSD_licenses 145 | :: 146 | 147 | Redistribution and use in source and binary forms, with or without 148 | modification, are permitted provided that the following conditions are 149 | met: 150 | 151 | 1. Redistributions of source code must retain the above copyright 152 | notice, this list of conditions and the following disclaimer. 153 | 154 | 2. Redistributions in binary form must reproduce the above copyright 155 | notice, this list of conditions and the following disclaimer in 156 | the documentation and/or other materials provided with the 157 | distribution. 158 | 159 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 160 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 161 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 162 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 163 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 164 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 165 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 166 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 167 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 168 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 169 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 170 | 171 | ``linenoise`` adds no further clauses. 172 | 173 | ``protobuf`` adds the following clause:: 174 | 175 | 3. Neither the name of Google Inc. nor the names of its 176 | contributors may be used to endorse or promote products derived 177 | from this software without specific prior written permission. 178 | 179 | ``clsocket`` adds the following clauses:: 180 | 181 | 3. The name of the author may not be used to endorse or promote 182 | products derived from this software without specific prior 183 | written permission. 184 | 185 | 4. The name "CarrierLabs" must not be used to endorse or promote 186 | products derived from this software without prior written 187 | permission. For written permission, please contact 188 | mark@carrierlabs.com 189 | 190 | ``websocketpp`` adds the following clause:: 191 | 192 | * Neither the name of the WebSocket++ Project nor the 193 | names of its contributors may be used to endorse or promote products 194 | derived from this software without specific prior written permission. 195 | 196 | ``websocketpp`` also includes these licenses in "Bundled Libraries":: 197 | 198 | ****** Base 64 Library (base64/base64.hpp) ****** 199 | base64.hpp is a repackaging of the base64.cpp and base64.h files into a 200 | single header suitable for use as a header only library. This conversion was 201 | done by Peter Thorson (webmaster@zaphoyd.com) in 2012. All modifications to 202 | the code are redistributed under the same license as the original, which is 203 | listed below. 204 | 205 | base64.cpp and base64.h 206 | 207 | Copyright (C) 2004-2008 René Nyffenegger 208 | 209 | This source code is provided 'as-is', without any express or implied 210 | warranty. In no event will the author be held liable for any damages 211 | arising from the use of this software. 212 | 213 | Permission is granted to anyone to use this software for any purpose, 214 | including commercial applications, and to alter it and redistribute it 215 | freely, subject to the following restrictions: 216 | 217 | 1. The origin of this source code must not be misrepresented; you must not 218 | claim that you wrote the original source code. If you use this source code 219 | in a product, an acknowledgment in the product documentation would be 220 | appreciated but is not required. 221 | 222 | 2. Altered source versions must be plainly marked as such, and must not be 223 | misrepresented as being the original source code. 224 | 225 | 3. This notice may not be removed or altered from any source distribution. 226 | 227 | René Nyffenegger rene.nyffenegger@adp-gmbh.ch 228 | 229 | ****** SHA1 Library (sha1/sha1.hpp) ****** 230 | sha1.hpp is a repackaging of the sha1.cpp and sha1.h files from the shallsha1 231 | library (http://code.google.com/p/smallsha1/) into a single header suitable for 232 | use as a header only library. This conversion was done by Peter Thorson 233 | (webmaster@zaphoyd.com) in 2013. All modifications to the code are redistributed 234 | under the same license as the original, which is listed below. 235 | 236 | Copyright (c) 2011, Micael Hildenborg 237 | All rights reserved. 238 | 239 | Redistribution and use in source and binary forms, with or without 240 | modification, are permitted provided that the following conditions are met: 241 | * Redistributions of source code must retain the above copyright 242 | notice, this list of conditions and the following disclaimer. 243 | * Redistributions in binary form must reproduce the above copyright 244 | notice, this list of conditions and the following disclaimer in the 245 | documentation and/or other materials provided with the distribution. 246 | * Neither the name of Micael Hildenborg nor the 247 | names of its contributors may be used to endorse or promote products 248 | derived from this software without specific prior written permission. 249 | 250 | THIS SOFTWARE IS PROVIDED BY Micael Hildenborg ''AS IS'' AND ANY 251 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 252 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 253 | DISCLAIMED. IN NO EVENT SHALL Micael Hildenborg BE LIABLE FOR ANY 254 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 255 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 256 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 257 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 258 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 259 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 260 | 261 | ****** MD5 Library (common/md5.hpp) ****** 262 | md5.hpp is a reformulation of the md5.h and md5.c code from 263 | http://www.opensource.apple.com/source/cups/cups-59/cups/md5.c to allow it to 264 | function as a component of a header only library. This conversion was done by 265 | Peter Thorson (webmaster@zaphoyd.com) in 2012 for the WebSocket++ project. The 266 | changes are released under the same license as the original (listed below) 267 | 268 | Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. 269 | 270 | This software is provided 'as-is', without any express or implied 271 | warranty. In no event will the authors be held liable for any damages 272 | arising from the use of this software. 273 | 274 | Permission is granted to anyone to use this software for any purpose, 275 | including commercial applications, and to alter it and redistribute it 276 | freely, subject to the following restrictions: 277 | 278 | 1. The origin of this software must not be misrepresented; you must not 279 | claim that you wrote the original software. If you use this software 280 | in a product, an acknowledgment in the product documentation would be 281 | appreciated but is not required. 282 | 2. Altered source versions must be plainly marked as such, and must not be 283 | misrepresented as being the original software. 284 | 3. This notice may not be removed or altered from any source distribution. 285 | 286 | L. Peter Deutsch 287 | ghost@aladdin.com 288 | 289 | ****** UTF8 Validation logic (utf8_validation.hpp) ****** 290 | utf8_validation.hpp is adapted from code originally written by Bjoern Hoehrmann 291 | . See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for 292 | details. 293 | 294 | The original license: 295 | 296 | Copyright (c) 2008-2009 Bjoern Hoehrmann 297 | 298 | Permission is hereby granted, free of charge, to any person obtaining a copy 299 | of this software and associated documentation files (the "Software"), to deal 300 | in the Software without restriction, including without limitation the rights 301 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 302 | copies of the Software, and to permit persons to whom the Software is 303 | furnished to do so, subject to the following conditions: 304 | 305 | The above copyright notice and this permission notice shall be included in 306 | all copies or substantial portions of the Software. 307 | 308 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 309 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 310 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 311 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 312 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 313 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 314 | SOFTWARE. -------------------------------------------------------------------------------- /OVERVIEW.md: -------------------------------------------------------------------------------- 1 | # Code overview 2 | 3 | DFPlex adds multiplayer into Dwarf mode by using the following simple algorithm every frame *for each player* (see `dfplex.cpp:dfplex_update()`): 4 | 5 | 1. (`state.cpp:restore_state()`) Restore the player's UI state (where they are looking, their cursor position, current menu, etc.) primarily through two different means: 6 | - Applying a sequence of key commands that were previously entered by this client. For example, the command sequence `D_NOBLES`, `STANDARDSCROLL_DOWN`, `STANDARDSCROLL_DOWN` will open up the `[n]obles` menu and scroll down twice. (See `state.cpp:apply_restore_key()`) 7 | - Directly editing dwarf fortress memory -- the cursor position and view position are restored this way, and a few other things as well. This editing occurs before, after, or even during the application of the keypress sequence above. (See `state.cpp`'s `restore_cursor()`, `restore_data()`, `restore_post_state()`, etc.) 8 | 2. (`command.cpp:apply_command()`) Apply any new key commands the player has entered this frame. If the command changes the current menu (see `hackutil.cpp:get_current_menu_id()`), it is stored and added to the key command sequence. 9 | - If we return to a previous menu, instead of adding the key to the sequence, we remove some keys. 10 | - Certain keys in certain menus are special cased, as the general-purpose logic doesn't work for those keys. See `command.cpp:apply_special_case()`. This is quite a meaty function! 11 | - Scrolling and typing keys are special cased so that they are always saved (except in certain special cases where they are not). 12 | 3. `screenbuf.cpp:perform_render()` Render and store the contents of the screen. 13 | 4. `screenbuf.cpp:transfer_screenbuf_client()` Copy the new screen to the client. (To save bandwidth, only the CURSES character information is sent, rather than pixels, and delta-encoding is used.) 14 | 5. `hackutil.cpp:return_to_root()` Return to the main dwarfmode screen. 15 | 6. (`server.cpp:tock()`) Later, when the client requests an update, the screen information is sent. 16 | 17 | Finally, after all player updates have occurred, we set the pause state and return from the plugin `dfplex_update()` so that DF may advance a frame on its own. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DFPlex ## 2 | 3 | *"Why must the cancer of multiplayer afflict everything?"* -- [/u/JesterHell](https://www.reddit.com/r/dwarffortress/comments/g8trnf/multiplayer_dwarf_fortress_is_now_a_reality/foptfyn/) 4 | 5 | *"...could be the end of all games."* -- [/u/r4nge](https://www.reddit.com/r/dwarffortress/comments/g8trnf/multiplayer_dwarf_fortress_is_now_a_reality/foqxr97/) 6 | 7 | DFPlex is a plugin for [DFHack](https://github.com/DFHack/dfhack) which brings simultaneous, real-time co-op multiplayer Fortress mode to [Dwarf Fortress](http://www.bay12games.com/dwarves/). Each player has their own independent view, cursor, menus, etc. so nobody has to wrestle for control. It's a fork of [Webfort](https://github.com/Ankoku/df-webfort), so players can join just by connecting from their web browser. 8 | 9 | If you prefer the solo experience, DFPlex allows you to have multiple views into your own fortress, or simply to run the game without pausing every time you enter a menu (an optional feature that improves the pacing on multiplayer). 10 | 11 | ### Installing ### 12 | 13 | See the [releases page](https://github.com/white-rabbit-dfplex/dfplex/releases) for a pre-built option. Simply extract the zip file into your DFHack installation, paying attention to ensure that the files end up in the appropriate subdirectories. 14 | 15 | To compile from source, you can use git: 16 | 17 | ``` 18 | git clone --recursive https://github.com/white-rabbit-dfplex/dfhack 19 | ``` 20 | 21 | Then just follow the [build instructions for dfhack](https://dfhack.readthedocs.io/en/stable/docs/Compile.html). Please take care to ensure that you install into the correct version of Dwarf Fortress. You can check which version DFHack is compatible with by looking at the `CMakeLists.txt` file in the DFHack repo. 22 | 23 | ### Launching DFPlex ### 24 | 25 | After installing DFPlex (along with DFHack), simply put the line `enable dfplex` in your `dfhack.init` file. Consider removing all other lines from that file, because dfplex is currently incompatible with many other plugins. Then simply connect to [http://localhost:8000/](http://localhost:8000/) in your web browser. 26 | 27 | #### Configuration #### 28 | 29 | Edit the file `data/init/dfplex.txt` to configure dfplex. If that file is missing, it can be found [here](dist/shared/data/init/dfplex.txt). 30 | 31 | In addition, we suggest enabling seasonal autosave (in `data/init/d_init.txt`) and disabling pause/zoom for certain common announcements (`data/init/announcements.txt`) by replacing their respective lines with these: `[BIRTH_CITIZEN:A_D:D_D]`, `[MOOD_BUILDING_CLAIMED:A_D:D_D]`, `[ARTIFACT_BEGUN:A_D:D_D]`. (While playtesting, these particular announcements have been especially disruptive.) 32 | 33 | To customize the graphics, edit `hack/www/config.js`. Players can set their own graphics by editing the URL in their web browser -- [here is a guide](static/README.md) from the authors of Webfort. 34 | 35 | #### Online Play #### 36 | 37 | DFPlex requires two ports to be available. They are both displayed in the dfplex window upon launching, and can be configured in `data/init/dfplex.txt`. To play on LAN, players can simply connect to your LAN IP address at the correct port: for example, [http://192.168.1.1:8000/](http://192.168.1.1:8000/). To play online (as opposed to on LAN), port forwarding must be enabled on your router. Enabling port forwarding and finding your LAN or WAN IP address are beyond the scope of this readme, so please look these up online if you are unfamiliar with the process. 38 | 39 | **DFPlex is not secure**. If you wish to play with people you do not trust, please take your own security precautions, such as running Dwarf Fortress within an isolated container. No efforts were taken by the authors of DFPlex to prevent clients from accessing the host filesystem. 40 | 41 | #### Contribution #### 42 | 43 | To learn how the code is structured and how the project works, please read [the overview](OVERVIEW.md). 44 | -------------------------------------------------------------------------------- /devel/df-assemble.sh: -------------------------------------------------------------------------------- 1 | # go to the correct directory 2 | if [ ! -d "plugins/" ] 3 | then 4 | if [ ! -d "devel" ] 5 | then 6 | cd .. 7 | fi 8 | if [ ! -d "devel" ] 9 | then 10 | echo "error: must be run from dfhack or dfhack/plugins/dfplex directory." 11 | exit 3 12 | fi 13 | cd ../.. 14 | if [ ! -d "plugins/" ] 15 | then 16 | echo "error: must be run from dfhack or dfhack/plugins/dfplex directory." 17 | exit 3 18 | fi 19 | fi 20 | 21 | # read df version from CMakeLists.txt 22 | export version=$(grep "set(DF_VERSION" CMakeLists.txt | sed -e 's/set(DF_VERSION "\(.*\)\")/\1/') 23 | 24 | if [ ! -f "build/CMakeCache.txt" ] 25 | then 26 | echo "Build dfhack into the build/ directory before running this script." 27 | fi 28 | 29 | which tar 30 | if [ $? -ne 0 ] 31 | then 32 | echo "tar (the program) not found." 33 | exit 3 34 | fi 35 | 36 | which bzip2 37 | if [ $? -ne 0 ] 38 | then 39 | echo "bzip2 (the program) not found." 40 | exit 3 41 | fi 42 | 43 | echo "df version $version" 44 | 45 | plexdir="plugins/dfplex" 46 | 47 | cd "$plexdir/" 48 | 49 | bzdest="df.tar.bz2" 50 | tardest="df.tar" 51 | 52 | if [ ! -f $tardest ] 53 | then 54 | if [ ! -f $bzdest ] 55 | then 56 | echo "downloading dwarf fortress v$version from bay12games.com..." 57 | sleep 1 58 | minor=$(echo "$version" | cut -d. -f2) 59 | patch=$(echo "$version" | cut -d. -f3) 60 | url="http://www.bay12games.com/dwarves/df_${minor}_${patch}_linux.tar.bz2" 61 | echo Downloading from $url 62 | wget "$url" -O "$bzdest" 63 | fi 64 | 65 | if [ -f "$bzdest" ] 66 | then 67 | echo "decompressing..." 68 | bzip2 -d $bzdest 69 | if [ -f "df_*.tar" ] 70 | then 71 | mv "df_*.tar" $tardest 72 | fi 73 | else 74 | echo "Failed to download." 75 | exit 2 76 | fi 77 | fi 78 | 79 | if [ ! -f "$tardest" ] 80 | then 81 | echo "Failed to download and decompress dwarf fortress. ($tardest not found.)" 82 | exit 2 83 | fi 84 | 85 | dfdir="df" 86 | 87 | # remove previous df version 88 | if [ -d $dfdir ] 89 | then 90 | echo "removing previous $dfdir/" 91 | if [ -L $dfdir/data/save ] 92 | then 93 | echo "removing symbollic link to save directory." 94 | unlink $dfdir/data/save 95 | fi 96 | rm -r $dfdir 97 | fi 98 | mkdir $dfdir 99 | 100 | echo "Extracting df.tar into $dfdir/" 101 | # extract tar 102 | tar xf "$tardest" --strip-components=1 -C $dfdir 103 | 104 | if [ $? -ne 0 ] 105 | then 106 | echo "Failed to extract dwarf fortress tar to $dfdir/" 107 | exit 2 108 | fi 109 | 110 | cd ../.. 111 | if [ ! -d "build" ] 112 | then 113 | echo "build/ directory not found in $(pwd)" 114 | exit 4 115 | fi 116 | 117 | # install dfhack into df 118 | echo "installing dfhack into $dfdir ..." 119 | echo "" 120 | cd build/ 121 | # ideally, this only forces a rebuild if the prefix was not set correctly. 122 | makeprogram=make 123 | 124 | grep -q "^CMAKE_MAKE_PROGRAM.*ninja" CMakeCache.txt 125 | if [ $? -eq 0 ] 126 | then 127 | makeprogram=ninja 128 | fi 129 | cmake .. -DCONSOLE_NO_CATCH=ON -DCMAKE_INSTALL_PREFIX=../$plexdir/$dfdir && $makeprogram install -j8 130 | if [ $? -ne 0 ] 131 | then 132 | echo "installation failed." 133 | exit 4 134 | fi 135 | cd .. 136 | 137 | if [ ! -d $plexdir ] 138 | then 139 | echo "Failed to find $plexdir" 140 | exit 4 141 | fi 142 | 143 | cd $plexdir 144 | 145 | # link in save data 146 | echo "softlinking local save data (for persistence)" 147 | if [ ! -d save ] 148 | then 149 | mkdir save 150 | fi 151 | if [ -d $dfdir/data/save ] 152 | then 153 | rm -r $dfdir/data/save 154 | fi 155 | cd $dfdir/data 156 | ln -s ../../save ./save 157 | cd - 158 | 159 | # edit init.txt 160 | echo "editing df configuration." 161 | sed -i 's/G_FPS_CAP:[0-9]*/G_FPS_CAP:8000/g' $dfdir/data/init/init.txt 162 | sed -i 's/MACRO_MS:[0-9]*/MACRO_MS:0/g' $dfdir/data/init/init.txt 163 | # disable sound for development convenience 164 | sed -i 's/SOUND:\(YES\|NO\)/SOUND:NO/g' $dfdir/data/init/init.txt 165 | # autosave 166 | sed -i 's/AUTOSAVE:NONE/AUTOSAVE:SEASONAL/g' $dfdir/data/init/d_init.txt 167 | # engravings 168 | sed -i 's/ENGRAVINGS_START_OBSCURED:NO/ENGRAVINGS_START_OBSCURED:YES/g' $dfdir/data/init/d_init.txt 169 | 170 | # announcement edits for more harmonious multiplayer 171 | sed -i 's/BIRTH_CITIZEN:A_D:D_D:P:R/BIRTH_CITIZEN:A_D:D_D/g' $dfdir/data/init/announcements.txt 172 | sed -i 's/MOOD_BUILDING_CLAIMED:A_D:D_D:P:R/MOOD_BUILDING_CLAIMED:A_D:D_D/g' $dfdir/data/init/announcements.txt 173 | sed -i 's/ARTIFACT_BEGUN:A_D:D_D:P:R/ARTIFACT_BEGUN:A_D:D_D/g' $dfdir/data/init/announcements.txt 174 | 175 | dfplexinit="devel/dfhack.init" 176 | if [ -f $dfplexinit ] 177 | then 178 | echo "copying in dfplex's dfhack.init" 179 | cp $dfplexinit "$dfdir/dfhack.init" 180 | fi 181 | 182 | chmod a+x devel/df-launch.sh 183 | 184 | echo 185 | echo "setup complete. dfhack is installed alongside df in $dfdir/" 186 | echo "Now run the following command:" 187 | echo "" 188 | echo " devel/df-launch.sh" 189 | echo "" 190 | echo "Then connect to localhost:1233/dfplex.html in your web browser." 191 | -------------------------------------------------------------------------------- /devel/df-launch.sh: -------------------------------------------------------------------------------- 1 | # go to the correct directory 2 | if [ ! -d "plugins/" ] 3 | then 4 | if [ ! -d "devel" ] 5 | then 6 | cd .. 7 | fi 8 | if [ ! -d "devel" ] 9 | then 10 | echo "error: must be run from dfhack or dfhack/plugins/dfplex directory." 11 | exit 3 12 | fi 13 | cd ../.. 14 | if [ ! -d "plugins/" ] 15 | then 16 | echo "error: must be run from dfhack or dfhack/plugins/dfplex directory." 17 | exit 3 18 | fi 19 | fi 20 | cd plugins/dfplex 21 | #!/bin/bash 22 | set -x 23 | dfdir="df" 24 | if [ ! -d $dfdir ] 25 | then 26 | echo "$dfdir/ does not exist. Did you run df-assemble.sh?" 27 | exit 1 28 | fi 29 | 30 | export DFPLEX_STATICPORT="8000" 31 | export DFPLEX_STATICDIR="../static" 32 | 33 | cd $dfdir 34 | ./dfhack "$@" 35 | cd - 36 | -------------------------------------------------------------------------------- /devel/dfhack.init: -------------------------------------------------------------------------------- 1 | enable dfplex 2 | -------------------------------------------------------------------------------- /devel/package-buildmaster.sh: -------------------------------------------------------------------------------- 1 | if [ "$#" -ne 1 ] 2 | then 3 | echo "usage: $0 " 4 | exit 5 | fi 6 | 7 | buildnumber="$1" 8 | url="https://buildmaster.lubar.me/artifacts/download?applicationId=48&releaseNumber=dfhack-0.47.04-r1&buildNumber=${1}&artifactName=" 9 | dst="release" 10 | pre="dfplex-v0.2-" 11 | assets="${pre}assets" 12 | root=`pwd` 13 | 14 | if [ ! -d "static" ] 15 | then 16 | echo "static/ folder not found -- is this the right directory?" 17 | exit 1 18 | fi 19 | 20 | if [ -d "$dst" ] 21 | then 22 | echo "Removing previous..." 23 | rm -r "$dst" 24 | fi 25 | 26 | ############ create assets template ############# 27 | echo "Creating asset template..." 28 | mkdir "$dst" && cd "$dst" && mkdir "$assets" && cd "$assets" 29 | 30 | # dist/shared 31 | for filename in $root/dist/shared/* 32 | do 33 | if [ -d "$filename" ] 34 | then 35 | cp -r "$filename" . 36 | elif [ -f "$filename" ] 37 | then 38 | cp "$filename" . 39 | fi 40 | done 41 | 42 | # static 43 | cp -r "$root/static" "./hack/www" 44 | 45 | # dfhack init 46 | cp "$root/devel/dfhack.init" . 47 | 48 | # license 49 | cp "$root/LICENSE.rst" . 50 | 51 | ############ create target packages ############# 52 | for platform in "Windows32" "Windows64" "Linux32" "Linux64" 53 | do 54 | cd "$root/$dst" 55 | echo "assembling ${platform}" 56 | target="${pre}${platform}" 57 | cp -r "${assets}" "${target}" 58 | cd "${target}/hack" 59 | if [ ! -d "plugins" ] 60 | then 61 | mkdir "plugins" 62 | fi 63 | cd "plugins" 64 | 65 | # Download zip from build server 66 | wget -O "artifact.zip" "${url}${platform}" 67 | if [ $? -ne 0 ] 68 | then 69 | echo "Failed to download artifact for $platform". 70 | exit 2 71 | fi 72 | 73 | # extract zip 74 | 7z e "artifact.zip" 75 | if [ $? -ne 0 ] 76 | then 77 | echo "Failed to extract zip for $platform". 78 | exit 3 79 | fi 80 | 81 | # remove zip to avoid name clash with next step. 82 | rm "artifact.zip" 83 | 84 | # extract zip 85 | zip=`find . -name "*.zip"` 86 | if [ -f "$zip" ] 87 | then 88 | 7z e "$zip" 89 | if [ $? -ne 0 ] 90 | then 91 | echo "Failed to extract archive for $platform". 92 | exit 5 93 | fi 94 | fi 95 | 96 | # extract tar.gz 97 | targz=`find . -name "*.tar.gz"` 98 | if [ -f "$targz" ] 99 | then 100 | tar -xf "$targz" 101 | if [ $? -ne 0 ] 102 | then 103 | echo "Failed to extract archive for $platform". 104 | exit 5 105 | fi 106 | fi 107 | 108 | if find . | grep -q "dfplex.plug.*" 109 | then 110 | echo "Extracted binary for $platform successfully." 111 | else 112 | echo "failed to extract binary from archive for $platform. Contents are:" 113 | find . 114 | exit 4 115 | fi 116 | 117 | # clean up. 118 | touch "a.zip" 119 | touch "a.tar.gz" 120 | rm *.zip 121 | rm *.tar.gz 122 | 123 | cd "$root/$dst" 124 | echo "Creating $target.zip ..." 125 | 7z a "$target.zip" "$target" 126 | 127 | if [ $? -ne 0 ] 128 | then 129 | echo "Failed to create archive for $platform, $target.zip". 130 | exit 7 131 | fi 132 | 133 | echo "" 134 | done -------------------------------------------------------------------------------- /dist/shared/data/init/bans.txt: -------------------------------------------------------------------------------- 1 | # List ip addresses here to ban them. 2 | # The ban list is reloaded by dfplex every 45 seconds. 3 | # You can view IP addresses in dfplex_server.log 4 | 5 | # For example, uncomment these lines: 6 | #45.2.13.253 7 | #::ffff:127.0.0.1 -------------------------------------------------------------------------------- /dist/shared/data/init/dfplex.txt: -------------------------------------------------------------------------------- 1 | This is the DFPlex config file, same syntax as DF raws. 2 | 3 | All (keyboard) keys are given as their ASCII encoding. A value of 0 will disable that key. 4 | 5 | (Don't forget to forward your ports if you're not playing on LAN.) 6 | 7 | The port the static site will be served on. Players should connect to this port in their web browsers. 8 | If this is set to 0, the static site server will not start. (You can use this to help identify 9 | what the cause of a crash is, or to run your own static site server, e.g. `python -m SimpleHTTPServer`) 10 | [STATICPORT:8000] 11 | 12 | The directory containing static site files to be served. 13 | [STATICDIR:hack/www] 14 | 15 | The port the server's websocket will listen on. Don't forget to forward your ports. 16 | If this is set to 0, the websocket will not start. (You can use this to help identify what the cause of a crash is.) 17 | [PORT:1234] 18 | 19 | The max number of concurrent connections that the server will accept. 20 | Zero means unbounded. 21 | [MAX_CLIENTS:0] 22 | 23 | Enable autosave while nobody is playing. You probably shouldn't use this yet. 24 | [AUTOSAVE_WHILE_IDLE:NO] 25 | 26 | This key toggles multiplex mode. 27 | [MULTIPLEXKEY:92] 28 | 29 | This key toggles client's debug output 30 | [DEBUGKEY:124] 31 | 32 | This key toggles server's debug output 33 | [SERVERDEBUGKEY:0] (125) 34 | 35 | Zoom to next client position (from default view) 36 | [PREV_CLIENT_POS_KEY:91] 37 | [NEXT_CLIENT_POS_KEY:93] 38 | 39 | Pause behaviour: 40 | ALWAYS: if any player enters a menu, the game will pause. 41 | EXPLICIT: Players can only pause the game by pressing the pause key. 42 | EXPLICIT_DWARFMENU: like "Explicit", but the pause button can be pressed 43 | from any submenu in the main dwarfmode view that doesn't use the key. 44 | *Experimental.* 45 | EXPLICIT_ANYMENU: like "Explicit", but the pause button can be pressed 46 | from *any* menu that doesn't use the key. 47 | *Experimental.* 48 | [PAUSE:EXPLICIT] 49 | 50 | Set this to 0 to disable in-game chat. 51 | [CHATKEY:96] 52 | 53 | Set this to 0 to disable chat names. 54 | [CHATNAMEKEY:126] 55 | 56 | Set this to 0 to disable requiring a name to chat. 57 | (If there is no key for setting a name, names will 58 | not be required.) 59 | [CHAT_NAME_REQUIRED:1] 60 | 61 | Display the cursor X as text? 62 | [CURSOR_IS_TEXT:1] 63 | 64 | Cap on the number of keys that can be on the stack. 65 | If this is exceeded, the stack will reset (user will be 66 | booted to the dwarf menu.) 67 | Set this to 0 to disable -- caveat emptor, clients could 68 | abuse this to slow down the server. 69 | [KEYSTACK_MAX:10000] -------------------------------------------------------------------------------- /dist/shared/hack/lua/plugins/dfplex.lua: -------------------------------------------------------------------------------- 1 | local _ENV = mkmodule('plugins.dfplex') 2 | 3 | --[[ 4 | 5 | Native functions: 6 | 7 | * get_client_count() 8 | * get_client_id_by_index(index) 9 | * get_client_nick(client-id) 10 | * get_current_menu_id() 11 | * get_client_cursorcoord(client-id) -> x, y, z 12 | * set_client_cursorcoord(client-id, x, y, z) 13 | * get_client_viewcoord(client-id) -> x, y, z 14 | * set_client_viewcoord(client-id, x, y, z) 15 | 16 | * register_cb_post_state_restore(callback) 17 | * callback(client-id) -> nil 18 | 19 | * lock_dfplex_mutex() 20 | * unlock_dfplex_mutex() 21 | 22 | --]] 23 | 24 | return _ENV -------------------------------------------------------------------------------- /dist/shared/hack/scripts/dfplex_restrict_z.lua: -------------------------------------------------------------------------------- 1 | local utils = require('utils') 2 | local repeatUtil = require('repeat-util') 3 | local dfplex = require('plugins.dfplex') 4 | 5 | print("Enabling restrict-z") 6 | 7 | -- https://stackoverflow.com/a/22831842 8 | function string.starts(String,Start) 9 | return string.sub(String,1,string.len(Start))==Start 10 | end 11 | 12 | local restrict_z = function(client) 13 | local flerb = dfplex.get_current_menu_id() 14 | 15 | -- some permitted menus 16 | if string.starts(flerb,"dwarfmode") ~= true then 17 | return 18 | end 19 | if string.starts(flerb,"dwarfmode/LookAround") then 20 | return 21 | end 22 | if string.starts(flerb,"dwarfmode/viewunit") then 23 | return 24 | end 25 | if string.starts(flerb,"dwarfmode/BuildingItems") then 26 | return 27 | end 28 | 29 | -- otherwise, force cursor coordinates if cursor is visible. 30 | local x, y, z = dfplex.get_client_cursorcoord(client) 31 | if x >= 0 and y >= 0 then 32 | -- x, y set, so cursor is visible. 33 | local z_required = 175 34 | if z ~= z_required then 35 | -- set cursor and view coordinate. 36 | dfplex.set_client_cursorcoord(client, x, y, z_required) 37 | local xv, yv, zv = dfplex.get_client_viewcoord(client) 38 | dfplex.set_client_viewcoord(client, xv, yv, z_required) 39 | end 40 | end 41 | end 42 | 43 | dfplex.register_cb_post_state_restore(restrict_z) -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Build script for windows, written in unix :P 3 | # This file is a part of Web Fortress 4 | # (c) 2014 Kyle Mclamb 5 | 6 | DF_VER="40.16" 7 | 8 | if [ ! -r "$1" ]; then 9 | echo "Invalid file: $1" 10 | echo "Usage: $0 " 11 | exit 1 12 | fi 13 | 14 | rm -rf package 15 | mkdir -v package 16 | mkdir -vp package/hack/plugins 17 | 18 | cp -v "$1" package/hack/plugins/ 19 | cp -vr dist/shared/* package/ 20 | cp -vr dist/$DF_VER/* package/ 21 | cp -vr static package/web 22 | 23 | cp_prefixed() { 24 | cp -v $1 package/WF-$1 25 | } 26 | 27 | cp_prefixed README.md 28 | cp_prefixed INSTALLING.txt 29 | cp_prefixed LICENSE 30 | echo "## CLIENT ##" >> package/WF-USING.txt 31 | cat static/README.md >> package/WF-USING.txt 32 | echo "" >> package/WF-USING.txt 33 | echo "## SERVER ##" >> package/WF-USING.txt 34 | cat server/README.md >> package/WF-USING.txt 35 | 36 | zipname="dfplex-$(git describe --tag)-df0.$DF_VER-win32.zip" 37 | 38 | rm -v "$zipname" 39 | (cd package && zip -r "../$zipname" ./*) 40 | 41 | rm -rf package 42 | echo "$zipname: Done." 43 | -------------------------------------------------------------------------------- /server/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | get_directory_property(isPlugin PARENT_DIRECTORY) 2 | 3 | project(dfplex) 4 | 5 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 6 | if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.9) 7 | option(DFPLEX_IXW "Use IXWebSocket instead of websocketpp" OFF) 8 | else() 9 | # GCC 4.9 is not compatible with IXWebSockets 10 | option(DFPLEX_IXW "Use IXWebSocket instead of websocketpp" OFF) 11 | endif() 12 | else() 13 | option(DFPLEX_IXW "Use IXWebSocket instead of websocketpp" OFF) 14 | endif() 15 | 16 | if (NOT isPlugin) 17 | cmake_minimum_required(VERSION 2.8) 18 | set(DFHACK_VER 0.34.11) 19 | set(DFHACK_REL r5) 20 | add_definitions(-DDFHACK_VERSION="${DFHACK_VER}-${DFHACK_REL}") 21 | # dfhack path 22 | set(DH dfhack) 23 | endif() 24 | 25 | # A list of source files 26 | file (GLOB SRC "*.cpp") 27 | 28 | # A list of headers 29 | file (GLOB HDRS "*.hpp") 30 | 31 | set_source_files_properties(${HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) 32 | 33 | # mash them together (headers are marked as headers and nothing will try to compile them) 34 | list(APPEND SRC ${HDRS}) 35 | 36 | set(PROJECT_LIBS) 37 | 38 | #linux 39 | if(UNIX) 40 | add_definitions( 41 | -std=c++11 42 | -DLINUX_BUILD 43 | ) 44 | if (NOT DFPLEX_IXW) 45 | add_definitions(-D_WEBSOCKETPP_CPP11_STL_) 46 | endif() 47 | list(APPEND PROJECT_LIBS pthread) 48 | set(DF_ROOT ${CMAKE_SOURCE_DIR}/df_linux) 49 | # windows 50 | else(UNIX) 51 | set(WFDIR ${CMAKE_CURRENT_BINARY_DIR}) 52 | add_definitions( 53 | -DWIN32_LEAN_AND_MEAN 54 | ) 55 | if (NOT DFPLEX_IXW) 56 | add_definitions( 57 | -D_WEBSOCKETPP_CPP11_FUNCTIONAL_ 58 | -D_WEBSOCKETPP_CPP11_SYSTEM_ERROR_ 59 | -D_WEBSOCKETPP_CPP11_RANDOM_DEVICE_ 60 | -D_WEBSOCKETPP_CPP11_MEMORY_ 61 | ) 62 | set(BOOST_ROOT ${WFDIR}/boost) 63 | link_directories(${BOOST_ROOT}/lib) # Boost doesn't find everything 64 | endif() 65 | endif(UNIX) 66 | 67 | if (NOT DFPLEX_IXW) 68 | # Boost/Asio 69 | set(Boost_FIND_REQUIRED TRUE) 70 | set(Boost_USE_STATIC_LIBS OFF) 71 | set(Boost_USE_SHARED_LIBS ON) 72 | set(Boost_USE_MULTITHREADED ON) 73 | set(WS_BOOST_LIBS system) 74 | find_package(Boost 1.38.0 REQUIRED COMPONENTS "${WS_BOOST_LIBS}") 75 | 76 | if(Boost_FOUND) 77 | include_directories(${Boost_INCLUDE_DIRS}) 78 | list(APPEND PROJECT_LIBS ${Boost_LIBRARIES}) 79 | endif() 80 | 81 | # websocketpp 82 | include_directories(websocketpp) 83 | 84 | add_definitions(-DDFPLEX_WEBSOCKETPP) 85 | else() 86 | # ixwebsocket 87 | include_directories(IXWebSocket) 88 | add_subdirectory(IXWebSocket) 89 | list(APPEND PROJECT_LIBS 90 | ixwebsocket 91 | ) 92 | add_definitions(-DDFPLEX_IXW) 93 | endif() 94 | 95 | # cpp-httplib 96 | include_directories(cpp-httplib) 97 | 98 | if (isPlugin) 99 | DFHACK_PLUGIN(dfplex ${SRC} LINK_LIBRARIES dfhack-tinythread ${PROJECT_LIBS} lua) 100 | target_link_libraries(dfplex ${Boost_LIBRARIES}) 101 | INSTALL(DIRECTORY ../dist/shared/ DESTINATION .) 102 | INSTALL(DIRECTORY ../static/ DESTINATION ./hack/www) 103 | else() 104 | if (NOT UNIX) 105 | message(FATAL_ERROR "Building dfplex out-of-tree not supported on windows.") 106 | endif() 107 | find_library(DFHACK_LIBRARY dfhack ${DH}/build/library) 108 | find_library(TINYTHREAD_LIBRARY dfhack-tinythread ${DH}/build/depends/tthread) 109 | list(APPEND PROJECT_LIBS 110 | ${DFHACK_LIBRARY} 111 | ${TINYTHREAD_LIBRARY} 112 | ) 113 | 114 | include_directories( 115 | ${DH}/library/include 116 | ${DH}/library/proto 117 | ${DH}/depends/protobuf 118 | ${DH}/depends/tthread 119 | ) 120 | 121 | add_library(dfplex.plug SHARED ${SRC}) 122 | target_link_libraries(dfplex.plug ${PROJECT_LIBS} lua) 123 | # Installing 124 | install(TARGETS dfplex.plug 125 | RUNTIME DESTINATION ${DF_ROOT}/hack/plugins 126 | LIBRARY DESTINATION ${DF_ROOT}/hack/plugins 127 | ) 128 | endif() 129 | -------------------------------------------------------------------------------- /server/Client.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "modules/Screen.h" 9 | #include "df/coord.h" 10 | #include "df/interface_key.h" 11 | #include "df/ui.h" 12 | #include "df/ui_unit_view_mode.h" 13 | 14 | #include "keymap.hpp" 15 | #include "hackutil.hpp" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | typedef int64_t client_long_id_t; 25 | 26 | struct ClientIdentity 27 | { 28 | std::string addr; 29 | std::string nick; 30 | uint8_t nick_colour = 0; 31 | bool is_admin = false; 32 | client_long_id_t long_id = 0; 33 | }; 34 | 35 | struct Client; 36 | 37 | typedef std::function restore_state_cb_t; 38 | 39 | // a command which is part of the algorithm for 40 | // restoring menu state. 41 | struct RestoreKey 42 | { 43 | std::set m_interface_keys; 44 | 45 | // should we check state after applying this key? 46 | // (should not be used with m_need_wait) 47 | bool m_check_state = false; 48 | size_t m_check_start; // if check fails, rewind back to here. 49 | bool m_catch = false; // this will intercept an escape. (requires m_check_start to be set.) 50 | 51 | // after applying the key, this should be the state of the menu. 52 | menu_id m_post_menu; 53 | size_t m_post_menu_depth; 54 | 55 | // this is the observed state of the menu after applying. 56 | bool m_catch_observed_autorewind = false; // check the observed state for this screen when comparing menu IDs on exit. 57 | menu_id m_observed_menu; 58 | size_t m_observed_menu_depth=0; 59 | bool m_blockcatch = false; // if the observed menu does not match the previous frames', error to check_start. 60 | 61 | // additional misc. properties ------------------------------------------ 62 | 63 | // these run when this command is applied, which could affect some state. 64 | // return non-zero value in case of error. 65 | std::vector m_callbacks; 66 | 67 | // as above, but these run after applying the key. 68 | std::vector m_callbacks_post; 69 | 70 | RestoreKey()=default; 71 | RestoreKey(df::interface_key key) 72 | : m_interface_keys{ key } 73 | { } 74 | RestoreKey(RestoreKey&&)=default; 75 | RestoreKey(const RestoreKey&)=default; 76 | RestoreKey& operator=(const RestoreKey&)=default; 77 | RestoreKey& operator=(RestoreKey&&)=default; 78 | }; 79 | 80 | // information to be fed to df to restore ui per-client. 81 | struct UIState 82 | { 83 | // pressing these keys from dwarfmode/Default will restore the UI state of the user. 84 | std::vector m_restore_keys; 85 | static const size_t K_RESTORE_PROGRESS_ROOT_MAX = 15; 86 | size_t m_restore_progress_root = 0; // how long have we tried returning to the root? 87 | size_t m_restore_progress = 0; // how far into m_restore_keys we've gotten. 88 | 89 | bool m_suppress_sidebar_refresh = false; // suppress the sidebar refresh at the end of this 90 | bool m_freeze_cursor = false; 91 | 92 | // pause state 93 | // 0: not paused 94 | // 1: paused due to being in a menu 95 | bool m_pause_required = false; 96 | 97 | bool m_viewcoord_set = false; 98 | Coord m_viewcoord; 99 | 100 | // for switching to with the [ ] keys. 101 | bool m_stored_viewcoord_skip = false; 102 | bool m_stored_camera_return = false; // return after an event is set; remove this eventually...? 103 | Coord m_stored_viewcoord; 104 | bool m_following_client = false; 105 | std::shared_ptr m_client_screen_cycle; 106 | 107 | // view dimensions 108 | int32_t m_map_dimx=-1, m_map_dimy=-1; 109 | 110 | bool m_cursorcoord_set = false; 111 | Coord m_cursorcoord; 112 | 113 | // Build menu cursor position 114 | // if false, do not restore the build menu cursor position 115 | bool m_buildcoord_set = false; 116 | Coord m_buildcoord; // stores the last position of the cursor from the build menu, if any 117 | 118 | bool m_designationcoord_share = false; 119 | bool m_designationcoord_set = false; 120 | Coord m_designationcoord; 121 | 122 | bool m_burrowcoord_share = false; 123 | bool m_burrowcoord_set = false; 124 | Coord m_burrowcoord; 125 | 126 | bool m_squadcoord_share = false; 127 | bool m_squadcoord_start_set = false; 128 | Coord m_squadcoord_start; 129 | 130 | // pair of colour and offset from cursorcoord 131 | std::vector> m_construction_plan; 132 | 133 | // pressing tab from the root menu changes these. 134 | uint8_t m_menu_width = 1, m_area_map_width = 2; 135 | 136 | // designation state (in vanilla, chop is default.) 137 | df::interface_key m_designate_mode = df::interface_key::DESIGNATE_CHOP; 138 | bool m_designate_marker = false, m_designate_priority_set = false; 139 | int32_t m_designate_priority = 4; 140 | // TODO: capture designate mine mode? auto-mine mode may be problematic... 141 | 142 | // stockpile state 143 | df::interface_key m_stockpile_mode = df::interface_key::STOCKPILE_ANIMAL; 144 | bool m_custom_stockpile_set; 145 | df::stockpile_settings m_custom_stockpile; 146 | 147 | // unit view mode 148 | df::ui_unit_view_mode::T_value m_unit_view_mode = df::ui_unit_view_mode::General; 149 | bool m_show_combat = true; 150 | bool m_show_labor = true; 151 | bool m_show_misc = true; 152 | int32_t m_view_unit = -1; 153 | int32_t m_view_unit_labor_scroll = 0; 154 | int32_t m_view_unit_labor_submenu = -1; 155 | bool m_defer_restore_cursor = false; 156 | int32_t m_viewcycle = 0; 157 | 158 | // burrow state 159 | bool m_brush_erasing = false; 160 | 161 | // civlist screen 162 | int32_t m_civ_x = -1, m_civ_y = -1; 163 | 164 | // stabilizes list-menus (e.g announcements, reports) 165 | // one per viewscreen on the stack. 166 | std::vector m_list_cursor; 167 | 168 | // resize state 169 | bool m_building_in_resize = false; 170 | int32_t m_building_resize_radius = 4; 171 | 172 | // squad state 173 | struct { 174 | bool in_select_indiv; 175 | int32_t sel_indiv_squad; // refers-to squad_id 176 | std::vector indiv_selected; // refers-to historical figures 177 | std::vector squad_selected; // refers-to squad id 178 | std::vector kill_selected; // refers-to unit id 179 | } m_squads; 180 | 181 | std::string debug_trace() const; 182 | 183 | // dfplex-specific UI information 184 | std::string m_dfplex_chat_message; 185 | bool m_dfplex_chat_entering = false; 186 | bool m_dfplex_chat_config = false; 187 | bool m_dfplex_chat_name_entering = false; 188 | bool m_dfplex_hide_chat = false; 189 | 190 | int32_t m_follow_unit_id = -1; 191 | int32_t m_follow_item_id = -1; 192 | 193 | size_t m_mission_report_ticks = 0; 194 | bool m_mission_report_paused = false; 195 | 196 | // resets most UI state 197 | void reset() 198 | { 199 | m_mission_report_paused = false; 200 | m_mission_report_ticks = 0; 201 | m_follow_unit_id = -1; 202 | m_follow_item_id = -1; 203 | m_restore_keys.clear(); 204 | m_restore_progress_root = 0; 205 | m_restore_progress = 0; 206 | m_pause_required = false; 207 | m_brush_erasing = false; 208 | m_viewcoord_set = false; 209 | m_cursorcoord_set = false; 210 | m_burrowcoord_share = false; 211 | m_burrowcoord_set = false; 212 | m_designationcoord_set = false; 213 | m_designationcoord_share = false; 214 | m_suppress_sidebar_refresh = false; 215 | m_building_in_resize = false; 216 | m_building_resize_radius = 4; 217 | m_stored_camera_return = false; 218 | m_stored_viewcoord_skip = false; 219 | m_following_client = false; 220 | m_construction_plan.clear(); 221 | m_unit_view_mode = df::ui_unit_view_mode::General; 222 | m_show_combat = true; 223 | m_show_labor = true; 224 | m_show_misc = true; 225 | m_defer_restore_cursor = false; 226 | m_view_unit = -1; 227 | m_view_unit_labor_scroll = 0; 228 | m_view_unit_labor_submenu = -1; 229 | m_freeze_cursor = false; 230 | m_viewcycle = 0; 231 | m_list_cursor.clear(); 232 | m_civ_x = -1; 233 | m_civ_y = -1; 234 | m_dfplex_chat_entering = false; 235 | m_dfplex_chat_message = ""; 236 | m_dfplex_chat_config = false; 237 | m_dfplex_chat_name_entering = false; 238 | m_map_dimx = m_map_dimy = -1; 239 | m_custom_stockpile_set = false; 240 | } 241 | 242 | // makes the UI ready to handle a new plex re-entry. 243 | void next() 244 | { 245 | m_restore_progress_root = 0; 246 | m_restore_progress = 0; 247 | m_suppress_sidebar_refresh = false; 248 | } 249 | }; 250 | 251 | // FIXME -- someone who understands the pen class can 252 | // double check this implementation. 253 | inline bool operator==(const DFHack::Screen::Pen& a, const DFHack::Screen::Pen& b) 254 | { 255 | if (a.tile_mode != b.tile_mode) 256 | { 257 | return false; 258 | } 259 | if (a.tile != b.tile) 260 | { 261 | return false; 262 | } 263 | if (a.bold != b.bold) return false; 264 | if (!a.tile) 265 | { 266 | if (a.ch != b.ch) return false; 267 | if (a.fg != b.fg) return false; 268 | if (a.bg != b.bg) return false; 269 | } 270 | else 271 | { 272 | switch (a.tile_mode) 273 | { 274 | case DFHack::Screen::Pen::AsIs: 275 | break; 276 | case DFHack::Screen::Pen::CharColor: 277 | if (a.fg != b.fg) return false; 278 | if (a.bg != b.bg) return false; 279 | break; 280 | case DFHack::Screen::Pen::TileColor: 281 | if (a.tile_fg != b.tile_fg) return false; 282 | if (a.tile_bg != b.tile_bg) return false; 283 | break; 284 | } 285 | } 286 | return true; 287 | } 288 | 289 | // a tile on the screen, with some extra information 290 | struct ClientTile 291 | { 292 | DFHack::Screen::Pen pen; 293 | bool modified; 294 | bool is_text; 295 | bool is_overworld; 296 | bool is_map; 297 | 298 | bool operator==(const ClientTile& other) const 299 | { 300 | // ignore modified 301 | return pen == other.pen && is_text == other.is_text; 302 | } 303 | 304 | bool operator!=(const ClientTile& other) const 305 | { 306 | return ! (*this == other); 307 | } 308 | }; 309 | 310 | // array of all tiles on the screen 311 | typedef ClientTile screenbuf_t[256 * 256]; 312 | 313 | struct Client; 314 | struct ClientUpdateInfo 315 | { 316 | bool is_multiplex; 317 | bool on_destroy; 318 | }; 319 | typedef std::function client_update_cb; 320 | 321 | struct Client { 322 | std::shared_ptr id{ new ClientIdentity() }; 323 | 324 | // Called once per update. 325 | // 326 | // If multiplexing, update occurs after state restore but 327 | // before post-state capture (and before screen capture). 328 | // 329 | // Should populate Client::keyqueue. For example: 330 | // client->keyqueue.emplace(df::enums::interface_key::D_DESIGNATE); 331 | // 332 | // also called if the client is deleted (on_destroy will be true.) 333 | client_update_cb update_cb; 334 | 335 | std::string info_message; // this string is displayed to the user. 336 | 337 | std::string m_debug_info = ""; // debug info sent to user. 338 | bool m_debug_enabled = false; 339 | 340 | // client's screen 341 | screenbuf_t sc; 342 | uint8_t dimx=0, dimy=0; 343 | uint8_t desired_dimx=80, desired_dimy=25; 344 | 345 | // pending keypresses from client. 346 | std::queue keyqueue; 347 | 348 | UIState ui; 349 | }; 350 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | The Webfort plugin has no defined configuration file. Instead, it uses 5 | environment variables. The variables it looks for are: 6 | 7 | | var | description | 8 | |------------------|--------------------------------------------------------------------------------------| 9 | | `WF_PORT` | The port number webfort listens on. default: 1234 | 10 | | `WF_MAX_CLIENTS` | The number of connections that can be opened at any one time. default: 0 (meaning unbounded). | 11 | -------------------------------------------------------------------------------- /server/callbacks.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #include "callbacks.hpp" 7 | #include "dfplex.hpp" 8 | 9 | namespace DFPlex 10 | { 11 | 12 | std::vector> g_cb_post_state_restore; 13 | std::vector> g_cb_shutdown; 14 | 15 | void add_cb_post_state_restore(std::function&& fn) 16 | { 17 | g_cb_post_state_restore.emplace_back(std::move(fn)); 18 | } 19 | 20 | void run_cb_post_state_restore(Client* cl) 21 | { 22 | if (g_cb_post_state_restore.size()) 23 | { 24 | for (const auto& fn : g_cb_post_state_restore) 25 | { 26 | fn(cl); 27 | } 28 | } 29 | } 30 | 31 | void add_cb_shutdown(std::function&& fn) 32 | { 33 | g_cb_shutdown.emplace_back(std::move(fn)); 34 | } 35 | 36 | void run_cb_shutdown() 37 | { 38 | if (g_cb_shutdown.size()) 39 | { 40 | for (const auto& fn : g_cb_shutdown) 41 | { 42 | fn(); 43 | } 44 | } 45 | } 46 | 47 | void cleanup_callbacks() 48 | { 49 | g_cb_post_state_restore.clear(); 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /server/callbacks.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "Client.hpp" 9 | #include 10 | 11 | namespace DFPlex 12 | { 13 | // triggered after a state restore. 14 | void add_cb_post_state_restore(std::function&&); 15 | void run_cb_post_state_restore(Client*); 16 | 17 | // triggered during shutdown of the plugin. 18 | void add_cb_shutdown(std::function&&); 19 | void run_cb_shutdown(); 20 | 21 | // removes all callbacks. 22 | void cleanup_callbacks(); 23 | } -------------------------------------------------------------------------------- /server/chat.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #include "chat.hpp" 8 | #include "config.hpp" 9 | 10 | // we need this for get_client() 11 | #include "dfplex.hpp" 12 | 13 | bool ChatMessage::is_expired(Client* client) const 14 | { 15 | if (!m_time_remaining.get()) return true; 16 | 17 | auto iter = m_time_remaining->find(client->id); 18 | if (iter == m_time_remaining->end()) return true; 19 | 20 | return iter->second <= 0; 21 | } 22 | 23 | bool ChatMessage::is_flash(Client* client) const 24 | { 25 | if (!m_time_remaining.get()) return false; 26 | 27 | auto iter = m_time_remaining->find(client->id); 28 | if (iter == m_time_remaining->end()) return false; 29 | 30 | return iter->second >= MESSAGE_TIME - MESSAGE_FLASH_TIME || iter->second < MESSAGE_FLASH_TIME; 31 | } 32 | 33 | void ChatMessage::expire(Client* client) 34 | { 35 | if (!m_time_remaining) return; 36 | 37 | auto iter = m_time_remaining->find(client->id); 38 | if (iter != m_time_remaining->end()) 39 | { 40 | m_time_remaining->erase(iter); 41 | } 42 | } 43 | 44 | void ChatLog::push_message(ChatMessage&& message) 45 | { 46 | // set time remaining for all existing clients. 47 | message.m_time_remaining.reset( 48 | new std::map, int32_t>() 49 | ); 50 | 51 | for (size_t i = 0; i < get_client_count(); ++i) 52 | { 53 | (*message.m_time_remaining)[get_client(i)->id] = 54 | MESSAGE_TIME; 55 | } 56 | 57 | // add message to log. 58 | m_messages.emplace_back( 59 | std::move(message) 60 | ); 61 | 62 | // erase messages in big chunks when max size reached. 63 | while (m_messages.size() >= MAX_MESSAGE_COUNT) 64 | { 65 | size_t erase_count = m_messages.size() / 8; 66 | m_active_message_index -= erase_count; 67 | m_messages.erase(m_messages.begin(), m_messages.begin() + erase_count); 68 | } 69 | } 70 | 71 | void ChatLog::tick(Client* client) 72 | { 73 | // tick each message's timer per-client. 74 | for (size_t i = m_active_message_index; i < m_messages.size(); ++i) 75 | { 76 | ChatMessage& message = m_messages.at(i); 77 | if (message.m_time_remaining) 78 | { 79 | auto iter = message.m_time_remaining->find(client->id); 80 | if (iter == message.m_time_remaining->end()) continue; 81 | if (iter->second-- <= 0) 82 | { 83 | message.m_time_remaining->erase(iter); 84 | } 85 | } 86 | } 87 | 88 | // update m_active_message_index, cleaning up maps. 89 | while ( 90 | m_active_message_index < m_messages.size() 91 | && (!m_messages.at(m_active_message_index).m_time_remaining || 92 | (m_messages.at(m_active_message_index).m_time_remaining 93 | && m_messages.at(m_active_message_index).m_time_remaining->empty())) 94 | ) 95 | { 96 | m_messages.at(m_active_message_index).m_time_remaining.reset(); 97 | ++m_active_message_index; 98 | } 99 | } -------------------------------------------------------------------------------- /server/chat.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "Client.hpp" 10 | #include 11 | #include 12 | #include 13 | 14 | struct ChatMessage 15 | { 16 | std::string m_contents; 17 | 18 | std::shared_ptr m_sender; 19 | 20 | // time remaining on this message per-client; 21 | std::unique_ptr, int32_t>> m_time_remaining; 22 | 23 | void expire(Client*); 24 | bool is_expired(Client*) const; 25 | bool is_flash(Client*) const; 26 | }; 27 | 28 | class ChatLog 29 | { 30 | public: 31 | std::vector m_messages; 32 | size_t m_active_message_index = 0; 33 | 34 | // (virtual methods are not linked -- so this can be called from other plugins.) 35 | virtual void push_message(ChatMessage&&); 36 | void tick(Client*); 37 | }; -------------------------------------------------------------------------------- /server/command.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "Client.hpp" 9 | #include "df/interface_key.h" 10 | 11 | // call once at start. 12 | // error -> returns true 13 | bool command_init(); 14 | 15 | // analyze and possibly apply the given command from the user. 16 | // -- parameters -- 17 | // keys: the command to be applied; as an optimization, 18 | // this is passed as a non-const reference and will be emptied. 19 | // cl: the client applying the command 20 | // raw: skip analysis (except for a permissions check) 21 | 22 | void apply_command(std::set&, Client* cl, bool raw); 23 | 24 | // permits a deferred rewind later. 25 | void rewind_keyqueue_to_catch(Client* client); -------------------------------------------------------------------------------- /server/config.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #include "config.hpp" 7 | #include "hackutil.hpp" 8 | #include "Core.h" 9 | 10 | bool AUTOSAVE_WHILE_IDLE = 0; 11 | uint32_t MAX_CLIENTS = 0; 12 | uint16_t PORT = 1234; 13 | uint16_t STATICPORT = 8000; 14 | std::string STATICDIR = "hack/www"; 15 | uint32_t MULTIPLEXKEY = 0; 16 | uint32_t CHATKEY = 0; 17 | uint32_t CHAT_NAME_KEY = 0; 18 | bool CHAT_NAME_REQUIRED = 1; 19 | uint32_t NEXT_CLIENT_POS_KEY = 0; 20 | uint32_t PREV_CLIENT_POS_KEY = 0; 21 | bool CURSOR_IS_TEXT = false; 22 | PauseBehaviour PAUSE_BEHAVIOUR = PauseBehaviour::EXPLICIT; 23 | uint32_t DEBUGKEY = 0; 24 | uint32_t SERVERDEBUGKEY = 0; 25 | uint16_t MESSAGE_TIME = 60 * 18; 26 | uint16_t MESSAGE_FLASH_TIME = 10; 27 | size_t MAX_MESSAGE_COUNT = 0x10000; 28 | bool CHAT_ENABLED = true; 29 | bool MULTISIZE = true; 30 | size_t KEYSTACK_MAX = 10000; 31 | std::string SECRET = ""; // auth is disabled by default 32 | std::vector g_ban_list; 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | using namespace std; // iostream heavy 39 | 40 | vector split(const char *str, char c) 41 | { 42 | vector result; 43 | do { 44 | const char *begin = str; 45 | 46 | while(*str != c && *str) 47 | str++; 48 | 49 | result.push_back(string(begin, str)); 50 | } while (0 != *str++); 51 | 52 | return result; 53 | } 54 | 55 | bool load_bans() 56 | { 57 | g_ban_list.clear(); 58 | ifstream f("data/init/bans.txt"); 59 | if (!f.is_open()) { 60 | static bool already_displayed_error = false; 61 | if (!already_displayed_error) 62 | { 63 | already_displayed_error = true; 64 | DFHack::Core::printerr("DFPlex failed to open bans list file; skipping.\n"); 65 | DFHack::Core::printerr("(Expected config file data/init/bans.txt)\n"); 66 | } 67 | return false; 68 | } 69 | 70 | string line; 71 | while(getline(f, line)) { 72 | size_t comment = line.find("#"); 73 | if (comment != std::string::npos) 74 | { 75 | line = line.substr(0, comment); 76 | } 77 | 78 | // trim 79 | while (startsWith(line, " ")) line = line.substr(1); 80 | while (endsWith(line, " ")) line = line.substr(0, line.length() - 1); 81 | 82 | if (line.length()) 83 | { 84 | g_ban_list.push_back(line); 85 | } 86 | } 87 | 88 | return true; 89 | } 90 | 91 | bool load_text_file() 92 | { 93 | ifstream f("data/init/dfplex.txt"); 94 | if (!f.is_open()) { 95 | DFHack::Core::printerr("DFPlex failed to open config file; skipping.\n"); 96 | DFHack::Core::printerr("(Expected config file data/init/dfplex.txt)\n"); 97 | return false; 98 | } 99 | 100 | string line; 101 | while(getline(f, line)) { 102 | size_t b = line.find("["); 103 | size_t e = line.rfind("]"); 104 | 105 | if (b == string::npos || e == string::npos || line.find_first_not_of(" ") < b) 106 | continue; 107 | 108 | line = line.substr(b+1, e-1); 109 | vector tokens = split(line.c_str(), ':'); 110 | const string& key = tokens[0]; 111 | const string& val = tokens[1]; 112 | 113 | if (key == "PORT") { 114 | PORT = (uint16_t)std::stol(val); 115 | } 116 | if (key == "STATICPORT") { 117 | STATICPORT = (uint16_t)std::stol(val); 118 | } 119 | if (key == "MAX_CLIENTS") { 120 | MAX_CLIENTS = (uint32_t)std::stol(val); 121 | } 122 | if (key == "PAUSE_BEHAVIOUR" || key == "PAUSE") 123 | { 124 | if (val == "ALWAYS") 125 | { 126 | PAUSE_BEHAVIOUR = PauseBehaviour::ALWAYS; 127 | } 128 | else if (val == "EXPLICIT") 129 | { 130 | PAUSE_BEHAVIOUR = PauseBehaviour::EXPLICIT; 131 | } 132 | else if (val == "DWARFMENU" || val == "EXPLICIT_DWARFMENU") 133 | { 134 | PAUSE_BEHAVIOUR = PauseBehaviour::EXPLICIT_DWARFMENU; 135 | } 136 | else if (val == "ANYMENU" || val == "EXPLICIT_ANYMENU") 137 | { 138 | PAUSE_BEHAVIOUR = PauseBehaviour::EXPLICIT_ANYMENU; 139 | } 140 | /*else if (val == "NEVER") 141 | { 142 | PAUSE_BEHAVIOUR = PauseBehaviour::NEVER; 143 | }*/ 144 | else 145 | { 146 | DFHack::Core::printerr("Pause behaviour not recognized.\n"); 147 | return false; 148 | } 149 | } 150 | if (key == "CURSOR_IS_TEXT") 151 | { 152 | CURSOR_IS_TEXT = std::stol(val); 153 | } 154 | if (key == "PREV_CLIENT_POS_KEY") 155 | { 156 | PREV_CLIENT_POS_KEY = std::stol(val); 157 | } 158 | if (key == "NEXT_CLIENT_POS_KEY") 159 | { 160 | NEXT_CLIENT_POS_KEY = std::stol(val); 161 | } 162 | if (key == "DEBUGKEY" || key == "DEBUG_KEY") 163 | { 164 | DEBUGKEY = std::stol(val); 165 | } 166 | if (key == "SERVERDEBUGKEY" || key == "SERVER_DEBUG_KEY") 167 | { 168 | SERVERDEBUGKEY = std::stol(val); 169 | } 170 | if (key == "MULTIPLEXKEY" || key == "MULTIPLEX_KEY") { 171 | MULTIPLEXKEY = std::stol(val); 172 | } 173 | if (key == "CHATKEY" || key == "CHAT_KEY") { 174 | CHATKEY = std::stol(val); 175 | CHAT_ENABLED = !!CHATKEY; 176 | } 177 | if (key == "CHAT_NAME_KEY" || key == "CHATNAMEKEY") { 178 | CHAT_NAME_KEY = std::stol(val); 179 | } 180 | if (key == "CHAT_NAME_REQUIRED") { 181 | CHAT_NAME_REQUIRED = !!std::stol(val); 182 | } 183 | if (key == "KEYSTACK_MAX") { 184 | KEYSTACK_MAX = std::stol(val); 185 | } 186 | } 187 | 188 | // conditionals 189 | if (!CHAT_NAME_KEY) CHAT_NAME_REQUIRED = false; 190 | if (!CHAT_NAME_KEY || !CHATKEY) CHAT_ENABLED = false; 191 | 192 | return true; 193 | } 194 | 195 | bool load_env_vars() 196 | { 197 | char* tmp; 198 | if ((tmp = getenv("DFPLEX_PORT"))) { 199 | PORT = (uint16_t)std::stol(tmp); 200 | } 201 | if ((tmp = getenv("DFPLEX_STATICPORT"))) { 202 | STATICPORT = (uint16_t)std::stol(tmp); 203 | } 204 | if ((tmp = getenv("DFPLEX_MAX_CLIENTS"))) { 205 | MAX_CLIENTS = (uint32_t)std::stol(tmp); 206 | } 207 | if ((tmp = getenv("DFPLEX_SECRET"))) { 208 | SECRET = tmp; 209 | } 210 | if ((tmp = getenv("DFPLEX_STATICDIR"))) { 211 | STATICDIR = tmp; 212 | } 213 | return true; 214 | } 215 | 216 | bool load_config() 217 | { 218 | return load_text_file() && load_env_vars(); 219 | } 220 | -------------------------------------------------------------------------------- /server/config.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | enum class PauseBehaviour { 14 | ALWAYS, 15 | EXPLICIT, 16 | EXPLICIT_DWARFMENU, 17 | EXPLICIT_ANYMENU, 18 | //NEVER 19 | }; 20 | 21 | extern bool AUTOSAVE_WHILE_IDLE; 22 | extern uint32_t MAX_CLIENTS; 23 | extern uint16_t PORT; 24 | extern uint16_t STATICPORT; 25 | extern std::string STATICDIR; 26 | extern uint32_t MULTIPLEXKEY; 27 | extern uint32_t DEBUGKEY; 28 | extern uint32_t SERVERDEBUGKEY; 29 | extern uint32_t NEXT_CLIENT_POS_KEY; 30 | extern uint32_t PREV_CLIENT_POS_KEY; 31 | extern bool CURSOR_IS_TEXT; 32 | extern std::string SECRET; 33 | extern PauseBehaviour PAUSE_BEHAVIOUR; 34 | extern uint32_t CHATKEY; 35 | extern uint32_t CHAT_NAME_KEY; 36 | extern bool CHAT_NAME_REQUIRED; 37 | extern bool CHAT_ENABLED; 38 | extern bool MULTISIZE; 39 | extern uint16_t MESSAGE_TIME; 40 | extern uint16_t MESSAGE_FLASH_TIME; 41 | extern size_t MAX_MESSAGE_COUNT; 42 | extern std::vector g_ban_list; 43 | extern size_t KEYSTACK_MAX; 44 | 45 | #define WF_VERSION "DFPlex-v0.2" 46 | #define WF_INVALID "DFPlex-invalid" 47 | 48 | #define CHAT_WIDTH 22 49 | #define CHAT_HEIGHT 13 50 | #define CHAT_MESSAGE_LINES 5 51 | 52 | bool load_config(); 53 | bool load_bans(); 54 | std::vector split(const char *str, char c = ' '); -------------------------------------------------------------------------------- /server/dfplex.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #pragma once 8 | 9 | // this is a hacky fix for a windows build error. 10 | #include "modules/EventManager.h" 11 | 12 | #include "Client.hpp" 13 | #include "ColorText.h" 14 | #include "chat.hpp" 15 | #include 16 | 17 | extern tthread::mutex dfplex_mutex; 18 | extern bool global_pause; 19 | extern bool plexing; 20 | extern int32_t frames_elapsed; 21 | 22 | extern ChatLog g_chatlog; 23 | 24 | bool is_paused(); 25 | 26 | // please make sure that dfplex_mutex is locked before calling, regardless 27 | // of callee thread. 28 | size_t get_client_count(); 29 | Client* get_client_by_id(client_long_id_t id); // retrieves client by unique number assigned to each client. 30 | Client* get_client(int32_t n); // retrieves nth client. 31 | Client* get_client(const ClientIdentity*); // retrieves client by identity 32 | int get_client_index(const ClientIdentity*); // retrieves client index by identity (or -1 if not found) 33 | 34 | // creates a new client 35 | Client* add_client(); 36 | 37 | // creates a new client and sets its callback function. 38 | // (see Client.hpp documentation on Client::update_cb) 39 | Client* add_client(client_update_cb&&); 40 | 41 | void remove_client(Client*); -------------------------------------------------------------------------------- /server/hackutil.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #include "hackutil.hpp" 8 | #include "dfplex.hpp" 9 | 10 | #include "df/building_trapst.h" 11 | #include "df/global_objects.h" 12 | #include "df/graphic.h" 13 | #include "df/historical_figure.h" 14 | #include "df/invasion_info.h" 15 | #include "df/item.h" 16 | #include "df/mission_report.h" 17 | #include "df/squad.h" 18 | #include "df/ui_sidebar_menus.h" 19 | #include "df/ui_sidebar_mode.h" 20 | #include "df/ui.h" 21 | #include "df/unit.h" 22 | #include "df/viewscreen_choose_start_sitest.h" 23 | #include "df/viewscreen_civlistst.h" 24 | #include "df/viewscreen_createquotast.h" 25 | #include "df/viewscreen_customize_unitst.h" 26 | #include "df/viewscreen_dungeonmodest.h" 27 | #include "df/viewscreen_dwarfmodest.h" 28 | #include "df/viewscreen_itemst.h" 29 | #include "df/viewscreen_layer_export_play_mapst.h" 30 | #include "df/viewscreen_layer_militaryst.h" 31 | #include "df/viewscreen_layer_squad_schedulest.h" 32 | #include "df/viewscreen_loadgamest.h" 33 | #include "df/viewscreen_meetingst.h" 34 | #include "df/viewscreen_movieplayerst.h" 35 | #include "df/viewscreen_new_regionst.h" 36 | #include "df/viewscreen_optionst.h" 37 | #include "df/viewscreen_overallstatusst.h" 38 | #include "df/viewscreen_reportlistst.h" 39 | #include "df/viewscreen_setupadventurest.h" 40 | #include "df/viewscreen_textviewerst.h" 41 | #include "df/viewscreen_titlest.h" 42 | #include "df/viewscreen_unitst.h" 43 | #include "df/viewscreen.h" 44 | #include "df/world.h" 45 | #include "modules/Gui.h" 46 | #include "modules/Screen.h" 47 | 48 | using namespace df; 49 | using namespace df::global; 50 | using namespace DFHack; 51 | 52 | #define IS_SCREEN(_sc) (id == &df::_sc::_identity) 53 | 54 | void show_announcement(std::string announcement) 55 | { 56 | DFHack::Gui::showAnnouncement(announcement); 57 | } 58 | 59 | bool is_at_root() 60 | { 61 | df::viewscreen* vs; 62 | df::virtual_identity* id; 63 | UPDATE_VS(vs, id); 64 | return (id == &df::viewscreen_dwarfmodest::_identity) 65 | && df::global::ui->main.mode == df::ui_sidebar_mode::Default; 66 | } 67 | 68 | bool is_realtime_dwarf_menu() 69 | { 70 | df::viewscreen* vs; 71 | df::virtual_identity* id; 72 | UPDATE_VS(vs, id); 73 | return (id == &df::viewscreen_dwarfmodest::_identity) 74 | && (df::global::ui->main.mode == df::ui_sidebar_mode::Default 75 | ||df::global::ui->main.mode == df::ui_sidebar_mode::Squads); 76 | } 77 | 78 | // force: exit even if that would be a state-changing action. 79 | static void apply_return_for(df::viewscreen* vs, bool force) 80 | { 81 | virtual_identity* id = df::virtual_identity::get(vs); 82 | 83 | // some screens cannot be escaped from without forcing. 84 | if (!force) 85 | { 86 | if (id == &df::viewscreen_meetingst::_identity) 87 | { 88 | return; 89 | } 90 | } 91 | 92 | // some screens require a bit of work to escape from. 93 | if (id == &df::viewscreen_layer_squad_schedulest::_identity) 94 | { 95 | df::viewscreen_layer_squad_schedulest* vs_schedule = static_cast(vs); 96 | if (vs_schedule->in_name_cell) 97 | { 98 | vs->feed_key(df::interface_key::SELECT); 99 | } 100 | } 101 | else if (id == &df::viewscreen_layer_militaryst::_identity) 102 | { 103 | df::viewscreen_layer_militaryst* vs_military = static_cast(vs); 104 | if (vs_military->page == df::viewscreen_layer_militaryst::Alerts) 105 | { 106 | if (vs_military->in_rename_alert) 107 | { 108 | vs->feed_key(df::interface_key::SELECT); 109 | } 110 | } 111 | else if (vs_military->page == df::viewscreen_layer_militaryst::Uniforms) 112 | { 113 | if (vs_military->equip.in_name_uniform) 114 | { 115 | vs->feed_key(df::interface_key::SELECT); 116 | } 117 | } 118 | else if (renaming_squad_id() >= 0) 119 | { 120 | vs->feed_key(df::interface_key::SELECT); 121 | } 122 | } 123 | vs->feed_key(df::interface_key::LEAVESCREEN); 124 | } 125 | 126 | // returns false on error. 127 | bool return_to_root() 128 | { 129 | df::viewscreen* vs; 130 | virtual_identity* id; 131 | UPDATE_VS(vs, id); 132 | if (is_at_root()) return true; 133 | 134 | // can't leave the meeting-topic screen. 135 | // (it would crash the game -- why..?) 136 | if (id == &df::viewscreen_meetingst::_identity) 137 | { 138 | return false; 139 | } 140 | 141 | Gui::resetDwarfmodeView(); 142 | for (size_t i = 0; i < 300; ++i) 143 | { 144 | if (is_at_root()) break; 145 | //UPDATE_VS(vs, id); 146 | //vs->feed_key(df::interface_key::LEAVESCREEN_ALL); // a segfault once occurred on this line. 147 | UPDATE_VS(vs, id); 148 | apply_return_for(vs, true); 149 | if (Screen::isDismissed(vs)) 150 | { 151 | // let's just force-kill the screen. 152 | remove_screen(vs); 153 | } 154 | } 155 | 156 | return is_at_root(); 157 | } 158 | 159 | bool defer_return_to_root() 160 | { 161 | if (is_at_root()) return true; 162 | 163 | for ( 164 | df::viewscreen* vs = DFHack::Gui::getCurViewscreen(true); 165 | vs && vs->parent && vs->parent->parent; // stop at dwarfmode screen 166 | vs = vs->parent 167 | ) 168 | { 169 | apply_return_for(vs, false); 170 | if (!Screen::isDismissed(vs)) 171 | { 172 | // failed to leave this screen. 173 | return false; 174 | } 175 | } 176 | 177 | Gui::resetDwarfmodeView(); 178 | 179 | return true; 180 | } 181 | 182 | size_t get_vs_depth(df::viewscreen* vs) 183 | { 184 | size_t i = 0; 185 | size_t K_MAX_DEPTH = 1024; 186 | 187 | while (vs && i < K_MAX_DEPTH) 188 | { 189 | vs = vs->parent; 190 | if (vs && vs->breakdown_level == df::enums::interface_breakdown_types::NONE) 191 | { 192 | ++i; 193 | } 194 | } 195 | 196 | return i; 197 | } 198 | 199 | bool is_text_tile(int x, int y, bool &is_map, bool& is_minimap) 200 | { 201 | df::viewscreen* ws = Gui::getCurViewscreen(true); 202 | virtual_identity* id = virtual_identity::get(ws); 203 | assert(ws != NULL); 204 | 205 | int32_t w = gps->dimx, h = gps->dimy; 206 | 207 | is_map = false; 208 | is_minimap = false; 209 | 210 | // screen border 211 | if (is_dwarf_mode()) 212 | { 213 | if (!x || !y || x == w - 1 || y == h - 1) 214 | return true; 215 | } 216 | 217 | if (IS_SCREEN(viewscreen_dwarfmodest)) 218 | { 219 | uint8_t menu_width, area_map_width; 220 | Gui::getMenuWidth(menu_width, area_map_width); 221 | int32_t menu_left = w - 1, menu_right = w - 1; 222 | 223 | bool menuforced = (df::global::ui->main.mode != df::ui_sidebar_mode::Default || df::global::cursor->x != -30000); 224 | 225 | if ((menuforced || menu_width == 1) && area_map_width == 2) // Menu + area map 226 | { 227 | menu_left = w - 56; 228 | menu_right = w - 25; 229 | } 230 | else if (menu_width == 2 && area_map_width == 2) // Area map only 231 | { 232 | menu_left = menu_right = w - 25; 233 | } 234 | else if (menu_width == 1) // Wide menu 235 | menu_left = w - 56; 236 | else if (menuforced || (menu_width == 2 && area_map_width == 3)) // Menu only 237 | menu_left = w - 32; 238 | 239 | if (x >= menu_left && x <= menu_right) 240 | { 241 | if (menuforced && df::global::ui->main.mode == df::ui_sidebar_mode::Burrows && df::global::ui->burrows.in_define_mode) 242 | { 243 | // Make burrow symbols use graphics font 244 | if ((y != 12 && y != 13 && !(x == menu_left + 2 && y == 2)) || x == menu_left || x == menu_right) 245 | return true; 246 | } 247 | else 248 | return true; 249 | } 250 | 251 | is_map = (x > 0 && x < menu_left); 252 | is_minimap = x > menu_left && x < w - 1; 253 | 254 | return false; 255 | } 256 | 257 | if (IS_SCREEN(viewscreen_civlistst)) 258 | { 259 | if (x < w - 55) 260 | { 261 | is_minimap = true; 262 | return false; 263 | } 264 | 265 | return true; 266 | } 267 | 268 | if (IS_SCREEN(viewscreen_dungeonmodest)) 269 | { 270 | // TODO: Adventure mode 271 | 272 | if (y >= h-2) 273 | return true; 274 | 275 | return false; 276 | } 277 | 278 | #if 0 279 | if (IS_SCREEN(viewscreen_setupadventurest)) 280 | { 281 | df::viewscreen_setupadventurest *s = static_cast(ws); 282 | if (s->subscreen != df::viewscreen_setupadventurest::Nemesis) 283 | return true; 284 | else if (x < 58 || x >= 78 || y == 0 || y >= 21) 285 | return true; 286 | 287 | return false; 288 | } 289 | #endif 290 | 291 | if (IS_SCREEN(viewscreen_choose_start_sitest)) 292 | { 293 | if (y <= 1 || y >= h - 7 || x == 0 || x >= w - 28) 294 | return true; 295 | 296 | is_minimap = true; 297 | return false; 298 | } 299 | 300 | if (IS_SCREEN(viewscreen_new_regionst)) 301 | { 302 | if (y <= 1 || y >= h - 2 || x <= 37 || x == w - 1) 303 | return true; 304 | 305 | is_minimap = true; 306 | 307 | return false; 308 | } 309 | 310 | if (IS_SCREEN(viewscreen_layer_export_play_mapst)) 311 | { 312 | if (x == w - 1 || x < w - 23) 313 | return true; 314 | 315 | return false; 316 | } 317 | 318 | if (IS_SCREEN(viewscreen_overallstatusst)) 319 | { 320 | if ((x == 46 || x == 71) && y >= 8) 321 | return false; 322 | 323 | return true; 324 | } 325 | 326 | if (IS_SCREEN(viewscreen_movieplayerst)) 327 | { 328 | df::viewscreen_movieplayerst *s = static_cast(ws); 329 | return !s->is_playing; 330 | } 331 | 332 | /*if (IS_SCREEN(viewscreen_petst)) 333 | { 334 | if (x == 41 && y >= 7) 335 | return false; 336 | 337 | return true; 338 | }*/ 339 | 340 | return true; 341 | } 342 | 343 | void remove_screen(df::viewscreen* v) 344 | { 345 | if (v->parent) 346 | { 347 | v->parent->child = v->child; 348 | } 349 | if (v->child) 350 | { 351 | v->child->parent = v->parent; 352 | } 353 | 354 | delete v; 355 | } 356 | 357 | bool is_siege() 358 | { 359 | for (const df::invasion_info* invasion : df::global::ui->invasions.list) 360 | { 361 | if (invasion && invasion->flags.bits.siege && invasion->flags.bits.active) 362 | { 363 | return true; 364 | } 365 | } 366 | return false; 367 | } 368 | 369 | std::string unit_info(int32_t unit_id) 370 | { 371 | std::stringstream ss; 372 | ss << "{unit id: " << unit_id; 373 | const df::unit* unit = df::unit::find(unit_id); 374 | if (unit) 375 | { 376 | ss << ", sex: " << (int32_t)unit->sex; 377 | const df::language_name& name = unit->name; 378 | ss << ", name: \"" << name.first_name << "\""; 379 | } 380 | ss << "}"; 381 | return ss.str(); 382 | } 383 | 384 | std::string historical_figure_info(int32_t figure_id) 385 | { 386 | std::stringstream ss; 387 | ss << "{historical figure id: " << figure_id; 388 | const df::historical_figure* figure = df::historical_figure::find(figure_id); 389 | if (figure) 390 | { 391 | ss << ", unit: " << unit_info(figure->unit_id); 392 | } 393 | ss << "}"; 394 | return ss.str(); 395 | } 396 | 397 | std::vector word_wrap_lines(const std::string& str, uint16_t width) 398 | { 399 | std::vector out; 400 | size_t _end = 0; 401 | while (_end < str.length()) 402 | { 403 | size_t start = _end; 404 | size_t last_break = _end; 405 | while (_end - start < width) 406 | { 407 | if (str[_end] == '\n') 408 | { 409 | last_break = _end; 410 | break; 411 | } 412 | else if (str[_end] == ' ') 413 | { 414 | last_break = _end; 415 | } 416 | ++_end; 417 | if (_end == str.length()) 418 | { 419 | last_break = _end; 420 | break; 421 | } 422 | } 423 | if (last_break == start) 424 | { 425 | last_break = _end; 426 | } 427 | out.emplace_back(str.substr(start, last_break - start)); 428 | _end = last_break; 429 | if (_end < str.length()) 430 | { 431 | if (str[_end] == '\n') 432 | { 433 | ++_end; 434 | } 435 | while (_end < str.length() && str[_end] == ' ') 436 | { 437 | ++_end; 438 | } 439 | } 440 | } 441 | 442 | return out; 443 | } 444 | 445 | // returns a unique string representing the current menu. 446 | menu_id get_current_menu_id() 447 | { 448 | df::viewscreen* vs; 449 | virtual_identity* id; 450 | UPDATE_VS(vs, id); 451 | 452 | std::string focus = Gui::getFocusString(vs); 453 | 454 | // game adds /Floor, /None, etc; we don't want that. 455 | if (focus.rfind("dwarfmode/LookAround", 0) == 0) 456 | { 457 | focus = "dwarfmode/LookAround"; 458 | } 459 | 460 | if (focus.rfind("dwarfmode/ViewUnits", 0) == 0) 461 | { 462 | focus = "dwarfmode/ViewUnits"; 463 | } 464 | 465 | if (focus.rfind("dwarfmode/BuildingItems", 0) == 0) 466 | { 467 | focus = "dwarfmode/BuildingItems"; 468 | } 469 | 470 | if (focus.rfind("unitlist", 0) == 0) 471 | { 472 | focus = "unitlist"; 473 | } 474 | 475 | // remove /On, /Off 476 | if (startsWith(focus, "layer_stockpile")) 477 | { 478 | focus = replace_all(focus, "/On", ""); 479 | focus = replace_all(focus, "/Off", ""); 480 | 481 | // FIXME: just replace the focusstring logic completely for this screen. This is dumb. :/ 482 | focus = replace_all(focus, "Animals/Animals", "Animals"); 483 | focus = replace_all(focus, "Corpses/Corpses", "Corpses"); 484 | focus = replace_all(focus, "Coins/Coins", "Coins"); 485 | focus = replace_all(focus, "Leather/Leather", "Leather"); 486 | focus = replace_all(focus, "Wood/Wood", "Wood"); 487 | } 488 | 489 | // replace /Job suffix with /Empty 490 | if (startsWith(focus, "dwarfmode/QueryBuilding")) 491 | { 492 | focus = replace_all(focus, "/Job", "/Empty"); 493 | } 494 | 495 | const df::enums::ui_sidebar_mode::ui_sidebar_mode LOCATIONS 496 | = static_cast((uint32_t)df::enums::ui_sidebar_mode::ArenaTrees + 1); 497 | 498 | // append some submenu IDs 499 | if (id == &df::viewscreen_dwarfmodest::_identity) 500 | { 501 | df::viewscreen_dwarfmodest* vs_dwarf = static_cast(vs); 502 | if (df::global::ui->main.mode == df::enums::ui_sidebar_mode::QueryBuilding) 503 | { 504 | df::ui_sidebar_menus& sidebar = *df::global::ui_sidebar_menus; 505 | if (df::building *selected = df::global::world->selected_building) 506 | { 507 | virtual_identity *building_id = virtual_identity::get(selected); 508 | if (building_id == &df::building_trapst::_identity) 509 | { 510 | df::building_trapst* trap = (df::building_trapst*)selected; 511 | if (trap->trap_type == trap_type::Lever) 512 | { 513 | if (df::global::ui_workshop_in_add && *df::global::ui_workshop_in_add) 514 | { 515 | // lever target 516 | if (df::global::ui_lever_target_type) 517 | { 518 | focus += "/target"; 519 | if (*df::global::ui_lever_target_type != df::lever_target_type::NONE) 520 | { 521 | focus += "/" + enum_item_key(*df::global::ui_lever_target_type); 522 | } 523 | } 524 | } 525 | } 526 | } 527 | // workshop subscreen information 528 | else if (endsWith(focus, "/AddJob")) 529 | { 530 | focus += " query-info: " 531 | + std::to_string(sidebar.workshop_job.mat_type) + " " 532 | + std::to_string(sidebar.workshop_job.category_id) + " " 533 | + std::to_string(sidebar.workshop_job.mat_index) + " " 534 | + std::to_string(sidebar.workshop_job.material_category.whole) + " " 535 | + std::to_string(sidebar.building.category_id); 536 | } 537 | } 538 | } 539 | else if (df::global::ui->main.mode == LOCATIONS) 540 | { 541 | df::ui_sidebar_menus& sidebar = *df::global::ui_sidebar_menus; 542 | if (sidebar.location.in_create) 543 | { 544 | focus += "/location/create"; 545 | } 546 | if (sidebar.location.in_choose_deity) 547 | { 548 | focus += "/select-deity"; 549 | } 550 | } 551 | else if (df::global::ui->main.mode == df::enums::ui_sidebar_mode::Zones) 552 | { 553 | const auto& zone = df::global::ui_sidebar_menus->zone; 554 | if (zone.remove) 555 | { 556 | focus += "/Remove"; 557 | } 558 | else 559 | { 560 | // for some reason, resizing floor zones can cause a segfault?? 561 | //focus += "/" + enum_item_key(zone.mode); 562 | } 563 | } 564 | else if (df::global::ui->main.mode == df::enums::ui_sidebar_mode::Hauling) 565 | { 566 | // completely redone this string because the default breaks the requirements too much. 567 | focus = "dwarfmode/Hauling"; 568 | if (df::global::ui->hauling.in_assign_vehicle) 569 | { 570 | focus += "/AssignVehicle"; 571 | } 572 | else 573 | { 574 | if (df::global::ui->hauling.in_name) 575 | focus += "/Rename"; 576 | else if (df::global::ui->hauling.in_stop) 577 | focus += "/DefineStop"; 578 | else 579 | focus += "/Select"; 580 | } 581 | } 582 | else if (df::global::ui->main.mode == df::enums::ui_sidebar_mode::ViewUnits) 583 | { 584 | if (df::global::ui_selected_unit) 585 | { 586 | int32_t selected_unit = *df::global::ui_selected_unit; 587 | if (df::unit* unit = vector_get(df::global::world->units.active, selected_unit)) 588 | { 589 | focus += "/unit-id-" + std::to_string(unit->id); 590 | } 591 | } 592 | } 593 | else if (df::global::ui->main.mode == df::enums::ui_sidebar_mode::Squads) 594 | { 595 | auto& squads = df::global::ui->squads; 596 | 597 | // if selecting inside squad, append to menu. 598 | if (squads.sel_indiv_squad >= 0 599 | && squads.sel_indiv_squad < static_cast(squads.list.size())) 600 | { 601 | df::squad* squad = squads.list.at(squads.sel_indiv_squad); 602 | if (squad) 603 | { 604 | focus += "/squad-id-" + std::to_string(squad->id); 605 | } 606 | } 607 | if (squads.in_move_order) 608 | { 609 | focus += "/move"; 610 | } 611 | if (squads.in_kill_order) 612 | { 613 | focus += "/kill"; 614 | } 615 | if (squads.in_kill_rect) 616 | { 617 | focus += "/rect"; 618 | } 619 | if (squads.in_kill_list) 620 | { 621 | focus += "/list"; 622 | } 623 | } 624 | } 625 | else if (id == &df::viewscreen_createquotast::_identity) 626 | { 627 | df::viewscreen_createquotast* vs_quota = static_cast(vs); 628 | if (vs_quota->want_quantity) focus += "/WantQuantity"; 629 | } 630 | else if (id == &df::viewscreen_unitst::_identity) 631 | { 632 | df::viewscreen_unitst* vs_unit = static_cast(vs); 633 | if (df::unit* unit = vs_unit->unit) 634 | { 635 | focus += "/" + std::to_string(unit->id); 636 | } 637 | } 638 | else if (id == &df::viewscreen_customize_unitst::_identity) 639 | { 640 | df::viewscreen_customize_unitst* vs_customize_unit = static_cast(vs); 641 | if (vs_customize_unit->editing_nickname) 642 | { 643 | focus += "/nickname"; 644 | } 645 | if (vs_customize_unit->editing_profession) 646 | { 647 | focus += "/profession"; 648 | } 649 | } 650 | else if (id == &df::viewscreen_layer_squad_schedulest::_identity) 651 | { 652 | df::viewscreen_layer_squad_schedulest* vs_schedule = static_cast(vs); 653 | assert(vs_schedule); 654 | if (vs_schedule->in_name_cell) 655 | { 656 | focus += "/name"; 657 | } 658 | if (vs_schedule->in_give_order) 659 | { 660 | focus += "/order/give"; 661 | } 662 | if (vs_schedule->in_edit_order) 663 | { 664 | focus += "/order/edit"; 665 | } 666 | } 667 | else if (id == &df::viewscreen_itemst::_identity) 668 | { 669 | df::viewscreen_itemst* vs_item = static_cast(vs); 670 | df::item* item = vs_item->item; 671 | if (item) 672 | { 673 | focus += "/" + std::to_string(item->id); 674 | } 675 | } 676 | else if (id == &df::viewscreen_civlistst::_identity) 677 | { 678 | df::viewscreen_civlistst* vs_civ = static_cast(vs); 679 | focus += "/" + enum_item_key(vs_civ->page); 680 | } 681 | else if (id == &df::viewscreen_layer_militaryst::_identity) 682 | { 683 | df::viewscreen_layer_militaryst* vs_military = static_cast(vs); 684 | if (vs_military->page == df::viewscreen_layer_militaryst::Alerts) 685 | { 686 | if (vs_military->in_rename_alert) 687 | { 688 | focus += "/name"; 689 | } 690 | if (vs_military->in_delete_alert) 691 | { 692 | focus += "/delete"; 693 | } 694 | } 695 | else if (vs_military->page == df::viewscreen_layer_militaryst::Uniforms) 696 | { 697 | if (vs_military->equip.in_name_uniform) 698 | { 699 | focus += "/name"; 700 | } 701 | else 702 | { 703 | focus += "/" + enum_item_key(vs_military->equip.edit_mode); 704 | } 705 | } 706 | else if (vs_military->page == df::viewscreen_layer_militaryst::Ammunition) 707 | { 708 | if (vs_military->ammo.in_add_item) 709 | { 710 | focus += "/addItem"; 711 | } 712 | if (vs_military->ammo.in_set_material) 713 | { 714 | focus += "/setMaterial"; 715 | } 716 | } 717 | else 718 | { 719 | if (renaming_squad_id() >= 0) 720 | { 721 | focus += "/rename/squad-" + std::to_string(renaming_squad_id()); 722 | } 723 | } 724 | } 725 | else if (id == &df::viewscreen_reportlistst::_identity) 726 | { 727 | df::viewscreen_reportlistst* vs_r = static_cast(vs); 728 | if (vs_r->mission_report) 729 | { 730 | focus += "/mission"; 731 | const std::string& title = vs_r->mission_report->title; 732 | if (title.length()) 733 | { 734 | focus += "(title=" + title + ")"; 735 | } 736 | } 737 | else if (vs_r->spoils_report_title) 738 | { 739 | focus += "/spoils(title=" + *vs_r->spoils_report_title + ")"; 740 | } 741 | } 742 | 743 | return focus; 744 | } 745 | 746 | bool viewing_mission_report() 747 | { 748 | df::viewscreen* vs; 749 | virtual_identity* id; 750 | UPDATE_VS(vs, id); 751 | 752 | if (id == &df::viewscreen_reportlistst::_identity) 753 | { 754 | df::viewscreen_reportlistst* vs_r = static_cast(vs); 755 | if (vs_r->mission_report) 756 | { 757 | return true; 758 | } 759 | } 760 | 761 | return false; 762 | } 763 | 764 | bool mission_report_paused() 765 | { 766 | df::viewscreen* vs; 767 | virtual_identity* id; 768 | UPDATE_VS(vs, id); 769 | 770 | if (id == &df::viewscreen_reportlistst::_identity) 771 | { 772 | df::viewscreen_reportlistst* vs_r = static_cast(vs); 773 | if (vs_r->mission_report) 774 | { 775 | return vs_r->mission_report_paused; 776 | } 777 | } 778 | 779 | return false; 780 | } 781 | 782 | bool mission_report_complete() 783 | { 784 | df::viewscreen* vs; 785 | virtual_identity* id; 786 | UPDATE_VS(vs, id); 787 | 788 | if (id == &df::viewscreen_reportlistst::_identity) 789 | { 790 | df::viewscreen_reportlistst* vs_r = static_cast(vs); 791 | if (vs_r->mission_report) 792 | { 793 | // not sure what "finished" means here... trying a few interpretations because lazy 794 | 795 | if (vs_r->mission_text_finished == 1 && vs_r->mission_path_finished == 1) 796 | { 797 | return true; 798 | } 799 | 800 | if (vs_r->mission_text_finished == vs_r->mission_text_progress && vs_r->mission_path_finished == vs_r->mission_path_progress) 801 | { 802 | return true; 803 | } 804 | } 805 | } 806 | 807 | return false; 808 | } 809 | 810 | int32_t renaming_squad_id() 811 | { 812 | df::viewscreen* vs; 813 | virtual_identity* id; 814 | UPDATE_VS(vs, id); 815 | if (id == &df::viewscreen_layer_militaryst::_identity) 816 | { 817 | df::viewscreen_layer_militaryst* vs_military = static_cast(vs); 818 | if (vs_military && vs_military->page == df::viewscreen_layer_militaryst::Positions) 819 | { 820 | df::squad* rename_squad = df::global::ui_sidebar_menus->unit.rename_squad; 821 | if (rename_squad) 822 | { 823 | return rename_squad->id; 824 | } 825 | } 826 | } 827 | return -1; 828 | } 829 | 830 | void center_view_on_coord(const Coord& _c) 831 | { 832 | Coord c = _c; 833 | auto dims = DFHack::Gui::getDwarfmodeViewDims(); 834 | c.x -= (dims.map_x2 - dims.map_x1) / 2; 835 | c.y -= (dims.y2 - dims.y1) / 2; 836 | 837 | int32_t w = df::global::world->map.x_count; 838 | int32_t h = df::global::world->map.y_count; 839 | 840 | // bounds clamping 841 | if (w > 0) 842 | { 843 | c.x = std::min(c.x, w - (dims.map_x2 - dims.map_x1) - 1); 844 | c.y = std::min(c.y, h - (dims.y2 - dims.y1) - 1); 845 | } 846 | 847 | if (c.x < 0) c.x = 0; 848 | if (c.y < 0) c.y = 0; 849 | 850 | Gui::setViewCoords(c.x, c.y, c.z); 851 | } 852 | 853 | bool menu_id_matches(const menu_id& a, const menu_id& b) 854 | { 855 | if (a == K_NOCHECK) return true; 856 | 857 | if (a == b) return true; 858 | 859 | // negation 860 | if (startsWith(a, "^")) 861 | { 862 | return a.substr(1) != b; 863 | } 864 | 865 | return false; 866 | } -------------------------------------------------------------------------------- /server/hackutil.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "Core.h" 10 | #include "Console.h" 11 | #include "modules/World.h" 12 | #include "df/ui_sidebar_mode.h" 13 | #include "df/viewscreen.h" 14 | #include "df/viewscreen_meetingst.h" 15 | #include "df/ui_build_selector.h" 16 | #include "df/unit.h" 17 | #include "modules/Gui.h" 18 | #include "modules/Screen.h" 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | struct Coord 25 | { 26 | int32_t x, y, z; 27 | Coord() 28 | : x(0) 29 | , y(0) 30 | , z(0) 31 | { } 32 | Coord(int32_t x, int32_t y, int32_t z) 33 | : x(x) 34 | , y(y) 35 | , z(z) 36 | { } 37 | Coord(const Coord&)=default; 38 | Coord(Coord&&)=default; 39 | Coord(const df::coord& c) 40 | : x(c.x) 41 | , y(c.y) 42 | , z(c.z) 43 | { } 44 | 45 | Coord& operator=(const Coord&)=default; 46 | Coord& operator=(Coord&&)=default; 47 | 48 | // due to an unknown bug, all operator commands need to be explicitly invoked 49 | // (e.g. a.operator-(b);) 50 | operator bool() const 51 | { 52 | return x != 0 || y != 0 || z != 0; 53 | } 54 | Coord operator-(const Coord& other) const 55 | { 56 | return{ x-other.x, y-other.y, z-other.z }; 57 | } 58 | Coord operator+(const Coord& other) const 59 | { 60 | return{ x+other.x, y+other.y, z+other.z }; 61 | } 62 | bool operator!=(const Coord& other) const 63 | { 64 | return x != other.x || y != other.y || z != other.z; 65 | } 66 | }; 67 | 68 | typedef std::string menu_id; 69 | static const menu_id K_NOCHECK = "*"; 70 | 71 | menu_id get_current_menu_id(); 72 | 73 | // returns true if a describes b. (e.g if a == b.) 74 | bool menu_id_matches(const menu_id& a, const menu_id& b); 75 | 76 | #define UPDATE_VS(vs, id) {vs = DFHack::Gui::getCurViewscreen(true); id = df::virtual_identity::get(vs); } 77 | 78 | // meant for printing debug information about a unit. 79 | std::string unit_info(int32_t unit_id); 80 | std::string historical_figure_info(int32_t unit_id); 81 | 82 | void show_announcement(std::string announcement); 83 | 84 | std::string historical_figure_info(int32_t figure_id); 85 | 86 | // returns how many not-dismissed ancestors this viewscreen has. 87 | size_t get_vs_depth(df::viewscreen* vs); 88 | 89 | inline bool is_dwarf_mode() 90 | { 91 | DFHack::t_gamemodes gm; 92 | DFHack::World::ReadGameMode(gm); 93 | return gm.g_mode == df::game_mode::DWARF; 94 | } 95 | 96 | bool is_at_root(); 97 | bool is_realtime_dwarf_menu(); // root menu or squads submenu 98 | bool return_to_root(); // error -> returns false 99 | 100 | // closes all viewscreens *if possible* 101 | // does not deallocate them 102 | // returns false on error 103 | bool defer_return_to_root(); 104 | 105 | bool is_text_tile(int x, int y, bool &is_map, bool &is_overworld); 106 | 107 | // https://stackoverflow.com/a/42844629 108 | inline bool endsWith(const std::string& str, const std::string& suffix) 109 | { 110 | return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix); 111 | } 112 | 113 | inline bool startsWith(const std::string& str, const std::string& prefix) 114 | { 115 | return str.rfind(prefix, 0) == 0; 116 | } 117 | 118 | inline bool contains(const std::string& str, const std::string& infix) 119 | { 120 | return str.find(infix) != std::string::npos; 121 | } 122 | 123 | // like the word warp in MiscUtils, but respects existing line endings. 124 | // TODO: use c++17's std::string_view instead. 125 | std::vector word_wrap_lines(const std::string& str, uint16_t width); 126 | 127 | template 128 | inline bool contains(const std::set& container, const T& value) 129 | { 130 | return container.find(value) != container.end(); 131 | } 132 | 133 | // from https://stackoverflow.com/a/24315631 134 | inline std::string replace_all(std::string str, const std::string& from, const std::string& to) { 135 | size_t start_pos = 0; 136 | while((start_pos = str.find(from, start_pos)) != std::string::npos) { 137 | str.replace(start_pos, from.length(), to); 138 | start_pos += to.length(); 139 | } 140 | return str; 141 | } 142 | 143 | // checks if x is in inclusive range [a, b] or [b, a] 144 | inline bool in_range(int32_t x, int32_t a, int32_t b) 145 | { 146 | return (x >= a && x <= b) || (x >= b && x<= a); 147 | } 148 | 149 | void remove_screen(df::viewscreen* v); 150 | 151 | inline uint8_t pen_colour(const DFHack::Screen::Pen& p) 152 | { 153 | return (p.bold << 6) | (p.bg << 3) | p.fg; 154 | } 155 | 156 | inline void set_pen_colour(DFHack::Screen::Pen& p, uint8_t col) 157 | { 158 | p.bold = !!(col & 64); 159 | p.bg = (col >> 3) & 7; 160 | p.fg = col & 7; 161 | } 162 | 163 | inline bool is_designation_mode_sub(df::ui_sidebar_mode mode) 164 | { 165 | if (mode >= df::ui_sidebar_mode::DesignateItemsClaim && mode <= df::ui_sidebar_mode::DesignateItemsUnhide) 166 | { 167 | return true; 168 | } 169 | 170 | return false; 171 | } 172 | 173 | // basic designations only 174 | inline bool is_designation_mode(df::ui_sidebar_mode mode) 175 | { 176 | using namespace df; 177 | if (mode >= ui_sidebar_mode::DesignateMine && mode <= ui_sidebar_mode::DesignateCarveFortification) 178 | { 179 | return true; 180 | } 181 | if (mode == ui_sidebar_mode::DesignateRemoveConstruction) 182 | { 183 | return true; 184 | } 185 | if (mode >= ui_sidebar_mode::DesignateChopTrees && mode <= ui_sidebar_mode::DesignateToggleMarker) 186 | { 187 | return true; 188 | } 189 | return false; 190 | } 191 | 192 | void center_view_on_coord(const Coord&); 193 | 194 | bool is_siege(); 195 | 196 | int32_t renaming_squad_id(); 197 | 198 | // FIXME: make these functions not inline. 199 | inline bool isBuildMenu(){ 200 | return df::global::ui->main.mode == df::enums::ui_sidebar_mode::Build; 201 | } 202 | 203 | inline bool isBuildPositionMenu(){ 204 | using df::global::ui_build_selector; 205 | if (ui_build_selector) 206 | { 207 | // Not selecting, or no choices? 208 | if (ui_build_selector->building_type < 0) 209 | return false; 210 | else if (ui_build_selector->stage != 2) 211 | { 212 | if (ui_build_selector->stage != 1) 213 | return false; 214 | else 215 | return true; 216 | } 217 | } 218 | return false; 219 | } 220 | 221 | inline bool following_item_or_unit() 222 | { 223 | return df::global::ui->follow_item != -1 || df::global::ui->follow_unit != -1; 224 | } 225 | 226 | bool viewing_mission_report(); 227 | bool mission_report_paused(); 228 | bool mission_report_complete(); -------------------------------------------------------------------------------- /server/input.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #include "input.hpp" 7 | 8 | #include 9 | 10 | SDL::Key mapInputCodeToSDL( const uint32_t code ) 11 | { 12 | #define MAP(a, b) case a: return b; 13 | switch (code) 14 | { 15 | // {{{ keysyms 16 | MAP(96, SDL::K_KP0); 17 | MAP(97, SDL::K_KP1); 18 | MAP(98, SDL::K_KP2); 19 | MAP(99, SDL::K_KP3); 20 | MAP(100, SDL::K_KP4); 21 | MAP(101, SDL::K_KP5); 22 | MAP(102, SDL::K_KP6); 23 | MAP(103, SDL::K_KP7); 24 | MAP(104, SDL::K_KP8); 25 | MAP(105, SDL::K_KP9); 26 | MAP(144, SDL::K_NUMLOCK); 27 | 28 | MAP(111, SDL::K_KP_DIVIDE); 29 | MAP(106, SDL::K_KP_MULTIPLY); 30 | MAP(109, SDL::K_KP_MINUS); 31 | MAP(107, SDL::K_KP_PLUS); 32 | 33 | MAP(33, SDL::K_PAGEUP); 34 | MAP(34, SDL::K_PAGEDOWN); 35 | MAP(35, SDL::K_END); 36 | MAP(36, SDL::K_HOME); 37 | MAP(46, SDL::K_DELETE); 38 | 39 | MAP(112, SDL::K_F1); 40 | MAP(113, SDL::K_F2); 41 | MAP(114, SDL::K_F3); 42 | MAP(115, SDL::K_F4); 43 | MAP(116, SDL::K_F5); 44 | MAP(117, SDL::K_F6); 45 | MAP(118, SDL::K_F7); 46 | MAP(119, SDL::K_F8); 47 | MAP(120, SDL::K_F9); 48 | MAP(121, SDL::K_F10); 49 | MAP(122, SDL::K_F11); 50 | MAP(123, SDL::K_F12); 51 | 52 | MAP(37, SDL::K_LEFT); 53 | MAP(39, SDL::K_RIGHT); 54 | MAP(38, SDL::K_UP); 55 | MAP(40, SDL::K_DOWN); 56 | 57 | MAP(188, SDL::K_LESS); 58 | MAP(190, SDL::K_GREATER); 59 | 60 | MAP(13, SDL::K_RETURN); 61 | 62 | //MAP(16, SDL::K_LSHIFT); 63 | //MAP(17, SDL::K_LCTRL); 64 | //MAP(18, SDL::K_LALT); 65 | 66 | MAP(27, SDL::K_ESCAPE); 67 | #undef MAP 68 | // }}} 69 | } 70 | if (code <= 177) 71 | return (SDL::Key)code; 72 | return SDL::K_UNKNOWN; 73 | } 74 | -------------------------------------------------------------------------------- /server/input.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "SDL_events.h" 9 | #include "SDL_keysym.h" 10 | 11 | SDL::Key mapInputCodeToSDL( const uint32_t code ); 12 | -------------------------------------------------------------------------------- /server/keymap.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #include "keymap.hpp" 7 | #include "parse_config.hpp" 8 | 9 | #include "ColorText.h" 10 | #include "df/interface_key.h" 11 | #include "SDL_events.h" 12 | #include "SDL_keysym.h" 13 | 14 | using std::string; 15 | using std::map; 16 | using std::vector; 17 | 18 | using namespace DFHack; 19 | 20 | static int32_t utf8_decode(const std::string& s) 21 | { 22 | if (s.length() == 1) return s.at(0); 23 | // TODO 24 | return -1; 25 | } 26 | 27 | static std::map sdlKeyNames; 28 | 29 | static void setSdlKeyNames() 30 | { 31 | using namespace SDL; 32 | sdlKeyNames.clear(); 33 | sdlKeyNames.insert({"Backspace", K_BACKSPACE}); 34 | sdlKeyNames.insert({"Tab", K_TAB}); 35 | sdlKeyNames.insert({"Clear", K_CLEAR}); 36 | sdlKeyNames.insert({"Enter", K_RETURN}); 37 | sdlKeyNames.insert({"Pause", K_PAUSE}); 38 | sdlKeyNames.insert({"ESC", K_ESCAPE}); 39 | sdlKeyNames.insert({"Space", K_SPACE}); 40 | sdlKeyNames.insert({"Exclaim", K_EXCLAIM}); 41 | sdlKeyNames.insert({"Quotedbl", K_QUOTEDBL}); 42 | sdlKeyNames.insert({"Hash", K_HASH}); 43 | sdlKeyNames.insert({"Dollar", K_DOLLAR}); 44 | sdlKeyNames.insert({"Ampersand", K_AMPERSAND}); 45 | sdlKeyNames.insert({"Quote", K_QUOTE}); 46 | sdlKeyNames.insert({"Leftparen", K_LEFTPAREN}); 47 | sdlKeyNames.insert({"Rightparen", K_RIGHTPAREN}); 48 | sdlKeyNames.insert({"Asterisk", K_ASTERISK}); 49 | sdlKeyNames.insert({"Plus", K_PLUS}); 50 | sdlKeyNames.insert({"Comma", K_COMMA}); 51 | sdlKeyNames.insert({"Minus", K_MINUS}); 52 | sdlKeyNames.insert({"Period", K_PERIOD}); 53 | sdlKeyNames.insert({"Slash", K_SLASH}); 54 | sdlKeyNames.insert({"0", K_0}); 55 | sdlKeyNames.insert({"1", K_1}); 56 | sdlKeyNames.insert({"2", K_2}); 57 | sdlKeyNames.insert({"3", K_3}); 58 | sdlKeyNames.insert({"4", K_4}); 59 | sdlKeyNames.insert({"5", K_5}); 60 | sdlKeyNames.insert({"6", K_6}); 61 | sdlKeyNames.insert({"7", K_7}); 62 | sdlKeyNames.insert({"8", K_8}); 63 | sdlKeyNames.insert({"9", K_9}); 64 | sdlKeyNames.insert({"Colon", K_COLON}); 65 | sdlKeyNames.insert({"Semicolon", K_SEMICOLON}); 66 | sdlKeyNames.insert({"Less", K_LESS}); 67 | sdlKeyNames.insert({"Equals", K_EQUALS}); 68 | sdlKeyNames.insert({"Greater", K_GREATER}); 69 | sdlKeyNames.insert({"Question", K_QUESTION}); 70 | sdlKeyNames.insert({"At", K_AT}); 71 | sdlKeyNames.insert({"Leftbracket", K_LEFTBRACKET}); 72 | sdlKeyNames.insert({"Backslash", K_BACKSLASH}); 73 | sdlKeyNames.insert({"Rightbracket", K_RIGHTBRACKET}); 74 | sdlKeyNames.insert({"Caret", K_CARET}); 75 | sdlKeyNames.insert({"Underscore", K_UNDERSCORE}); 76 | sdlKeyNames.insert({"Backquote", K_BACKQUOTE}); 77 | sdlKeyNames.insert({"a", K_a}); 78 | sdlKeyNames.insert({"b", K_b}); 79 | sdlKeyNames.insert({"c", K_c}); 80 | sdlKeyNames.insert({"d", K_d}); 81 | sdlKeyNames.insert({"e", K_e}); 82 | sdlKeyNames.insert({"f", K_f}); 83 | sdlKeyNames.insert({"g", K_g}); 84 | sdlKeyNames.insert({"h", K_h}); 85 | sdlKeyNames.insert({"i", K_i}); 86 | sdlKeyNames.insert({"j", K_j}); 87 | sdlKeyNames.insert({"k", K_k}); 88 | sdlKeyNames.insert({"l", K_l}); 89 | sdlKeyNames.insert({"m", K_m}); 90 | sdlKeyNames.insert({"n", K_n}); 91 | sdlKeyNames.insert({"o", K_o}); 92 | sdlKeyNames.insert({"p", K_p}); 93 | sdlKeyNames.insert({"q", K_q}); 94 | sdlKeyNames.insert({"r", K_r}); 95 | sdlKeyNames.insert({"s", K_s}); 96 | sdlKeyNames.insert({"t", K_t}); 97 | sdlKeyNames.insert({"u", K_u}); 98 | sdlKeyNames.insert({"v", K_v}); 99 | sdlKeyNames.insert({"w", K_w}); 100 | sdlKeyNames.insert({"x", K_x}); 101 | sdlKeyNames.insert({"y", K_y}); 102 | sdlKeyNames.insert({"z", K_z}); 103 | sdlKeyNames.insert({"Delete", K_DELETE}); 104 | sdlKeyNames.insert({"Numpad 0", K_KP0}); 105 | sdlKeyNames.insert({"Numpad 1", K_KP1}); 106 | sdlKeyNames.insert({"Numpad 2", K_KP2}); 107 | sdlKeyNames.insert({"Numpad 3", K_KP3}); 108 | sdlKeyNames.insert({"Numpad 4", K_KP4}); 109 | sdlKeyNames.insert({"Numpad 5", K_KP5}); 110 | sdlKeyNames.insert({"Numpad 6", K_KP6}); 111 | sdlKeyNames.insert({"Numpad 7", K_KP7}); 112 | sdlKeyNames.insert({"Numpad 8", K_KP8}); 113 | sdlKeyNames.insert({"Numpad 9", K_KP9}); 114 | sdlKeyNames.insert({"Numpad Period", K_KP_PERIOD}); 115 | sdlKeyNames.insert({"Numpad Divide", K_KP_DIVIDE}); 116 | sdlKeyNames.insert({"Numpad Multiply", K_KP_MULTIPLY}); 117 | sdlKeyNames.insert({"Numpad Plus", K_KP_PLUS}); 118 | sdlKeyNames.insert({"Numpad Minus", K_KP_MINUS}); 119 | sdlKeyNames.insert({"Numpad Enter", K_KP_ENTER}); 120 | sdlKeyNames.insert({"Numpad Equals", K_KP_EQUALS}); 121 | sdlKeyNames.insert({"Up", K_UP}); 122 | sdlKeyNames.insert({"Down", K_DOWN}); 123 | sdlKeyNames.insert({"Right", K_RIGHT}); 124 | sdlKeyNames.insert({"Left", K_LEFT}); 125 | sdlKeyNames.insert({"Insert", K_INSERT}); 126 | sdlKeyNames.insert({"Home", K_HOME}); 127 | sdlKeyNames.insert({"End", K_END}); 128 | sdlKeyNames.insert({"Page Up", K_PAGEUP}); 129 | sdlKeyNames.insert({"Page Down", K_PAGEDOWN}); 130 | sdlKeyNames.insert({"F1", K_F1}); 131 | sdlKeyNames.insert({"F2", K_F2}); 132 | sdlKeyNames.insert({"F3", K_F3}); 133 | sdlKeyNames.insert({"F4", K_F4}); 134 | sdlKeyNames.insert({"F5", K_F5}); 135 | sdlKeyNames.insert({"F6", K_F6}); 136 | sdlKeyNames.insert({"F7", K_F7}); 137 | sdlKeyNames.insert({"F8", K_F8}); 138 | sdlKeyNames.insert({"F9", K_F9}); 139 | sdlKeyNames.insert({"F10", K_F10}); 140 | sdlKeyNames.insert({"F11", K_F11}); 141 | sdlKeyNames.insert({"F12", K_F12}); 142 | sdlKeyNames.insert({"F13", K_F13}); 143 | sdlKeyNames.insert({"F14", K_F14}); 144 | sdlKeyNames.insert({"F15", K_F15}); 145 | sdlKeyNames.insert({"Numlock", K_NUMLOCK}); 146 | sdlKeyNames.insert({"Capslock", K_CAPSLOCK}); 147 | sdlKeyNames.insert({"Scrollock", K_SCROLLOCK}); 148 | sdlKeyNames.insert({"Rshift", K_RSHIFT}); 149 | sdlKeyNames.insert({"Lshift", K_LSHIFT}); 150 | sdlKeyNames.insert({"Rctrl", K_RCTRL}); 151 | sdlKeyNames.insert({"Lctrl", K_LCTRL}); 152 | sdlKeyNames.insert({"Ralt", K_RALT}); 153 | sdlKeyNames.insert({"Lalt", K_LALT}); 154 | sdlKeyNames.insert({"Rmeta", K_RMETA}); 155 | sdlKeyNames.insert({"Lmeta", K_LMETA}); 156 | sdlKeyNames.insert({"Lsuper", K_LSUPER}); 157 | sdlKeyNames.insert({"Rsuper", K_RSUPER}); 158 | sdlKeyNames.insert({"Mode", K_MODE}); 159 | sdlKeyNames.insert({"Compose", K_COMPOSE}); 160 | sdlKeyNames.insert({"Help", K_HELP}); 161 | sdlKeyNames.insert({"Print", K_PRINT}); 162 | sdlKeyNames.insert({"Sysreq", K_SYSREQ}); 163 | sdlKeyNames.insert({"Break", K_BREAK}); 164 | sdlKeyNames.insert({"Menu", K_MENU}); 165 | sdlKeyNames.insert({"Power", K_POWER}); 166 | sdlKeyNames.insert({"Euro", K_EURO}); 167 | sdlKeyNames.insert({"Undo", K_UNDO}); 168 | } 169 | 170 | bool KeyMap::loadKeyBindings(DFHack::color_ostream& out, const string& file) 171 | { 172 | setSdlKeyNames(); 173 | 174 | const std::vector symbols = parse_config_file(file); 175 | out.color(COLOR_RED); 176 | if (symbols.empty()) 177 | { 178 | out << "No interface key config symbols found." << endl; 179 | return false; 180 | } 181 | 182 | df::interface_key key = df::enums::interface_key::NONE; 183 | int32_t repeat = 0; 184 | (void)repeat; 185 | 186 | for (const config_symbol& symbol : symbols) 187 | { 188 | if (symbol.op == "BIND") 189 | { 190 | if (symbol.args.size() != 2) 191 | { 192 | out << "Failed to parse keybinding instruction " << symbol << endl; 193 | return false; 194 | } 195 | 196 | // interface key 197 | if (!find_enum_item(&key, symbol.args.at(0))) 198 | { 199 | out << "Unknown interface key " << symbol.args.at(1); 200 | return false; 201 | } 202 | 203 | // repeat 204 | if (symbol.args.at(1) == "REPEAT_NOT") 205 | { 206 | repeat = 0; 207 | } 208 | else if (symbol.args.at(1) == "REPEAT_SLOW") 209 | { 210 | repeat = 1; 211 | } 212 | else if (symbol.args.at(1) == "REPEAT_FAST") 213 | { 214 | repeat = 2; 215 | } 216 | else 217 | { 218 | out << "unknown flag " << symbol.args.at(1) << endl; 219 | return false; 220 | } 221 | } 222 | else if (symbol.op == "SYM") 223 | { 224 | if (symbol.args.size() != 2) 225 | { 226 | out << "Failed to parse keybinding instruction " << symbol 227 | << " (wrong number of args)" << endl; 228 | return false; 229 | } 230 | 231 | KeyEvent ev; 232 | ev.type = EventType::type_key; 233 | ev.mod = std::stoi(symbol.args.at(0)); 234 | 235 | auto iter = sdlKeyNames.find(symbol.args.at(1)); 236 | if (iter == sdlKeyNames.end()) 237 | { 238 | out << "Failed to find SDL key named \"" << symbol.args.at(1) << "\"" << endl; 239 | return false; 240 | } 241 | ev.key = iter->second; 242 | 243 | keymap.emplace(std::move(ev), key); 244 | } 245 | else if (symbol.op == "BUTTON") 246 | { 247 | // TODO 248 | } 249 | else if (symbol.op == "KEY") 250 | { 251 | if (symbol.args.size() != 1) 252 | { 253 | out << "Failed to parse keybinding instruction " << symbol 254 | << " (wrong number of args)" << endl; 255 | return false; 256 | } 257 | 258 | KeyEvent ev; 259 | ev.type = EventType::type_unicode; 260 | 261 | // TODO unicode 262 | std::string charname = symbol.args.at(0); 263 | ev.unicode = utf8_decode(charname); 264 | 265 | keymap.emplace(std::move(ev), key); 266 | } 267 | else 268 | { 269 | out << "Unknown operator (first entry) in " << symbol << endl; 270 | return false; 271 | } 272 | } 273 | 274 | out.color(COLOR_RESET); 275 | return true; 276 | } 277 | 278 | std::string KeyMap::getCommandNames(const std::set& keys) 279 | { 280 | std::stringstream ss; 281 | ss << "{"; 282 | bool first = true; 283 | for (const df::interface_key& key : keys) 284 | { 285 | if (!first) 286 | { 287 | ss << ", "; 288 | } 289 | first = false; 290 | ss << getCommandName(key); 291 | } 292 | ss << "}"; 293 | if (ss.str().length() >= 256 && keys.size() > 1) 294 | { 295 | return "{" + std::to_string(keys.size()) + " keys...}"; 296 | } 297 | return ss.str(); 298 | } 299 | 300 | std::string KeyMap::getCommandName(df::interface_key key) 301 | { 302 | return DFHack::enum_item_key(key); 303 | } 304 | 305 | std::set KeyMap::toInterfaceKey(const KeyEvent & match){ 306 | std::set bindings; 307 | 308 | if (match.interface_keys.get()) 309 | { 310 | bindings.insert(match.interface_keys.get()->begin(), match.interface_keys.get()->end()); 311 | } 312 | 313 | std::pair::iterator,std::multimap::iterator> its; 314 | 315 | for (its = keymap.equal_range(match); its.first != its.second; ++its.first) 316 | bindings.insert((its.first)->second); 317 | 318 | return bindings; 319 | } 320 | 321 | KeyEvent& KeyEvent::operator=(const KeyEvent& other) 322 | { 323 | type = other.type; 324 | mod = other.mod; 325 | unicode = other.unicode; 326 | key = other.key; 327 | button = other.button; 328 | if (other.interface_keys) 329 | { 330 | interface_keys.reset(new std::set( 331 | *other.interface_keys.get() 332 | )); 333 | } 334 | return *this; 335 | } 336 | 337 | std::ostream& operator<<(std::ostream& a, const KeyEvent& match) 338 | { 339 | a << "KeyEvent {"; 340 | switch (match.type) 341 | { 342 | case EventType::type_unicode: 343 | a << "unicode "; 344 | a << (int)match.unicode; 345 | break; 346 | case EventType::type_key: 347 | a << "key "; 348 | a << (int)match.key; 349 | if (match.unicode){ 350 | a<<", unicode " << (int)match.unicode; 351 | } 352 | a << ", mod " << (int)match.mod; 353 | break; 354 | case EventType::type_button: 355 | a << "button "; 356 | a << (int)match.button; 357 | break; 358 | case EventType::type_interface: 359 | a << "interface_keys:"; 360 | if (match.interface_keys) 361 | { 362 | for (const df::interface_key& key : *match.interface_keys.get()) 363 | { 364 | a << " " << enum_item_key(key); 365 | } 366 | } 367 | break; 368 | } 369 | a << "}"; 370 | return a; 371 | } 372 | 373 | KeyMap keybindings; -------------------------------------------------------------------------------- /server/keymap.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "df/interface_key.h" 17 | #include "SDL_keysym.h" 18 | 19 | enum class EventType { 20 | type_unicode, 21 | type_key, 22 | type_button, 23 | type_interface, 24 | }; 25 | 26 | // a key event represents either a set of interface keys or 27 | // the keypress event that would generate a set of interface keys. 28 | struct KeyEvent { 29 | EventType type; 30 | uint8_t mod = 0; // not defined for type=unicode. 1: shift, 2: ctrl, 4:alt 31 | 32 | std::unique_ptr> interface_keys; 33 | uint16_t unicode = 0; 34 | SDL::Key key = SDL::K_UNKNOWN; 35 | uint8_t button = 0; 36 | 37 | inline bool operator== (const KeyEvent &other) const { 38 | if (mod != other.mod) return false; 39 | if (type != other.type) return false; 40 | switch (type) { 41 | case EventType::type_unicode: return unicode == other.unicode; 42 | case EventType::type_key: return key == other.key; 43 | case EventType::type_button: return button == other.button; 44 | default: return false; 45 | } 46 | } 47 | 48 | inline bool operator< (const KeyEvent &other) const { 49 | if (mod != other.mod) return mod < other.mod; 50 | if (type != other.type) return type < other.type; 51 | switch (type) { 52 | case EventType::type_unicode: return unicode < other.unicode; 53 | case EventType::type_key: return key < other.key; 54 | case EventType::type_button: return button < other.button; 55 | default: return false; 56 | } 57 | } 58 | 59 | KeyEvent()=default; 60 | KeyEvent(const KeyEvent& other) 61 | { 62 | *this = other; 63 | } 64 | KeyEvent(KeyEvent&&)=default; 65 | KeyEvent& operator=(const KeyEvent& other); 66 | KeyEvent& operator=(KeyEvent&&)=default; 67 | KeyEvent(const std::set& keys) 68 | { 69 | type = EventType::type_interface; 70 | interface_keys.reset(new std::set(keys)); 71 | } 72 | KeyEvent(std::set&& keys) 73 | { 74 | type = EventType::type_interface; 75 | interface_keys.reset(new std::set(std::move(keys))); 76 | } 77 | KeyEvent(df::interface_key key) 78 | { 79 | type = EventType::type_interface; 80 | interface_keys.reset(new std::set()); 81 | interface_keys->insert(key); 82 | } 83 | }; 84 | 85 | std::ostream& operator<<(std::ostream& a, const KeyEvent& match); 86 | 87 | class KeyMap 88 | { 89 | std::multimap keymap; 90 | 91 | public: 92 | // returns false on error. 93 | bool loadKeyBindings(DFHack::color_ostream& out, const std::string& file); 94 | std::set toInterfaceKey(const KeyEvent & key); 95 | std::string getCommandNames(const std::set&); 96 | std::string getCommandName(df::interface_key); 97 | }; 98 | 99 | // keybindings 100 | extern KeyMap keybindings; 101 | -------------------------------------------------------------------------------- /server/lua.cpp: -------------------------------------------------------------------------------- 1 | #include "dfplex.hpp" 2 | #include "Client.hpp" 3 | #include "callbacks.hpp" 4 | 5 | #include "Core.h" 6 | #include "modules/Gui.h" 7 | #include "DataFuncs.h" 8 | #include "PluginManager.h" 9 | #include 10 | 11 | namespace 12 | { 13 | static int g_tab_idx; 14 | 15 | // [-0, +0, -] 16 | void init_lua_reg(lua_State* L) 17 | { 18 | static bool init = false; 19 | if (!init) 20 | { 21 | lua_newtable(L); // +1 22 | g_tab_idx = luaL_ref(L,LUA_REGISTRYINDEX); // -1 23 | auto tab_idx = g_tab_idx; 24 | DFPlex::add_cb_shutdown( 25 | [L, tab_idx]() 26 | { 27 | luaL_unref(L, LUA_REGISTRYINDEX, g_tab_idx); // 0 28 | init = false; 29 | } 30 | ); 31 | init = true; 32 | } 33 | } 34 | 35 | // inspired by https://stackoverflow.com/a/31952046 36 | // [-1, +0, -] 37 | int lua_store_reg(lua_State* L) 38 | { 39 | init_lua_reg(L); 40 | 41 | // push table. 42 | lua_rawgeti(L, LUA_REGISTRYINDEX, g_tab_idx); // + 1 43 | 44 | // table should be before arg. 45 | lua_rotate(L, -2, 1); // 0 46 | 47 | // store arg in table. 48 | int t = luaL_ref(L, -2); // -1 49 | 50 | // pop table. 51 | lua_pop(L, 1); // -1 52 | 53 | // return reference to arg. 54 | return t; 55 | } 56 | 57 | // [-0, +1, -] 58 | void lua_load_reg(lua_State* L, int index) 59 | { 60 | // retrieve table. 61 | lua_rawgeti(L,LUA_REGISTRYINDEX,g_tab_idx); // +1 62 | 63 | // retrieve value. 64 | lua_rawgeti(L, -1, index); // +1 65 | 66 | // we want to pop the table. 67 | lua_rotate(L, -2, 1); // 0 68 | lua_pop(L, 1); 69 | } 70 | 71 | // returns 0 if no client found, positive unique identifier otherwise. 72 | uint32_t lua_get_client_count() 73 | { 74 | return get_client_count(); 75 | } 76 | 77 | client_long_id_t lua_get_client_id_by_index(uint32_t index) 78 | { 79 | Client* c = get_client(index); 80 | if (c) 81 | { 82 | return c->id->long_id; 83 | } 84 | 85 | return 0; 86 | } 87 | 88 | int lua_get_client_nick(lua_State* L) 89 | { 90 | client_long_id_t id = lua_tointeger(L, -1); // 0 91 | lua_pop(L, 1); // -1 92 | Client* client = get_client_by_id(id); 93 | if (client) 94 | { 95 | lua_pushstring(L, client->id->nick.c_str()); 96 | } 97 | else 98 | { 99 | lua_pushnil(L); 100 | } 101 | return 1; 102 | } 103 | 104 | int lua_get_client_cursorcoord(lua_State* L) 105 | { 106 | int id = lua_tointeger(L, -1); 107 | lua_pop(L, 1); 108 | Client* cl = get_client_by_id(id); 109 | 110 | int x = -30000, y = -30000, z = -30000; 111 | 112 | if (cl) 113 | { 114 | if (cl->ui.m_cursorcoord_set) 115 | { 116 | x = cl->ui.m_cursorcoord.x; 117 | y = cl->ui.m_cursorcoord.y; 118 | z = cl->ui.m_cursorcoord.z; 119 | } 120 | } 121 | 122 | lua_pushinteger(L, x); 123 | lua_pushinteger(L, y); 124 | lua_pushinteger(L, z); 125 | 126 | return 3; 127 | } 128 | 129 | void lua_set_client_cursorcoord(client_long_id_t id, int x, int y, int z) 130 | { 131 | Client* cl = get_client_by_id(id); 132 | if (cl) 133 | { 134 | cl->ui.m_cursorcoord_set = true; 135 | cl->ui.m_cursorcoord.x = x; 136 | cl->ui.m_cursorcoord.y = y; 137 | cl->ui.m_cursorcoord.z = z; 138 | DFHack::Gui::setCursorCoords(x, y, z); 139 | } 140 | } 141 | 142 | void lua_set_client_viewcoord(client_long_id_t id, int x, int y, int z) 143 | { 144 | Client* cl = get_client_by_id(id); 145 | if (cl) 146 | { 147 | cl->ui.m_viewcoord_set = true; 148 | cl->ui.m_viewcoord.x = x; 149 | cl->ui.m_viewcoord.y = y; 150 | cl->ui.m_viewcoord.z = z; 151 | DFHack::Gui::setViewCoords(x, y, z); 152 | } 153 | } 154 | 155 | void lua_lock_dfplex_mutex() 156 | { 157 | dfplex_mutex.lock(); 158 | } 159 | 160 | void lua_unlock_dfplex_mutex() 161 | { 162 | dfplex_mutex.unlock(); 163 | } 164 | 165 | int lua_get_client_viewcoord(lua_State* L) 166 | { 167 | int id = lua_tointeger(L, -1); 168 | lua_pop(L, 1); 169 | Client* cl = get_client_by_id(id); 170 | 171 | int x = -30000, y = -30000, z = -30000; 172 | 173 | if (cl) 174 | { 175 | if (cl->ui.m_viewcoord_set) 176 | { 177 | x = cl->ui.m_viewcoord.x; 178 | y = cl->ui.m_viewcoord.y; 179 | z = cl->ui.m_viewcoord.z; 180 | } 181 | } 182 | 183 | lua_pushinteger(L, x); 184 | lua_pushinteger(L, y); 185 | lua_pushinteger(L, z); 186 | 187 | return 3; 188 | } 189 | 190 | int lua_get_current_menu_id(lua_State* L) 191 | { 192 | std::string s { get_current_menu_id() }; 193 | lua_pushstring(L, s.c_str()); 194 | return 1; 195 | } 196 | 197 | int lua_register_cb_post_state_restore(lua_State* L) 198 | { 199 | // pop & store callback function in registry. 200 | int index = lua_store_reg(L); 201 | 202 | DFPlex::add_cb_post_state_restore( 203 | [L, index](Client* cl) 204 | { 205 | client_long_id_t id = (cl) 206 | ? cl->id->long_id 207 | : 0; 208 | 209 | // push callback function onto stack 210 | lua_load_reg(L, index); // +1 211 | 212 | // push client index onto stack (arg) 213 | lua_pushinteger(L, id); // +1 214 | 215 | // call callback function. 216 | lua_call(L, 1, 0); // -2 217 | } 218 | ); 219 | 220 | // return value 221 | lua_pushnil(L); 222 | return 1; 223 | } 224 | } 225 | 226 | #undef DFHACK_LUA_FUNCTION 227 | #define DFHACK_LUA_FUNCTION(name) { #name, df::wrap_function(lua_##name, true) } 228 | 229 | #undef DFHACK_LUA_COMMAND 230 | #define DFHACK_LUA_COMMAND(name) { #name, lua_##name } 231 | 232 | DFHACK_PLUGIN_LUA_FUNCTIONS { 233 | DFHACK_LUA_FUNCTION(get_client_count), 234 | DFHACK_LUA_FUNCTION(get_client_id_by_index), 235 | DFHACK_LUA_FUNCTION(set_client_cursorcoord), 236 | DFHACK_LUA_FUNCTION(set_client_viewcoord), 237 | DFHACK_LUA_FUNCTION(lock_dfplex_mutex), 238 | DFHACK_LUA_FUNCTION(unlock_dfplex_mutex), 239 | DFHACK_LUA_END 240 | }; 241 | 242 | DFHACK_PLUGIN_LUA_COMMANDS { 243 | DFHACK_LUA_COMMAND(get_current_menu_id), 244 | DFHACK_LUA_COMMAND(get_client_nick), 245 | DFHACK_LUA_COMMAND(register_cb_post_state_restore), 246 | DFHACK_LUA_COMMAND(get_client_cursorcoord), 247 | DFHACK_LUA_COMMAND(get_client_viewcoord), 248 | DFHACK_LUA_END 249 | }; -------------------------------------------------------------------------------- /server/parse_config.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #include "parse_config.hpp" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | std::vector parse_config_file(const std::string& path_to_raw) 14 | { 15 | std::ifstream f(path_to_raw); 16 | std::vector symbols; 17 | if (!f.is_open()) { 18 | std::cerr << "Dwarfplex failed to open config file, skipping." << std::endl; 19 | return symbols; 20 | } 21 | 22 | std::string line; 23 | while(getline(f, line)) 24 | { 25 | size_t offset = 0; 26 | while (true) 27 | { 28 | size_t seek = line.find("[", offset); 29 | if (seek != std::string::npos) 30 | { 31 | offset = line.find("]", offset); 32 | if (offset == std::string::npos) 33 | { 34 | break; 35 | } 36 | 37 | // to parse interface.txt 38 | if (offset == line.length() - 3 && line.at(line.length() - 2) == ']') 39 | { 40 | offset++; 41 | } 42 | 43 | // found [ and ] tokens, now find : 44 | size_t sep_index = seek; 45 | bool set_op = false; 46 | symbols.emplace_back(); 47 | config_symbol& symbol = symbols.back(); 48 | while (sep_index < offset) 49 | { 50 | size_t start_index = sep_index + 1; 51 | 52 | // to parse interface.txt 53 | size_t add_search = 0; 54 | if (set_op && symbol.op == "KEY") add_search = 1; 55 | 56 | sep_index = line.find(":", start_index + add_search); 57 | if (sep_index == std::string::npos) 58 | { 59 | // last match is ':' 60 | sep_index = offset; 61 | } 62 | 63 | std::string sub = line.substr(start_index, sep_index - start_index); 64 | 65 | if (set_op) 66 | { 67 | symbol.args.emplace_back(std::move(sub)); 68 | } 69 | else 70 | { 71 | symbol.op = std::move(sub); 72 | set_op = true; 73 | } 74 | } 75 | } 76 | else 77 | { 78 | break; 79 | } 80 | } 81 | } 82 | return symbols; 83 | } 84 | 85 | std::ostream& operator<<(std::ostream& out, const config_symbol& match) 86 | { 87 | out << "[" << match.op; 88 | for (const std::string& arg : match.args) 89 | { 90 | out << ":" << arg; 91 | } 92 | out << "]"; 93 | return out; 94 | } -------------------------------------------------------------------------------- /server/parse_config.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | struct config_symbol 13 | { 14 | std::string op; 15 | std::vector args; 16 | }; 17 | 18 | std::ostream& operator<<(std::ostream& a, const config_symbol& match); 19 | 20 | std::vector parse_config_file(const std::string& path_to_raw); -------------------------------------------------------------------------------- /server/screenbuf.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "Client.hpp" 10 | 11 | extern screenbuf_t screenbuf; 12 | 13 | void hook_renderer(); 14 | 15 | void unhook_renderer(); 16 | 17 | void scrape_screenbuf(Client* cl); 18 | 19 | void transfer_screenbuf_client(Client* client); 20 | 21 | void transfer_screenbuf_to_all(); 22 | 23 | void set_size(int32_t w, int32_t h); 24 | 25 | void perform_render(); 26 | 27 | void restore_size(); -------------------------------------------------------------------------------- /server/server.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #include "dfplex.hpp" 8 | #include "server.hpp" 9 | #include "config.hpp" 10 | #include "serverlog.hpp" 11 | #include "DFHackVersion.h" 12 | #include "Core.h" 13 | #include "tinythread.h" 14 | 15 | #include 16 | 17 | #include "config.hpp" 18 | #include "dfplex.hpp" 19 | #include "input.hpp" 20 | 21 | #include "MemAccess.h" 22 | #include "Console.h" 23 | #include "modules/World.h" 24 | #include "df/global_objects.h" 25 | #include "df/graphic.h" 26 | using df::global::gps; 27 | 28 | static unsigned char buf[0x100000]; 29 | 30 | static std::ostream* out; 31 | 32 | std::set clients; 33 | 34 | class logbuf : public std::stringbuf { 35 | public: 36 | logbuf() : std::stringbuf() 37 | { } 38 | int sync() 39 | { 40 | std::string o = this->str(); 41 | size_t i = -1; 42 | // remove empty lines. 43 | while ((i = o.find("\n\n")) != std::string::npos) { 44 | o.replace(i, 2, "\n"); 45 | } 46 | // Remove uninformative [application] 47 | while ((i = o.find("[application]")) != std::string::npos) { 48 | o.replace(i, 13, "[DFPLEX]"); 49 | } 50 | 51 | // color warnings and errors 52 | const bool err = (o.find("ERROR") != std::string::npos); 53 | 54 | if (err) 55 | { 56 | DFHack::Core::printerr("%s", o.c_str()); 57 | } 58 | else 59 | { 60 | DFHack::Core::print("%s", o.c_str()); 61 | } 62 | str(""); 63 | return 0; 64 | } 65 | }; 66 | 67 | // TODO: migrate to dfplex.cpp? 68 | size_t get_client_count() 69 | { 70 | return clients.size(); 71 | } 72 | 73 | void remove_client(Client* cl) 74 | { 75 | ClientUpdateInfo info; 76 | info.is_multiplex = false; 77 | info.on_destroy = true; 78 | if (cl) 79 | { 80 | for (auto iter = clients.begin(); iter != clients.end(); ++iter) 81 | { 82 | if (*iter == cl) 83 | { 84 | if ((*iter)->update_cb) 85 | { 86 | (*iter)->update_cb(*iter, info); 87 | } 88 | delete *iter; 89 | clients.erase(iter); 90 | break; 91 | } 92 | } 93 | } 94 | } 95 | 96 | Client* add_client() 97 | { 98 | Client* cl = *clients.emplace(new Client()).first; 99 | 100 | DFPlex::log_message("A new client has joined."); 101 | 102 | // assign identity 103 | uint64_t id = 1; 104 | cl->id->long_id = id++; 105 | 106 | // clear screen 107 | memset(cl->sc, 0, sizeof(cl->sc)); 108 | 109 | return cl; 110 | } 111 | 112 | Client* add_client(client_update_cb&& cb) 113 | { 114 | Client* cl = add_client(); 115 | cl->update_cb = std::move(cb); 116 | return cl; 117 | } 118 | 119 | Client* get_client(int32_t n) 120 | { 121 | if (n < 0) return nullptr; 122 | 123 | auto it = clients.begin(); 124 | for (size_t i = 0; i < static_cast(n); ++i) 125 | { 126 | if (it == clients.end()) return nullptr; 127 | it++; 128 | } 129 | if (it == clients.end()) return nullptr; 130 | assert(*it); 131 | return *it; 132 | } 133 | 134 | Client* get_client(const ClientIdentity* id) 135 | { 136 | if (!id) return nullptr; 137 | auto it = clients.begin(); 138 | for (size_t i = 0; true; ++i) 139 | { 140 | if (it == clients.end()) return nullptr; 141 | if ((*it)->id.get() == id) return *it; 142 | it++; 143 | } 144 | 145 | // paranoia 146 | return nullptr; 147 | } 148 | 149 | Client* get_client_by_id(client_long_id_t long_id) 150 | { 151 | auto it = clients.begin(); 152 | for (size_t i = 0; true; ++i) 153 | { 154 | if (it == clients.end()) return nullptr; 155 | if ((*it)->id->long_id == long_id) return *it; 156 | it++; 157 | } 158 | 159 | // paranoia 160 | return nullptr; 161 | } 162 | 163 | int get_client_index(const ClientIdentity* id) 164 | { 165 | if (!id) return -1; 166 | auto it = clients.begin(); 167 | for (size_t i = 0; true; ++i) 168 | { 169 | if (it == clients.end()) return -1; 170 | if ((*it)->id.get() == id) return i; 171 | it++; 172 | } 173 | 174 | // paranoia 175 | return -1; 176 | } 177 | 178 | std::string str(std::string s) 179 | { 180 | return "\"" + s + "\""; 181 | } 182 | 183 | #define STATUS_ROUTE "/api/status.json" 184 | std::string status_json() 185 | { 186 | std::stringstream json; 187 | int active_players = clients.size(); 188 | std::string current_player = ""; 189 | int32_t time_left = -1; 190 | bool is_somebody_playing = active_players > 0; 191 | 192 | json << std::boolalpha << "{" 193 | << " \"active_players\": " << active_players 194 | << ", \"current_player\": " << str(current_player) 195 | << ", \"time_left\": " << -1 196 | << ", \"is_somebody_playing\": " << is_somebody_playing 197 | << ", \"using_ingame_time\": " << false 198 | << ", \"dfhack_version\": " << str(DFHACK_VERSION) 199 | << ", \"webfort_version\": " << str(WF_VERSION) 200 | << " }\n"; 201 | 202 | return json.str(); 203 | } 204 | 205 | // cl is not const b/c we modify the "modified" flag per-tile for delta encoding. 206 | size_t tock(Client* cl) 207 | { 208 | if (!cl) return 0; 209 | 210 | unsigned char *b = buf; 211 | // [0] msgtype 212 | *(b++) = 110; 213 | 214 | uint8_t client_count = clients.size(); 215 | // [1] # of connected clients. 216 | *(b++) = client_count; 217 | 218 | // [2] is active 219 | *(b++) = 1; 220 | 221 | // [3-6] load (sum of frames) -- REMOVED 222 | const int32_t load = 0; 223 | memcpy(b, &load, sizeof(load)); 224 | b += sizeof(load); 225 | 226 | // [7-8] game dimensions 227 | *(b++) = cl->dimx; 228 | *(b++) = cl->dimy; 229 | 230 | // dfplex sent the active player's nickname here. 231 | // this is now used to send info_message. 232 | // [9] (length info_message.) 233 | uint8_t info_len = std::min(0xff, cl->info_message.length() + 1); 234 | *(b++) = info_len; 235 | 236 | // [10-M] info message. 237 | memcpy(b, cl->info_message.c_str(), info_len); 238 | b += info_len; 239 | 240 | // [?] (length debug info) 241 | if (cl->m_debug_enabled) 242 | { 243 | uint16_t debug_info_len = std::min(0xffff, cl->m_debug_info.length() + 1); 244 | *(b++) = debug_info_len & 0x00ff; 245 | *(b++) = (debug_info_len & 0xff00) >> 8; 246 | 247 | // [?] info message. 248 | memcpy(b, cl->m_debug_info.c_str(), debug_info_len); 249 | b += debug_info_len; 250 | } 251 | else 252 | { 253 | // send no debug info. 254 | *(b++) = 0; 255 | *(b++) = 0; 256 | } 257 | 258 | // [M-N] Changed tiles. 5 bytes per tile 259 | for (int y = 0; y < cl->dimy; y++) { 260 | for (int x = 0; x < cl->dimx; x++) { 261 | const int tile = x * cl->dimy + y; 262 | if (tile >= 256 * 256) break; 263 | if (b >= buf + sizeof(buf) - 0x400) 264 | { 265 | return 0; 266 | } 267 | ClientTile& ct = cl->sc[tile]; // client's tile 268 | if (ct.modified) 269 | { 270 | ct.modified = false; 271 | *(b++) = x; 272 | *(b++) = y; 273 | *(b++) = ct.pen.ch; 274 | *(b++) = ct.pen.bg | (ct.is_text << 6) | (ct.is_overworld << 7); 275 | 276 | int bold = ct.pen.bold << 3; 277 | int fg = (ct.pen.fg + bold) & 0x0f; 278 | 279 | *(b++) = fg; 280 | } 281 | } 282 | } 283 | return (b - buf); 284 | } 285 | 286 | size_t on_message(Client* cl, const unsigned char* mdata, size_t msz) 287 | { 288 | if (mdata[0] == 117 && msz == 3) { // ResizeEvent 289 | cl->desired_dimx = mdata[1]; 290 | cl->desired_dimy = mdata[2]; 291 | } else if (mdata[0] == 111 && msz == 4) { // KeyEvent 292 | 293 | if (mdata[1]){ 294 | KeyEvent match; 295 | match.type = EventType::type_key; 296 | match.mod = mdata[3]; 297 | match.unicode = mdata[2];// retain unicode information 298 | match.key = mapInputCodeToSDL(mdata[1]); 299 | // does SDL1.2 have this function? 300 | // match.scancode = SDL_GetScancodeFromKey(key) 301 | // add to queue 302 | cl->keyqueue.push(match); 303 | } else if (mdata[2]) 304 | { 305 | KeyEvent match; 306 | match.mod = 0; // unicode must not have modifiers. 307 | match.type = EventType::type_unicode; 308 | match.unicode = mdata[2]; 309 | cl->keyqueue.push(match); 310 | } 311 | } else if (mdata[0] == 115) { // refreshScreen 312 | // in particular, this sets the modified flag to 0. 313 | memset(cl->sc, 0, sizeof(cl->sc)); 314 | } else { 315 | return tock(cl); 316 | } 317 | 318 | return 0; 319 | } 320 | 321 | #ifdef DFPLEX_IXW 322 | #include 323 | #include 324 | 325 | using namespace ix; 326 | 327 | typedef std::shared_ptr conn_hdl_t; 328 | typedef std::shared_ptr WebSocketPtr; 329 | 330 | std::map> conn_map; 331 | 332 | Client* get_client(conn_hdl_t connection) 333 | { 334 | auto iter = conn_map.find(connection); 335 | if (iter == conn_map.end()) return nullptr; 336 | return get_client(iter->second.get()); 337 | } 338 | 339 | std::string get_subprotocol(WebSocketPtr webSocket) 340 | { 341 | const std::vector& protocols = webSocket->getSubProtocols(); 342 | if (protocols.empty()) return ""; 343 | return protocols.front(); 344 | } 345 | 346 | void on_open_ix(const ix::WebSocketMessagePtr& msg, conn_hdl_t connection, WebSocketPtr webSocket) 347 | { 348 | tthread::lock_guard guard(dfplex_mutex); 349 | 350 | if (get_subprotocol(webSocket) == WF_INVALID) { 351 | webSocket->close(4000, "Invalid version, expected '" WF_VERSION "'."); 352 | return; 353 | } 354 | 355 | if (clients.size() >= MAX_CLIENTS && MAX_CLIENTS != 0) { 356 | webSocket->close(4001, "Server is full."); 357 | return; 358 | } 359 | 360 | // TODO: get address from ixwebsockets. 361 | std::string addr = "???"; 362 | 363 | if (std::find(g_ban_list.begin(), g_ban_list.end(), addr) != g_ban_list.end()) 364 | { 365 | webSocket->close(4003, "Banned."); 366 | return; 367 | } 368 | 369 | // FIXME: parse URL for these 370 | std::string nick = ""; 371 | std::string user_secret = ""; 372 | 373 | Client* cl = add_client(); 374 | cl->id->is_admin = (user_secret == SECRET); 375 | cl->id->addr = addr; 376 | cl->id->nick = nick; 377 | 378 | DFPlex::log_message(" Client addr: \"" + addr + "\""); 379 | if (cl->id->is_admin) 380 | { 381 | DFPlex::log_message(" Client is admin."); 382 | } 383 | if (cl->id->nick.length()) 384 | { 385 | DFPlex::log_message(" Client nick: " + nick); 386 | } 387 | 388 | conn_map[connection] = cl->id; 389 | } 390 | 391 | void on_close_ix(const ix::WebSocketMessagePtr& msg, conn_hdl_t connection, WebSocketPtr webSocket) 392 | { 393 | tthread::lock_guard guard(dfplex_mutex); 394 | 395 | auto iter = conn_map.find(connection); 396 | if (iter != conn_map.end()) 397 | { 398 | Client* cl = get_client(iter->second.get()); 399 | conn_map.erase(iter); 400 | remove_client(cl); 401 | } 402 | } 403 | 404 | void on_message_ix(const ix::WebSocketMessagePtr& msg, conn_hdl_t connection, WebSocketPtr webSocket) 405 | { 406 | tthread::lock_guard guard(dfplex_mutex); 407 | 408 | Client* cl = get_client(connection); 409 | 410 | if (cl) 411 | { 412 | size_t response_size = on_message( 413 | cl, 414 | reinterpret_cast(msg->str.c_str()), 415 | msg->str.length() 416 | ); 417 | 418 | // send response 419 | if (response_size) 420 | { 421 | webSocket->send( 422 | std::string(reinterpret_cast(buf), response_size), 423 | true // binary mode 424 | ); 425 | } 426 | } 427 | } 428 | 429 | void wsthreadmain(void *i_raw_out) 430 | { 431 | logbuf lb; 432 | std::ostream logstream(&lb); 433 | 434 | ix::initNetSystem(); 435 | 436 | ix::WebSocketServer server(PORT, "0.0.0.0"); 437 | 438 | server.setOnConnectionCallback( 439 | [&server](std::shared_ptr webSocket, 440 | std::shared_ptr connectionState) 441 | { 442 | webSocket->setOnMessageCallback( 443 | [webSocket, connectionState, &server](const ix::WebSocketMessagePtr& msg) 444 | { 445 | if (msg->type == ix::WebSocketMessageType::Open) 446 | { 447 | DFHack::Core::print("New connection\n"); 448 | on_open_ix(msg, connectionState, webSocket); 449 | } 450 | else if (msg->type == ix::WebSocketMessageType::Message) 451 | { 452 | on_message_ix(msg, connectionState, webSocket); 453 | } 454 | else if (msg->type == ix::WebSocketMessageType::Close) 455 | { 456 | on_close_ix(msg, connectionState, webSocket); 457 | } 458 | } 459 | ); 460 | }); 461 | 462 | auto res = server.listen(); 463 | if (!res.first) 464 | { 465 | DFHack::Core::printerr("Websocket server failed to start on port %d. (Is the port in use?)\n", PORT); 466 | } 467 | DFHack::Core::printerr("Websocket server starting on port %d using IXWebSocket.", PORT); 468 | 469 | // Run the server in the background. Server can be stoped by calling server.stop() 470 | server.start(); 471 | 472 | // Block until server.stop() is called. 473 | server.wait(); 474 | 475 | ix::uninitNetSystem(); 476 | } 477 | #endif 478 | 479 | #ifdef DFPLEX_WEBSOCKETPP 480 | #include 481 | #include 482 | 483 | namespace ws = websocketpp; 484 | // FIXME: use unique_ptr or the boost equivalent 485 | typedef ws::connection_hdl conn_hdl; 486 | 487 | static std::owner_less conn_lt; 488 | inline bool operator==(const conn_hdl& p, const conn_hdl& q) 489 | { 490 | return (!conn_lt(p, q) && !conn_lt(q, p)); 491 | } 492 | inline bool operator!=(const conn_hdl& p, const conn_hdl& q) 493 | { 494 | return conn_lt(p, q) || conn_lt(q, p); 495 | } 496 | 497 | namespace lib = websocketpp::lib; 498 | using websocketpp::lib::placeholders::_1; 499 | using websocketpp::lib::placeholders::_2; 500 | using websocketpp::lib::bind; 501 | 502 | typedef ws::server server; 503 | 504 | typedef server::message_ptr message_ptr; 505 | 506 | static conn_hdl null_conn = std::weak_ptr(); 507 | 508 | std::map> conn_map; 509 | 510 | class appbuf : public std::stringbuf { 511 | public: 512 | appbuf(server* i_srv) : std::stringbuf() 513 | { 514 | srv = i_srv; 515 | } 516 | int sync() 517 | { 518 | srv->get_alog().write(ws::log::alevel::app, this->str()); 519 | str(""); 520 | return 0; 521 | } 522 | private: 523 | server* srv; 524 | }; 525 | 526 | Client* get_client(conn_hdl hdl) 527 | { 528 | auto iter = conn_map.find(hdl); 529 | if (iter == conn_map.end()) return nullptr; 530 | return iter->second; 531 | } 532 | 533 | void on_http_ws(server* s, conn_hdl hdl) 534 | { 535 | server::connection_ptr con = s->get_con_from_hdl(hdl); 536 | std::stringstream output; 537 | std::string route = con->get_resource(); 538 | if (route == STATUS_ROUTE) { 539 | con->set_status(websocketpp::http::status_code::ok); 540 | con->replace_header("Content-Type", "application/json"); 541 | con->replace_header("Access-Control-Allow-Origin", "*"); 542 | con->set_body(status_json()); 543 | } 544 | } 545 | 546 | bool validate_open_ws(server* s, conn_hdl hdl) 547 | { 548 | auto raw_conn = s->get_con_from_hdl(hdl); 549 | 550 | std::vector protos = raw_conn->get_requested_subprotocols(); 551 | if (std::find(protos.begin(), protos.end(), WF_VERSION) != protos.end()) { 552 | raw_conn->select_subprotocol(WF_VERSION); 553 | } else if (std::find(protos.begin(), protos.end(), WF_INVALID) != protos.end()) { 554 | raw_conn->select_subprotocol(WF_INVALID); 555 | } 556 | 557 | return true; 558 | } 559 | 560 | void on_open_ws(server* s, conn_hdl hdl) 561 | { 562 | tthread::lock_guard guard(dfplex_mutex); 563 | if (s->get_con_from_hdl(hdl)->get_subprotocol() == WF_INVALID) { 564 | s->close(hdl, 4000, "Invalid version, expected '" WF_VERSION "'."); 565 | return; 566 | } 567 | 568 | if (clients.size() >= MAX_CLIENTS && MAX_CLIENTS != 0) { 569 | s->close(hdl, 4001, "Server is full."); 570 | return; 571 | } 572 | 573 | auto raw_conn = s->get_con_from_hdl(hdl); 574 | std::string addr = raw_conn->get_raw_socket().remote_endpoint().address().to_string(); 575 | 576 | if (std::find(g_ban_list.begin(), g_ban_list.end(), addr) != g_ban_list.end()) 577 | { 578 | s->close(hdl, 4003, "Banned."); 579 | return; 580 | } 581 | 582 | auto path = split(raw_conn->get_resource().substr(1).c_str(), '/'); 583 | std::string nick = path[0]; 584 | std::string user_secret = (path.size() > 1) ? path[1] : ""; 585 | 586 | Client* cl = add_client(); 587 | cl->id->is_admin = (user_secret == SECRET); 588 | cl->id->addr = addr; 589 | cl->id->nick = nick; 590 | 591 | DFPlex::log_message(" Client addr: \"" + addr + "\""); 592 | if (cl->id->is_admin) 593 | { 594 | DFPlex::log_message(" Client is admin."); 595 | } 596 | if (cl->id->nick.length()) 597 | { 598 | DFPlex::log_message(" Client nick: " + nick); 599 | } 600 | 601 | conn_map[hdl] = cl; 602 | } 603 | 604 | void on_close_ws(server* s, conn_hdl c) 605 | { 606 | tthread::lock_guard guard(dfplex_mutex); 607 | 608 | remove_client(get_client(c)); 609 | 610 | conn_map.erase(c); 611 | } 612 | 613 | void on_message_ws(server* s, conn_hdl hdl, message_ptr msg) 614 | { 615 | tthread::lock_guard guard(dfplex_mutex); 616 | auto str = msg->get_payload(); 617 | const unsigned char *mdata = (const unsigned char*) str.c_str(); 618 | int msz = str.size(); 619 | 620 | size_t response = on_message(get_client(hdl), mdata, msz); 621 | if (response) 622 | { 623 | s->send(hdl, (const void*) buf, response, ws::frame::opcode::binary); 624 | } 625 | } 626 | 627 | void on_init_ws(conn_hdl hdl, boost::asio::ip::tcp::socket & s) 628 | { 629 | s.set_option(boost::asio::ip::tcp::no_delay(true)); 630 | } 631 | 632 | void wsthreadmain(void *i_raw_out) 633 | { 634 | logbuf lb; 635 | std::ostream logstream(&lb); 636 | 637 | server srv; 638 | 639 | appbuf abuf(&srv); 640 | std::ostream astream(&abuf); 641 | out = &astream; 642 | 643 | try { 644 | srv.clear_access_channels(ws::log::alevel::all); 645 | srv.set_access_channels( 646 | ws::log::alevel::connect | 647 | ws::log::alevel::disconnect | 648 | ws::log::alevel::app 649 | ); 650 | srv.set_error_channels( 651 | ws::log::elevel::info | 652 | ws::log::elevel::warn | 653 | ws::log::elevel::rerror | 654 | ws::log::elevel::fatal 655 | ); 656 | srv.init_asio(); 657 | 658 | srv.get_alog().set_ostream(&logstream); 659 | 660 | srv.set_socket_init_handler(&on_init_ws); 661 | srv.set_http_handler(bind(&on_http_ws, &srv, ::_1)); 662 | srv.set_validate_handler(bind(&validate_open_ws, &srv, ::_1)); 663 | srv.set_open_handler(bind(&on_open_ws, &srv, ::_1)); 664 | srv.set_message_handler(bind(&on_message_ws, &srv, ::_1, ::_2)); 665 | srv.set_close_handler(bind(&on_close_ws, &srv, ::_1)); 666 | // See https://stackoverflow.com/a/548912 667 | // Prevent segfaults when restarting dwarf fortress, if the port was 668 | // not released properly on exit 669 | srv.set_reuse_addr(true); 670 | lib::error_code ec; 671 | 672 | // FIXME: this sometimes segfaults. 673 | srv.listen(PORT, ec); 674 | if (ec) { 675 | *out << "ERROR: Unable to start Dwarfplex on port " << PORT 676 | << ", is it being used somehere else?" << std::endl; 677 | return; 678 | } 679 | 680 | srv.start_accept(); 681 | *out << "Dwarfplex websocket serving on " << PORT << " using websocketpp." << std::endl; 682 | *out << "(Do not connect to this in your browser.) " << std::endl; 683 | } catch (const std::exception & e) { 684 | *out << "Dwarfplex failed to start: " << e.what() << std::endl; 685 | } catch (lib::error_code e) { 686 | *out << "Dwarfplex failed to start: " << e.message() << std::endl; 687 | } catch (...) { 688 | *out << "Dwarfplex failed to start: other exception" << std::endl; 689 | } 690 | 691 | try { 692 | srv.run(); 693 | } catch (const std::exception & e) { 694 | *out << "ERROR: std::exception caught: " << e.what() << std::endl; 695 | } catch (lib::error_code e) { 696 | *out << "ERROR: ws++ exception caught: " << e.message() << std::endl; 697 | } catch (...) { 698 | *out << "ERROR: Unknown exception caught:" << std::endl; 699 | } 700 | return; 701 | } 702 | 703 | #endif -------------------------------------------------------------------------------- /server/server.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #pragma once 8 | 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "Client.hpp" 15 | 16 | extern std::set clients; 17 | 18 | // used to launch server by dfplex. 19 | void wsthreadmain(void*); 20 | 21 | -------------------------------------------------------------------------------- /server/serverlog.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | 7 | #include "serverlog.hpp" 8 | #include 9 | 10 | namespace DFPlex 11 | { 12 | 13 | static std::fstream logfile; 14 | static bool is_open = false; 15 | 16 | bool log_begin(const std::string& path) 17 | { 18 | if (!is_open) 19 | { 20 | logfile.open(path, std::ios_base::app); 21 | 22 | if (!logfile.good()) return true; 23 | 24 | is_open = true; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | void log_end() 31 | { 32 | if (is_open) 33 | { 34 | logfile.close(); 35 | is_open = false; 36 | } 37 | } 38 | 39 | void log_message(const std::string& message) 40 | { 41 | if (is_open) 42 | { 43 | logfile << message << std::endl; 44 | logfile.flush(); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /server/serverlog.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2020 white-rabbit, ISC license 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | namespace DFPlex 11 | { 12 | 13 | // returns true if error. 14 | bool log_begin(const std::string& path); 15 | 16 | void log_end(); 17 | void log_message(const std::string& message); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /server/state.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 5 | * Copyright (c) 2014 mifki, ISC license. 6 | * Copyright (c) 2020 white-rabbit, ISC license 7 | */ 8 | 9 | #include 10 | 11 | struct Client; 12 | 13 | enum class RestoreResult 14 | { 15 | SUCCESS, 16 | FAIL, 17 | ABORT_PLEX, // failed so bad we should switch to uniplex mode 18 | }; 19 | extern std::string restore_state_error; 20 | RestoreResult restore_state(Client* client); 21 | 22 | void deferred_state_restore(Client*); 23 | 24 | void capture_post_state(Client* client); -------------------------------------------------------------------------------- /server/state_cb.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | /* 8 | This file contains helper functions which can be attached as callbacks 9 | to RestoreKeys to affect how state is restored. 10 | */ 11 | 12 | #include "Client.hpp" 13 | #include "command.hpp" 14 | #include "config.hpp" 15 | #include "dfplex.hpp" 16 | #include "hackutil.hpp" 17 | #include "input.hpp" 18 | #include "keymap.hpp" 19 | #include "screenbuf.hpp" 20 | #include "server.hpp" 21 | #include "state_cb.hpp" 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include "tinythread.h" 31 | #include "MemAccess.h" 32 | #include "modules/EventManager.h" 33 | #include "modules/Gui.h" 34 | #include "modules/MapCache.h" 35 | #include "modules/Screen.h" 36 | #include "modules/Units.h" 37 | #include "modules/World.h" 38 | #include "PluginManager.h" 39 | 40 | #include "df/announcement_flags.h" 41 | #include "df/announcements.h" 42 | #include "df/building.h" 43 | #include "df/buildings_other_id.h" 44 | #include "df/d_init.h" 45 | #include "df/enabler.h" 46 | #include "df/graphic.h" 47 | #include "df/historical_figure.h" 48 | #include "df/interfacest.h" 49 | #include "df/items_other_id.h" 50 | #include "df/renderer.h" 51 | #include "df/report.h" 52 | #include "df/squad_position.h" 53 | #include "df/squad.h" 54 | #include "df/ui_build_selector.h" 55 | #include "df/ui_sidebar_menus.h" 56 | #include "df/ui_unit_view_mode.h" 57 | #include "df/unit.h" 58 | #include "df/viewscreen_announcelistst.h" 59 | #include "df/viewscreen_createquotast.h" 60 | #include "df/viewscreen_customize_unitst.h" 61 | #include "df/viewscreen_civlistst.h" 62 | #include "df/viewscreen_dwarfmodest.h" 63 | #include "df/viewscreen_loadgamest.h" 64 | #include "df/viewscreen_meetingst.h" 65 | #include "df/viewscreen_movieplayerst.h" 66 | #include "df/viewscreen_optionst.h" 67 | #include "df/viewscreen_reportlistst.h" 68 | #include "df/viewscreen_textviewerst.h" 69 | #include "df/viewscreen_tradelistst.h" 70 | #include "df/viewscreen_titlest.h" 71 | #include "df/viewscreen_unitst.h" 72 | #include "df/viewscreen.h" 73 | #include "df/world.h" 74 | 75 | using namespace DFHack; 76 | using namespace df::enums; 77 | using df::global::world; 78 | using std::string; 79 | using std::vector; 80 | using df::global::gps; 81 | using df::global::init; 82 | 83 | using df::global::enabler; 84 | using df::renderer; 85 | 86 | int suppress_sidebar_refresh(Client* client) 87 | { 88 | client->ui.m_suppress_sidebar_refresh = true; 89 | return 0; 90 | } 91 | 92 | // helper function for restore_state. 93 | int restore_cursor(Client* client) 94 | { 95 | UIState& ui = client->ui; 96 | 97 | // sets viewcoords 98 | if (ui.m_viewcoord_set) 99 | { 100 | Gui::setViewCoords(ui.m_viewcoord.x, ui.m_viewcoord.y, ui.m_viewcoord.z); 101 | } 102 | if (ui.m_following_client && ui.m_client_screen_cycle.get()) 103 | { 104 | Client* dst = get_client(ui.m_client_screen_cycle.get()); 105 | if (dst) 106 | { 107 | center_view_on_coord(dst->ui.m_stored_viewcoord.operator+( 108 | {dst->ui.m_map_dimx/2, dst->ui.m_map_dimy/2, 0}) 109 | ); 110 | } 111 | } 112 | 113 | // sets cursor coords 114 | if (ui.m_cursorcoord_set) 115 | { 116 | Gui::setCursorCoords(ui.m_cursorcoord.x, ui.m_cursorcoord.y, ui.m_cursorcoord.z); 117 | } 118 | if (ui.m_designationcoord_set) 119 | { 120 | Gui::setDesignationCoords(ui.m_designationcoord.x, ui.m_designationcoord.y, ui.m_designationcoord.z); 121 | } 122 | if (ui.m_squadcoord_start_set) 123 | { 124 | df::global::ui->squads.rect_start.x = ui.m_squadcoord_start.x; 125 | df::global::ui->squads.rect_start.y = ui.m_squadcoord_start.y; 126 | df::global::ui->squads.rect_start.z = ui.m_squadcoord_start.z; 127 | } 128 | if (ui.m_burrowcoord_set) 129 | { 130 | df::global::ui->burrows.rect_start.x = ui.m_burrowcoord.x; 131 | df::global::ui->burrows.rect_start.y = ui.m_burrowcoord.y; 132 | df::global::ui->burrows.rect_start.z = ui.m_burrowcoord.z; 133 | } 134 | df::global::ui->burrows.brush_erasing = ui.m_brush_erasing; 135 | 136 | return 0; 137 | } 138 | 139 | restore_state_cb_t produce_restore_cb_restore_cursor() 140 | { 141 | Coord c; 142 | Gui::getCursorCoords(c.x, c.y, c.z); 143 | return [c](Client* client) -> int 144 | { 145 | Gui::setCursorCoords(c.x, c.y, c.z); 146 | return 0; 147 | }; 148 | } 149 | 150 | int restore_squads_state(Client* client) 151 | { 152 | UIState& ui = client->ui; 153 | 154 | auto& squads = df::global::ui->squads; 155 | auto& ui_squads = ui.m_squads; 156 | 157 | // need to clear this on entry. 158 | squads.in_kill_rect = false; 159 | squads.in_kill_order = false; 160 | squads.in_kill_list = false; 161 | squads.in_move_order = false; 162 | 163 | // selected individuals 164 | squads.in_select_indiv = ui_squads.in_select_indiv; 165 | 166 | // sanitize 167 | squads.indiv_selected.clear(); 168 | for (int32_t figure_id : ui_squads.indiv_selected) 169 | { 170 | df::historical_figure* figure = df::historical_figure::find(figure_id); 171 | if (!figure) continue; 172 | df::unit* unit = df::unit::find(figure->unit_id); 173 | if (!unit) continue; 174 | 175 | // ensure unit still part of a squad. 176 | bool found_squad = false; 177 | for (df::squad* squad : squads.list) 178 | { 179 | if (squad && squad->id == unit->military.squad_id) 180 | { 181 | found_squad = true; 182 | break; 183 | } 184 | } 185 | if (!found_squad) continue; 186 | 187 | // ensure unit is assigned to a squad position. 188 | df::squad* squad = df::squad::find(unit->military.squad_id); 189 | if (!squad) continue; 190 | 191 | bool found_position = false; 192 | for (df::squad_position* squad_position : squad->positions) 193 | { 194 | if (squad_position && squad_position->occupant == figure_id) 195 | { 196 | found_position = true; 197 | } 198 | } 199 | if (!found_position) continue; 200 | 201 | // we were unable to prove that this figure is not valid to select, 202 | // so we will allow this figure to remain selected. 203 | squads.indiv_selected.push_back(figure_id); 204 | } 205 | 206 | // selected squads 207 | for (size_t i = 0; i < squads.list.size() && i < squads.sel_squads.size(); ++i) 208 | { 209 | squads.sel_squads.at(i) = false; 210 | 211 | df::squad* squad = squads.list.at(i); 212 | if (!squad) continue; 213 | 214 | squads.sel_squads.at(i) = 215 | ( 216 | std::find(ui_squads.squad_selected.begin(), ui_squads.squad_selected.end(), squad->id) 217 | != ui_squads.squad_selected.end() 218 | ); 219 | } 220 | 221 | return 0; 222 | } 223 | 224 | // returns 1 on error. 225 | int restore_unit_view_state(Client* client) 226 | { 227 | using namespace df::enums::interface_key; 228 | 229 | df::viewscreen* vs; 230 | virtual_identity* id; 231 | (void)id; 232 | UPDATE_VS(vs, id); 233 | 234 | UIState& ui = client->ui; 235 | df::global::ui_unit_view_mode->value = ui.m_unit_view_mode; 236 | df::global::ui_sidebar_menus->show_combat = ui.m_show_combat; 237 | df::global::ui_sidebar_menus->show_labor = ui.m_show_labor; 238 | df::global::ui_sidebar_menus->show_misc = ui.m_show_misc; 239 | 240 | // set df::global::ui_selected_unit 241 | if (df::unit* unit = df::unit::find(ui.m_view_unit)) 242 | { 243 | // go to unit's position. 244 | Coord pos = unit->pos; 245 | Gui::setCursorCoords(pos.x, pos.y, pos.z); 246 | Gui::refreshSidebar(); 247 | 248 | // rapidly tap UNITVIEW_NEXT until the unit we desire is found. 249 | vs->feed_key(UNITVIEW_NEXT); 250 | int32_t unit_sel_start = *df::global::ui_selected_unit; 251 | bool success; 252 | while (true) 253 | { 254 | vs->feed_key(UNITVIEW_NEXT); 255 | if (df::unit* unit_selected = vector_get(world->units.active, *df::global::ui_selected_unit)) 256 | { 257 | if (unit_selected->id == unit->id) 258 | { 259 | success = true; 260 | break; 261 | } 262 | } 263 | if (unit_sel_start == *df::global::ui_selected_unit) 264 | { 265 | success = false; 266 | break; 267 | } 268 | } 269 | 270 | if (!success) 271 | { 272 | // return to the stored cursor position. 273 | restore_cursor(client); 274 | Gui::refreshSidebar(); 275 | ui.m_view_unit = -1; 276 | return 1; 277 | } 278 | else 279 | { 280 | ui.m_defer_restore_cursor = true; 281 | ui.m_suppress_sidebar_refresh = true; 282 | 283 | // restore labour menu position 284 | if (ui.m_unit_view_mode == df::ui_unit_view_mode::PrefLabor) 285 | { 286 | vs->feed_key(UNITVIEW_PRF); 287 | vs->feed_key(UNITVIEW_PRF_PROF); 288 | 289 | // set scroll position 290 | if (ui.m_view_unit_labor_submenu >= 0) 291 | { 292 | for (int32_t i = 0; i < ui.m_view_unit_labor_submenu; ++i) 293 | { 294 | vs->feed_key(SECONDSCROLL_DOWN); 295 | } 296 | vs->feed_key(SELECT); 297 | } 298 | for (int32_t i = 0; i < ui.m_view_unit_labor_scroll; ++i) 299 | { 300 | vs->feed_key(SECONDSCROLL_DOWN); 301 | } 302 | } 303 | return 0; 304 | } 305 | } 306 | return 0; 307 | } 308 | 309 | int refresh_sidebar(Client*) 310 | { 311 | Gui::refreshSidebar(); 312 | return 0; 313 | } -------------------------------------------------------------------------------- /server/state_cb.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 5 | * Copyright (c) 2020 white-rabbit, ISC license 6 | */ 7 | 8 | #include "Client.hpp" 9 | 10 | int suppress_sidebar_refresh(Client*); 11 | 12 | // restores cursor from the cursor coordinates at the time of calling this function. 13 | restore_state_cb_t produce_restore_cb_restore_cursor(); 14 | 15 | // restores cursor coordinates from the previous frame's coordinates 16 | int restore_cursor(Client*); 17 | 18 | int restore_squads_state(Client*); 19 | 20 | int restore_unit_view_state(Client*); 21 | 22 | int refresh_sidebar(Client*); -------------------------------------------------------------------------------- /server/staticserver.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #include "staticserver.hpp" 8 | #include "config.hpp" 9 | #include "dfplex.hpp" 10 | 11 | #include "Console.h" 12 | #include "Core.h" 13 | 14 | #ifndef _WIN32 15 | // this seems to cause build errors on windows. 16 | #define SO_REUSEPORT 17 | #endif 18 | 19 | #include 20 | 21 | using namespace DFHack; 22 | 23 | void init_static(void*) 24 | { 25 | using namespace httplib; 26 | 27 | Server svr; 28 | //Server server(STATICPORT, STATICDIR.c_str()); 29 | 30 | auto ret = svr.set_mount_point("/", STATICDIR.c_str()); 31 | svr.Get("/", [](const Request& req, Response& res) { 32 | res.set_redirect("dfplex.html"); 33 | }); 34 | 35 | svr.Get("/config-srv.js", [](const Request& req, Response& res) { 36 | std::stringstream ss; 37 | ss << "// This file is dynamically generated.\n"; 38 | ss << "config.port = '" << PORT << "';\n"; 39 | ss << "config.protocol = '" << WF_VERSION << "';\n"; 40 | res.set_content(ss.str().c_str(), "application/javascript"); 41 | }); 42 | 43 | if (!ret) 44 | { 45 | Core::printerr("[DFPLEX] Failed to serve static site files from \"%s\"\n", STATICDIR.c_str()); 46 | } 47 | else 48 | { 49 | Core::print("[DFPLEX] Static site server starting on port %d", STATICPORT); 50 | Core::print("[DFPLEX] Serving files from directory %s", STATICDIR.c_str()); 51 | Core::print("[DFPLEX] Connect to http://localhost:%d/dfplex.html in your browser.", STATICPORT); 52 | svr.listen("0.0.0.0", STATICPORT); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/staticserver.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Dwarfplex is based on Webfort, created by Vitaly Pronkin on 14/05/14. 3 | * Copyright (c) 2014 mifki, ISC license. 4 | * Copyright (c) 2020 white-rabbit, ISC license 5 | */ 6 | 7 | #pragma once 8 | 9 | void init_static(void*); -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | Parameters 2 | ========== 3 | 4 | dfplex.html supports a number of query parameters for configuration. Some 5 | of these really should be persistent settings but others are really only 6 | for debug purposes. here are all of them: 7 | 8 | | option | value | default | description | 9 | |-------------|---------------------|----------------------|-------------------------------------------| 10 | | `port` | any port number | 1234 | The port number of the websocket. | 11 | | `tiles` | an image in `art/` | `Spacefox_16x16.png` | The tileset to use. | 12 | | `text` | an image in `art/` | `ShizzleClean.png` | the tileset to use for ingame text. | 13 | | `show-fps` | a boolean | false | Whether or not to show the FPS counter. | 14 | | `hide-chat` | a boolean | false | Whether or not to hide the IRC side pane. | 15 | | `colors` | a file in `colors/` | default colorscheme | The colorscheme to use, in json format. | 16 | | `nick` | any string | random | The nickname to use | 17 | | `store` | a boolean | undefined | if true, store all current settings | 18 | 19 | A quick primer on query strings: 20 | 21 | Anything past a ? in a URL is a query string 22 | 23 | http:///dfplex.html?param=value 24 | 25 | here, the parameter `param` is being set to `value`. 26 | 27 | http:///dfplex.html?param 28 | 29 | If you don't give a value. it is assumed to be true. So here, 30 | `param` is true. 31 | 32 | You can also chain multiple parameters using & 33 | 34 | http:///dfplex.html?param1=1¶m2=2 35 | 36 | Here, `param1` is set to `1`, and `param2` is set to `2`. 37 | 38 | A real world example: 39 | 40 | http:///dfplex.html?nick=Urist&hide-chat&tiles=ShizzleClean.png 41 | 42 | Will set your `nick` to Urist, hide the chat pane, and set the tileset 43 | to `ShizzleClean.png`. 44 | 45 | Parameters can be stored into your browser's `localStorage`, where they can 46 | persist between sessions. for example, opening: 47 | 48 | http:///dfplex.html?nick=Urist&store 49 | 50 | will store the nick `Urist` and restore it such that 51 | 52 | http:///dfplex.html 53 | 54 | will also have the the nick `Urist`. ATM, storage can only be reset to 55 | defaults by using the console command: 56 | 57 | localStorage.clear() 58 | 59 | -------------------------------------------------------------------------------- /static/art/CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | These tilesets were contributed by Dragoon209, and are (c) their 2 | respective creators. 3 | -------------------------------------------------------------------------------- /static/art/Curses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/Curses.png -------------------------------------------------------------------------------- /static/art/Ironhand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/Ironhand.png -------------------------------------------------------------------------------- /static/art/Ironhand_Square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/Ironhand_Square.png -------------------------------------------------------------------------------- /static/art/Mayday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/Mayday.png -------------------------------------------------------------------------------- /static/art/Obsidian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/Obsidian.png -------------------------------------------------------------------------------- /static/art/Phoebus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/Phoebus.png -------------------------------------------------------------------------------- /static/art/ShizzleClean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/ShizzleClean.png -------------------------------------------------------------------------------- /static/art/SimpleMood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/SimpleMood.png -------------------------------------------------------------------------------- /static/art/Spacefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/Spacefox.png -------------------------------------------------------------------------------- /static/art/Spacefox_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/Spacefox_16x16.png -------------------------------------------------------------------------------- /static/art/curses_640x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/curses_640x300.png -------------------------------------------------------------------------------- /static/art/curses_800x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/curses_800x600.png -------------------------------------------------------------------------------- /static/art/curses_square_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/curses_square_16x16.png -------------------------------------------------------------------------------- /static/art/t_Anno.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/t_Anno.png -------------------------------------------------------------------------------- /static/art/t_Bisasam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/t_Bisasam.png -------------------------------------------------------------------------------- /static/art/t_Cheepicus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/t_Cheepicus.png -------------------------------------------------------------------------------- /static/art/t_Cheepicus8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/t_Cheepicus8bit.png -------------------------------------------------------------------------------- /static/art/t_Phoebus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/t_Phoebus.png -------------------------------------------------------------------------------- /static/art/t_Phssthpok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/t_Phssthpok.png -------------------------------------------------------------------------------- /static/art/t_ShizzleClean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/t_ShizzleClean.png -------------------------------------------------------------------------------- /static/art/t_Spacefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/t_Spacefox.png -------------------------------------------------------------------------------- /static/art/t_wanderlust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/t_wanderlust.png -------------------------------------------------------------------------------- /static/art/wanderlust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/art/wanderlust.png -------------------------------------------------------------------------------- /static/colors/CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | These colorfiles were contributed by Dragoon209 2 | The script used can be found at 3 | https://github.com/Ankoku/df-webfort/issues/27 4 | -------------------------------------------------------------------------------- /static/colors/CowThing.json: -------------------------------------------------------------------------------- 1 | [ 2 | 49, 49, 62, 3 | 64, 81, 218, 4 | 86, 182, 54, 5 | 82, 183, 192, 6 | 206, 49, 55, 7 | 142, 58, 186, 8 | 240, 162, 42, 9 | 175, 181, 183, 10 | 118, 118, 129, 11 | 98, 149, 237, 12 | 175, 242, 57, 13 | 172, 235, 217, 14 | 248, 80, 62, 15 | 221, 98, 197, 16 | 252, 239, 98, 17 | 255, 255, 255 18 | ] 19 | -------------------------------------------------------------------------------- /static/colors/RedGreenColorblind.json: -------------------------------------------------------------------------------- 1 | [ 2 | 0, 0, 0, 3 | 0, 0, 240, 4 | 0, 128, 0, 5 | 0, 112, 144, 6 | 240, 0, 0, 7 | 160, 0, 128, 8 | 128, 96, 0, 9 | 208, 208, 208, 10 | 112, 112, 112, 11 | 80, 80, 255, 12 | 0, 208, 0, 13 | 64, 208, 255, 14 | 255, 80, 80, 15 | 255, 48, 240, 16 | 255, 255, 64, 17 | 255,255,255 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridAsh.json: -------------------------------------------------------------------------------- 1 | [ 2 | 0, 0, 0, 3 | 73, 73, 73, 4 | 103, 103, 103, 5 | 165, 165, 165, 6 | 37, 37, 37, 7 | 85, 85, 85, 8 | 60, 60, 60, 9 | 142, 142, 142, 10 | 52, 52, 52, 11 | 129, 129, 129, 12 | 219, 219, 219, 13 | 247, 247, 247, 14 | 80, 80, 80, 15 | 119, 119, 120, 16 | 182, 182, 182, 17 | 255, 255, 255 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridBone.json: -------------------------------------------------------------------------------- 1 | [ 2 | 38, 23, 10, 3 | 15, 82, 186, 4 | 120, 134, 23, 5 | 86, 184, 114, 6 | 132, 0, 0, 7 | 124, 26, 96, 8 | 104, 75, 58, 9 | 154, 132, 109, 10 | 65, 53, 43, 11 | 0, 138, 255, 12 | 196, 219, 38, 13 | 72, 255, 184, 14 | 192, 61, 36, 15 | 255, 66, 130, 16 | 255, 195, 34, 17 | 252, 250, 208 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridDarkSand.json: -------------------------------------------------------------------------------- 1 | [ 2 | 2, 1, 16, 3 | 4, 81, 140, 4 | 93, 109, 6, 5 | 12, 79, 76, 6 | 142, 40, 0, 7 | 131, 0, 52, 8 | 114, 89, 58, 9 | 51, 69, 84, 10 | 17, 35, 51, 11 | 0, 176, 238, 12 | 166, 176, 0, 13 | 12, 187, 160, 14 | 206, 73, 1, 15 | 160, 3, 7, 16 | 255, 183, 77, 17 | 230, 245, 247 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridDefault.json: -------------------------------------------------------------------------------- 1 | [ 2 | 0, 0, 0, 3 | 0, 0, 128, 4 | 0, 128, 0, 5 | 0, 128, 128, 6 | 128, 0, 0, 7 | 128, 0, 128, 8 | 128, 128, 0, 9 | 192, 192, 192, 10 | 128, 128, 128, 11 | 0, 0, 255, 12 | 0, 255, 0, 13 | 0, 255, 255, 14 | 255, 0, 0, 15 | 255, 0, 255, 16 | 255, 255, 0, 17 | 255, 255, 255 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridDefaultPlus.json: -------------------------------------------------------------------------------- 1 | [ 2 | 0, 0, 0, 3 | 18, 51, 175, 4 | 0, 128, 0, 5 | 0, 178, 178, 6 | 107, 0, 0, 7 | 128, 0, 128, 8 | 118, 94, 0, 9 | 192, 192, 192, 10 | 80, 80, 80, 11 | 0, 114, 255, 12 | 0, 255, 0, 13 | 0, 255, 255, 14 | 255, 0, 0, 15 | 209, 0, 172, 16 | 255, 255, 0, 17 | 255, 255, 255 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridDusk.json: -------------------------------------------------------------------------------- 1 | [ 2 | 10, 10, 10, 3 | 4, 81, 140, 4 | 93, 109, 6, 5 | 12, 79, 76, 6 | 142, 40, 0, 7 | 131, 0, 52, 8 | 235, 136, 0, 9 | 128, 109, 99, 10 | 48, 38, 45, 11 | 0, 176, 238, 12 | 166, 176, 0, 13 | 12, 187, 160, 14 | 206, 73, 1, 15 | 160, 3, 7, 16 | 255, 183, 77, 17 | 242, 247, 230 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridFallen.json: -------------------------------------------------------------------------------- 1 | [ 2 | 32, 30, 21, 3 | 35, 67, 83, 4 | 119, 113, 54, 5 | 63, 82, 43, 6 | 106, 39, 19, 7 | 103, 23, 8, 8 | 92, 79, 40, 9 | 78, 85, 55, 10 | 50, 55, 35, 11 | 48, 121, 143, 12 | 239, 193, 64, 13 | 115, 125, 38, 14 | 178, 76, 21, 15 | 181, 42, 11, 16 | 251, 166, 18, 17 | 244, 255, 208 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridFields.json: -------------------------------------------------------------------------------- 1 | [ 2 | 17, 11, 22, 3 | 39, 68, 92, 4 | 107, 104, 25, 5 | 62, 116, 102, 6 | 92, 37, 20, 7 | 98, 24, 6, 8 | 141, 85, 12, 9 | 79, 67, 58, 10 | 51, 44, 39, 11 | 47, 172, 199, 12 | 190, 188, 39, 13 | 157, 220, 133, 14 | 175, 59, 20, 15 | 164, 36, 6, 16 | 224, 164, 4, 17 | 235, 222, 198 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridHeat.json: -------------------------------------------------------------------------------- 1 | [ 2 | 23, 11, 11, 3 | 57, 75, 69, 4 | 125, 111, 16, 5 | 81, 122, 78, 6 | 110, 44, 13, 7 | 116, 30, 4, 8 | 156, 92, 8, 9 | 98, 74, 40, 10 | 71, 52, 27, 11 | 66, 176, 182, 12 | 199, 192, 26, 13 | 170, 222, 108, 14 | 186, 66, 13, 15 | 176, 42, 4, 16 | 228, 168, 2, 17 | 238, 224, 181 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridJade.json: -------------------------------------------------------------------------------- 1 | [ 2 | 17, 11, 9, 3 | 46, 73, 94, 4 | 108, 107, 37, 5 | 92, 99, 58, 6 | 118, 42, 27, 7 | 174, 44, 59, 8 | 114, 85, 57, 9 | 69, 60, 49, 10 | 42, 30, 27, 11 | 76, 136, 158, 12 | 195, 193, 61, 13 | 112, 161, 108, 14 | 175, 101, 47, 15 | 171, 66, 30, 16 | 203, 154, 69, 17 | 219, 214, 156 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridLaser.json: -------------------------------------------------------------------------------- 1 | [ 2 | 10, 10, 0, 3 | 0, 51, 33, 4 | 136, 109, 53, 5 | 9, 131, 19, 6 | 98, 32, 12, 7 | 74, 14, 3, 8 | 132, 78, 7, 9 | 56, 64, 35, 10 | 41, 39, 2, 11 | 40, 112, 53, 12 | 239, 193, 64, 13 | 35, 202, 55, 14 | 175, 59, 20, 15 | 165, 33, 3, 16 | 233, 171, 4, 17 | 204, 239, 115 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridMatrix.json: -------------------------------------------------------------------------------- 1 | [ 2 | 25, 32, 7, 3 | 24, 119, 79, 4 | 77, 114, 24, 5 | 179, 135, 0, 6 | 118, 68, 11, 7 | 32, 70, 49, 8 | 77, 84, 7, 9 | 107, 110, 75, 10 | 56, 58, 38, 11 | 13, 189, 117, 12 | 136, 190, 18, 13 | 255, 204, 0, 14 | 185, 103, 6, 15 | 82, 127, 57, 16 | 185, 218, 28, 17 | 245, 254, 210 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridMishka.json: -------------------------------------------------------------------------------- 1 | [ 2 | 23, 15, 13, 3 | 0, 95, 255, 4 | 123, 160, 0, 5 | 0, 135, 95, 6 | 186, 83, 0, 7 | 167, 36, 0, 8 | 176, 137, 81, 9 | 120, 100, 75, 10 | 74, 52, 46, 11 | 0, 179, 255, 12 | 214, 239, 0, 13 | 0, 255, 180, 14 | 234, 132, 0, 15 | 255, 67, 16, 16 | 228, 179, 27, 17 | 241, 227, 184 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridMud.json: -------------------------------------------------------------------------------- 1 | [ 2 | 32, 30, 21, 3 | 35, 67, 83, 4 | 119, 113, 54, 5 | 68, 84, 21, 6 | 137, 54, 14, 7 | 132, 103, 34, 8 | 92, 79, 40, 9 | 78, 85, 55, 10 | 50, 55, 35, 11 | 48, 121, 143, 12 | 173, 164, 94, 13 | 118, 139, 32, 14 | 188, 74, 19, 15 | 204, 160, 54, 16 | 251, 166, 18, 17 | 255, 252, 227 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridNatural.json: -------------------------------------------------------------------------------- 1 | [ 2 | 27, 18, 9, 3 | 32, 50, 150, 4 | 30, 100, 40, 5 | 16, 120, 115, 6 | 110, 20, 20, 7 | 133, 17, 171, 8 | 100, 64, 42, 9 | 116, 108, 84, 10 | 70, 65, 50, 11 | 0, 120, 255, 12 | 170, 255, 0, 13 | 27, 210, 205, 14 | 192, 36, 36, 15 | 230, 71, 170, 16 | 250, 180, 10, 17 | 220, 220, 220 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridSorrow.json: -------------------------------------------------------------------------------- 1 | [ 2 | 5, 0, 17, 3 | 46, 73, 94, 4 | 85, 83, 42, 5 | 67, 125, 66, 6 | 142, 40, 0, 7 | 123, 38, 54, 8 | 129, 101, 53, 9 | 135, 142, 93, 10 | 57, 42, 32, 11 | 76, 136, 158, 12 | 175, 181, 75, 13 | 121, 189, 143, 14 | 182, 73, 38, 15 | 161, 86, 66, 16 | 203, 154, 69, 17 | 255, 247, 130 18 | ] -------------------------------------------------------------------------------- /static/colors/VheridWarm.json: -------------------------------------------------------------------------------- 1 | [ 2 | 27, 18, 9, 3 | 26, 65, 165, 4 | 110, 123, 21, 5 | 0, 120, 96, 6 | 82, 25, 0, 7 | 76, 0, 55, 8 | 98, 61, 38, 9 | 154, 132, 109, 10 | 62, 53, 44, 11 | 0, 108, 255, 12 | 196, 219, 38, 13 | 0, 255, 204, 14 | 192, 61, 36, 15 | 255, 32, 141, 16 | 255, 191, 0, 17 | 255, 237, 218 18 | ] -------------------------------------------------------------------------------- /static/colors/chroma.json: -------------------------------------------------------------------------------- 1 | [ 2 | 0, 0, 0, 3 | 0, 0, 192, 4 | 0, 128, 0, 5 | 0, 112, 144, 6 | 192, 0, 0, 7 | 160, 0, 128, 8 | 96, 96, 0, 9 | 192, 192, 192, 10 | 128, 128, 128, 11 | 48, 48, 255, 12 | 0, 208, 0, 13 | 64, 208, 255, 14 | 255, 48, 48, 15 | 255, 64, 208, 16 | 255, 255, 64, 17 | 255, 255, 255 18 | ] 19 | -------------------------------------------------------------------------------- /static/colors/curses.json: -------------------------------------------------------------------------------- 1 | [ 2 | 0, 0, 0, 3 | 0, 0, 128, 4 | 0, 128, 0, 5 | 0, 128, 128, 6 | 128, 0, 0, 7 | 128, 0, 128, 8 | 128, 128, 0, 9 | 192, 192, 192, 10 | 128, 128, 128, 11 | 0, 0, 255, 12 | 0, 255, 0, 13 | 0, 255, 255, 14 | 255, 0, 0, 15 | 255, 0, 255, 16 | 255, 255, 0, 17 | 255, 255, 255 18 | ] 19 | -------------------------------------------------------------------------------- /static/colors/default.json: -------------------------------------------------------------------------------- 1 | [ 2 | 32, 39, 49, 3 | 0, 106, 255, 4 | 68, 184, 57, 5 | 114, 156, 251, 6 | 212, 54, 85, 7 | 176, 50, 255, 8 | 217, 118, 65, 9 | 170, 196, 178, 10 | 128, 151, 156, 11 | 48, 165, 255, 12 | 144, 255, 79, 13 | 168, 212, 255, 14 | 255, 82, 82, 15 | 255, 107, 255, 16 | 255, 232, 102, 17 | 255, 250, 232 18 | ] 19 | -------------------------------------------------------------------------------- /static/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * config.js 3 | * part of the Web Fortress frontend. 4 | * Copyright (c) 2014 mifki, ISC license. 5 | * 6 | * This config file is directly executed, so be careful! 7 | * any value defined here becomes the default, which can be overrided via query 8 | * string. 9 | */ 10 | 11 | // note that some fields are edited by config-srv, which is 12 | // dynamically-generated. 13 | var config = { 14 | port: '1234', 15 | protocol: 'DFPlex-invalid', 16 | tiles: "Phoebus.png", 17 | text: "ShizzleClean.png", 18 | overworld: "ShizzleClean.png", 19 | nick: "", 20 | secret: "", 21 | colors: undefined, 22 | hide_chat: undefined, 23 | show_fps: undefined 24 | }; 25 | -------------------------------------------------------------------------------- /static/dfplex.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: rgb(32,39,49); 4 | overflow: hidden; 5 | } 6 | 7 | .status { 8 | height: 1.5em; 9 | line-height: 1.5em; 10 | background-color: orange; 11 | color: white; 12 | font-family: monospace; 13 | } 14 | 15 | .trace { 16 | color:white; 17 | font-family:monospace; 18 | background-color: rgba(200, 100, 0, 0.7); 19 | } 20 | 21 | #message { 22 | float: left; 23 | width: 70%; 24 | text-align: center; 25 | } 26 | 27 | #user-count { 28 | float: left; 29 | width: 15%; 30 | text-align: left; 31 | } 32 | 33 | #time-left { 34 | float: left; 35 | width: 15%; 36 | text-align: right; 37 | } 38 | 39 | #webchat { 40 | width: 100%; 41 | height: 100%; 42 | } 43 | 44 | .col { 45 | position: relative; 46 | min-height: 1px; 47 | float: left; 48 | margin-left: 0; margin-right: 0; 49 | padding-left: 0; padding-right: 0; 50 | } 51 | 52 | .col.col-right { 53 | position: absolute; 54 | top: 0; 55 | right: 0; 56 | height: 100%; 57 | } 58 | 59 | .col-left { 60 | width: 100%; 61 | height: 100vh; 62 | } 63 | 64 | .canvas-container { 65 | text-align: center; 66 | position: absolute; 67 | bottom: 0; 68 | top: 0; 69 | margin-top: 1.5em; 70 | width: 100%; 71 | height: 100%; 72 | } 73 | 74 | canvas { 75 | position: relative; 76 | } 77 | -------------------------------------------------------------------------------- /static/dfplex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dwarf Fortress 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
-
14 |
Loading...
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/favicon.ico -------------------------------------------------------------------------------- /static/js/dfplex.js: -------------------------------------------------------------------------------- 1 | /* 2 | * dfplex.js 3 | * Copyright (c) 2014 mifki, ISC license. 4 | */ 5 | 6 | /*jslint browser:true */ 7 | 8 | var params = getParams(); 9 | // TODO: tag colors 10 | var colors = [ 11 | 32, 39, 49, 12 | 0, 106, 255, 13 | 68, 184, 57, 14 | 114, 156, 251, 15 | 212, 54, 85, 16 | 176, 50, 255, 17 | 217, 118, 65, 18 | 170, 196, 178, 19 | 128, 151, 156, 20 | 48, 165, 255, 21 | 144, 255, 79, 22 | 168, 212, 255, 23 | 255, 82, 82, 24 | 255, 107, 255, 25 | 255, 232, 102, 26 | 255, 250, 232 27 | ]; 28 | 29 | var MAX_FPS = 20; 30 | 31 | var port = params.port; 32 | var protocol = params.protocol; 33 | var tileSet = params.tiles; 34 | var textSet = params.text; 35 | var ovrSet = params.overworld; 36 | var colorscheme = params.colors; 37 | var nick = params.nick; 38 | var secret = params.secret; 39 | 40 | var wsUri = 'ws://' + location.hostname + ':' + port + 41 | '/' + encodeURIComponent(nick) + 42 | '/' + encodeURIComponent(secret); 43 | console.log(wsUri); 44 | var active = false; 45 | var lastFrame = 0; 46 | 47 | var tilew = 16; 48 | var tileh = 16; 49 | 50 | var cmd = { 51 | "update": 110, 52 | "sendKey": 111, 53 | "connect": 115, 54 | "resize": 117, 55 | "requestTurn": 116 56 | }; 57 | 58 | // Converts integer value in seconds to a time string, HH:MM:SS 59 | function toTime(n) { 60 | var h = Math.floor(n / 60 / 60); 61 | var m = Math.floor(n / 60) % 60; 62 | var s = n % 60; 63 | return ("0" + h).slice(-2) + ":" + 64 | ("0" + m).slice(-2) + ":" + 65 | ("0" + s).slice(-2); 66 | } 67 | 68 | function plural(n, unit) 69 | { 70 | return n + " " + unit + (n === 1 ? "" : "s"); 71 | } 72 | 73 | // Converts an integer value in ticks to the dwarven calendar 74 | function toGameTime(n) { 75 | var years = Math.floor(n / 12 / 28 / 1200); 76 | var months = Math.floor(n / 28 / 1200) % 12; 77 | var days = Math.floor(n / 1200) % 28; 78 | var ticks = n % 1200; 79 | 80 | var times = []; 81 | if (years > 0) { 82 | times.push(plural(years, "year")); 83 | } 84 | if (months > 0) { 85 | times.push(plural(months, "month")); 86 | } else if (days > 0) { 87 | times.push(plural(days, "day")); 88 | } else { 89 | times.push(plural(ticks, "tick")); 90 | } 91 | 92 | return times.join(", "); 93 | } 94 | 95 | function setStats(userCount, loadframes) { 96 | var u = document.getElementById('user-count'); 97 | // var t = document.getElementById('time-left'); 98 | u.innerHTML = String(userCount) + " "; 99 | } 100 | 101 | function setDebug(debugInfo) { 102 | var c = document.getElementById('trace-container'); 103 | var u = document.getElementById('trace'); 104 | if (debugInfo == "") 105 | { 106 | u.innerHTML = ""; 107 | //c.setAttribute("style","width:0px"); 108 | } 109 | else 110 | { 111 | var ih = "
" + debugInfo + "
"; 112 | if (u.innerHTML != ih) 113 | { 114 | u.innerHTML = ih; 115 | } 116 | //c.setAttribute("style","width:200px"); 117 | } 118 | } 119 | 120 | var statusOnClick = null; 121 | function setStatus(text, color, onclick) { 122 | var m = document.getElementById('message'); 123 | if (m.innerHTML != text) 124 | { 125 | m.innerHTML = text; 126 | } 127 | var st = m.parentNode; 128 | if (statusOnClick) { 129 | st.removeEventListener('click', statusOnClick); 130 | } 131 | statusOnClick = onclick; 132 | if (onclick) { 133 | st.addEventListener('click', onclick); 134 | st.style.cursor = 'pointer'; 135 | } else { 136 | st.style.cursor = ''; 137 | } 138 | st.style.backgroundColor = color; 139 | } 140 | 141 | function connect() { 142 | setStatus('Connecting...', 'orange'); 143 | websocket = new WebSocket(wsUri); 144 | websocket.binaryType = 'arraybuffer'; 145 | websocket.onopen = onOpen; 146 | websocket.onclose = onClose; 147 | websocket.onerror = onError; 148 | } 149 | 150 | function onOpen(evt) { 151 | setStatus('Connected, initializing...', 'orange'); 152 | 153 | websocket.send(new Uint8Array([cmd.connect])); 154 | 155 | websocket.send(new Uint8Array([cmd.update])); 156 | websocket.onmessage = onMessage; 157 | } 158 | 159 | var isError = false; 160 | function onClose(evt) { 161 | console.log("Disconnect code #" + evt.code + ", reason: " + evt.reason); 162 | console.log(isError); 163 | if (isError) { 164 | isError = false; 165 | setStatus('Connection Error. Click to retry', 'red', connect); 166 | } else if (evt.reason) { 167 | setStatus(evt.reason + ' Click to try again.', 'red', connect); 168 | } else { 169 | setStatus('Unknown disconnect: Check the console (Ctrl-Shift-J), then click to reconnect.', 'red', connect); 170 | } 171 | } 172 | 173 | function onError(ev) { 174 | console.log("error triggered."); 175 | isError = true; 176 | } 177 | 178 | function requestTurn() { 179 | websocket.send(new Uint8Array([cmd.requestTurn])); 180 | } 181 | 182 | function renderQueueStatus(s) { 183 | var display = s.currentPlayer || "Connected."; 184 | active = true; 185 | var colour = 'yellow'; 186 | if (display == "Connected." || display.startsWith("(MP)")) 187 | colour = 'green' 188 | if (display.startsWith("(UP)")) 189 | colour = 'grey' 190 | setStatus(display, colour, requestTurn); 191 | setStats(s.playerCount, s.load); 192 | setDebug(s.debugInfo); 193 | } 194 | 195 | // TODO: document, split 196 | function renderUpdate(ctx, data, offset) { 197 | var t = []; // text tile indices 198 | var ovr = []; // overworld tile indices 199 | var k; 200 | var x; 201 | var y; 202 | var s; 203 | var bg; 204 | var fg; 205 | var tilew2 = tilew << 4; 206 | var tileh2 = tileh << 4; 207 | 208 | for (k = offset; k < data.length; k += 5) { 209 | x = data[k + 0]; 210 | y = data[k + 1]; 211 | 212 | s = data[k + 2]; 213 | bg = data[k + 3] & 0xf; 214 | fg = data[k + 4]; 215 | 216 | var bg_x = ((bg & 3) * tilew2) + 15 * tilew; 217 | var bg_y = ((bg >> 2) * tileh2) + 15 * tileh; 218 | ctx.drawImage( 219 | // source image (tileset) 220 | cd, 221 | // source rect 222 | bg_x, bg_y, tilew, tileh, 223 | // dest rect 224 | x * tilew, y * tileh, tilew, tileh 225 | ); 226 | 227 | // 6th bit: text. 7th bit: overworld 228 | if ((data[k + 3] & 64)) { 229 | t.push(k); 230 | continue; 231 | } 232 | if ((data[k + 3] & 128)) { 233 | ovr.push(k); 234 | continue; 235 | } 236 | var fg_x = (s & 0xf) * tilew + ((fg & 3) * tilew2); 237 | var fg_y = (s >> 4) * tileh + ((fg >> 2) * tileh2); 238 | ctx.drawImage( 239 | // source image (tileset) 240 | cd, 241 | // source rect 242 | fg_x, fg_y, tilew, tileh, 243 | // dest rect 244 | x * tilew, y * tileh, tilew, tileh 245 | ); 246 | } 247 | 248 | // draw text 249 | for (var m = 0; m < t.length; m++) { 250 | k = t[m]; 251 | x = data[k + 0]; 252 | y = data[k + 1]; 253 | 254 | s = data[k + 2]; 255 | bg = data[k + 3]; 256 | fg = data[k + 4]; 257 | 258 | var i = (s & 0xf) * tilew + ((fg & 3) * tilew2); 259 | var j = (s >> 4) * tileh + ((fg >> 2) * tileh2); 260 | ctx.drawImage( 261 | // source image (textset) 262 | ct, 263 | // source rect 264 | i, j, tilew, tileh, 265 | // dest rect 266 | x * tilew, y * tileh, tilew, tileh 267 | ); 268 | } 269 | 270 | for (var m = 0; m < ovr.length; m++) { 271 | k = ovr[m]; 272 | x = data[k + 0]; 273 | y = data[k + 1]; 274 | 275 | s = data[k + 2]; 276 | bg = data[k + 3]; 277 | fg = data[k + 4]; 278 | 279 | var i = (s & 0xf) * tilew + ((fg & 3) * tilew2); 280 | var j = (s >> 4) * tileh + ((fg >> 2) * tileh2); 281 | ctx.drawImage( 282 | // source image (textset) 283 | covr, 284 | // source rect 285 | i, j, tilew, tileh, 286 | // dest rect 287 | x * tilew, y * tileh, tilew, tileh 288 | ); 289 | } 290 | } 291 | 292 | function updateCanvasDOM() { 293 | canvas.style.width = ""; 294 | canvas.style.height = ""; 295 | 296 | var maxw = canvas.parentNode.offsetWidth; 297 | var maxh = canvas.parentNode.offsetHeight - document.getElementById('status-id').offsetHeight; 298 | 299 | console.log(canvas.width + "x" + canvas.height + " in " + maxw + "x" + maxh); 300 | 301 | if (maxw >= canvas.width && maxh >= canvas.height) { 302 | canvas.style.left = (maxw - canvas.width) / 2 + "px"; 303 | return 304 | } 305 | 306 | var aspectRatio = canvas.width / canvas.height; 307 | var constrainWidth = (maxw / maxh < aspectRatio); 308 | 309 | console.log("constrainWidth: " + constrainWidth) 310 | canvas.style.left = "0" 311 | 312 | if (constrainWidth) { 313 | canvas.style.width = "100%"; 314 | canvas.style.height = "" 315 | } else { 316 | canvas.style.width = ""; 317 | canvas.style.height = "100%"; 318 | } 319 | } 320 | 321 | function onMessage(evt) { 322 | var data = new Uint8Array(evt.data); 323 | 324 | var ctx = canvas.getContext('2d'); 325 | if (data[0] === cmd.update) { 326 | if (stats) { stats.begin(); } 327 | var gameStatus = {}; 328 | gameStatus.playerCount = data[1] & 127; 329 | 330 | gameStatus.isActive = (data[2] & 1) !== 0; 331 | 332 | // removed 333 | gameStatus.isNoPlayer = false; 334 | gameStatus.ingameTime = false; 335 | gameStatus.timeLeft = -1; 336 | 337 | // # of frames overall 338 | gameStatus.load = 339 | (data[3]<<0) | 340 | (data[4]<<8) | 341 | (data[5]<<16) | 342 | (data[6]<<24); 343 | 344 | var neww = data[7] * tilew; 345 | var newh = data[8] * tileh; 346 | if (neww != canvas.width || newh != canvas.height) { 347 | canvas.width = neww; 348 | canvas.height = newh; 349 | 350 | updateCanvasDOM() 351 | } 352 | 353 | var nickSize = data[9]; 354 | // this only works because we know the input is uri-encoded ascii 355 | var activeNick = ""; 356 | for (var i = 10; (i < 10 + nickSize) && data[i] !== 0; i++) { 357 | activeNick += String.fromCharCode(data[i]); 358 | } 359 | gameStatus.currentPlayer = decodeURIComponent(activeNick); 360 | 361 | var offset = 10 + nickSize; 362 | 363 | var debugInfoSize = data[offset] | (data[offset + 1] << 8); 364 | offset += 2; 365 | var debugInfo = "" 366 | for (var i = 0; i < debugInfoSize && data[offset + i] !== 0; ++i) { 367 | debugInfo += String.fromCharCode(data[offset + i]); 368 | } 369 | offset += debugInfoSize; 370 | gameStatus.debugInfo = debugInfo; 371 | 372 | renderQueueStatus(gameStatus); 373 | renderUpdate(ctx, data, offset); 374 | 375 | var now = performance.now(); 376 | var nextFrame = (1000 / MAX_FPS) - (now - lastFrame); 377 | if (nextFrame < 4) { 378 | websocket.send(new Uint8Array([cmd.update])); 379 | } else { 380 | setTimeout(function() { 381 | websocket.send(new Uint8Array([cmd.update])); 382 | }, nextFrame); 383 | } 384 | lastFrame = performance.now(); 385 | if (stats) { stats.end(); } 386 | } 387 | } 388 | 389 | function colorize(img, cnv) { 390 | var tsw = tilew << 4; 391 | var tsh = tileh << 4; 392 | cnv.width = tilew << 6; 393 | cnv.height = tileh << 6; 394 | 395 | var ctx3 = cnv.getContext('2d'); 396 | 397 | for (var j = 0, j2 = 0; j < 4; j++, j2 += tsh) { 398 | for (var i = 0, i2 = 0; i < 4; i++, i2 += tsw) { 399 | // offset into colors array 400 | var c = (j * 4 + i) * 3; 401 | 402 | if (tsw === img.width && tsh === img.height) { 403 | ctx3.drawImage(img, i2, j2); 404 | } else { 405 | // tileset has a different size than the biggest; scale it 406 | // (TODO: better solution?) 407 | ctx3.drawImage(img, i2, j2, tsw, tsh); 408 | } 409 | 410 | var idata = ctx3.getImageData(i2, j2, tilew << 4, tileh << 4); 411 | var pixels = idata.data; 412 | 413 | for (var u = 0, len = pixels.length; u < len; u += 4) { 414 | if (pixels[u] === 255 && pixels[u + 1] === 0 && pixels[u + 2] === 255) { 415 | // poor man's transparency 416 | pixels[u] = 0; 417 | pixels[u + 1] = 0; 418 | pixels[u + 2] = 0; 419 | pixels[u + 3] = 0; 420 | } 421 | pixels[u] = pixels[u] * (colors[c + 0] / 255); 422 | pixels[u + 1] = pixels[u + 1] * (colors[c + 1] / 255); 423 | pixels[u + 2] = pixels[u + 2] * (colors[c + 2] / 255); 424 | } 425 | ctx3.putImageData(idata, i2, j2); 426 | 427 | ctx3.fillStyle = 'rgb(' + 428 | colors[c + 0] + ',' + 429 | colors[c + 1] + ',' + 430 | colors[c + 2] + ')'; 431 | 432 | ctx3.fillRect(i2 + tilew * 15, j2 + tileh * 15, tilew, tileh); 433 | } 434 | } 435 | } 436 | 437 | // Crazy closures, Batman, what's going on here? 438 | var make_loader = function() { 439 | var loading = 0; 440 | return function() { 441 | loading += 1; 442 | return function() { 443 | loading -= 1; 444 | if (loading <= 0) { 445 | init(); 446 | } 447 | }; 448 | }; 449 | }(); 450 | 451 | var cd, ct, covr; 452 | function init() { 453 | document.body.style.backgroundColor = 454 | 'rgb(' + colors[0] + ',' + colors[1] + ',' + colors[2] + ')'; 455 | 456 | tilew = Math.max(ts.width, tt.width, tovr.width) / 16; 457 | tileh = Math.max(ts.height, tt.height, tovr.height) / 16; 458 | 459 | cd = document.createElement('canvas'); 460 | colorize(ts, cd); 461 | 462 | ct = document.createElement('canvas'); 463 | colorize(tt, ct); 464 | 465 | covr = document.createElement('canvas'); 466 | colorize(tovr, covr); 467 | 468 | lastFrame = performance.now(); 469 | 470 | connect(); 471 | } 472 | 473 | var stats; 474 | if (params.show_fps) { 475 | stats = new Stats(); 476 | document.body.appendChild(stats.domElement); 477 | stats.domElement.style.position = "absolute"; 478 | stats.domElement.style.bottom = "0"; 479 | stats.domElement.style.left = "0"; 480 | } 481 | 482 | function getFolder(path) { 483 | return path.substring(0, path.lastIndexOf('/') + 1); 484 | } 485 | 486 | var root = getFolder(window.location.pathname); 487 | 488 | // tileset 489 | var ts = document.createElement('img'); 490 | ts.src = root + "art/" + tileSet; 491 | ts.onload = make_loader(); 492 | 493 | // textset 494 | var tt = document.createElement('img'); 495 | tt.src = root + "art/" + textSet; 496 | tt.onload = make_loader(); 497 | 498 | // overworldset 499 | var tovr = document.createElement('img'); 500 | tovr.src = root + "art/" + ovrSet; 501 | tovr.onload = make_loader(); 502 | 503 | if (colorscheme !== undefined) { 504 | var colorReq = new XMLHttpRequest(); 505 | var colorLoader = make_loader(); 506 | colorReq.onload = function() { 507 | colors = JSON.parse(this.responseText); 508 | colorLoader(); 509 | }; 510 | colorReq.open("get", root + "colors/" + colorscheme); 511 | colorReq.send(); 512 | } 513 | 514 | 515 | var canvas = document.getElementById('myCanvas'); 516 | canvas.style.position="absolute" 517 | canvas.style.left="0" 518 | canvas.style.top="0" 519 | document.onkeydown = function(ev) { 520 | if (!active) 521 | return; 522 | 523 | if (ev.keyCode === 18 || 524 | ev.keyCode === 17 || 525 | ev.keyCode === 16) { 526 | return; 527 | } 528 | 529 | var mod = ev.shiftKey | (ev.ctrlKey << 1) | (ev.altKey << 2); 530 | var charCode = 0; 531 | if (ev.key.length == 1){ 532 | charCode = ev.key.charCodeAt(0) 533 | } 534 | var data = new Uint8Array([cmd.sendKey, ev.keyCode, charCode, mod]); 535 | logKeyCode(ev); 536 | websocket.send(data); 537 | ev.preventDefault(); 538 | }; 539 | 540 | function udpateScreenSize() { 541 | var maxw = canvas.parentNode.offsetWidth; 542 | var maxh = canvas.parentNode.offsetHeight - document.getElementById('status-id').offsetHeight; 543 | var gridw = Math.min(Math.floor(maxw / tilew), 255); 544 | // need to subtract 1, likely the header. 545 | var gridh = Math.min(Math.floor(maxh / tileh), 255); 546 | 547 | // request resize from server. 548 | var data = new Uint8Array([cmd.resize, gridw, gridh]); 549 | 550 | // TODO: only send if screen size changed. 551 | websocket.send(data); 552 | } 553 | 554 | window.onresize = udpateScreenSize; 555 | window.onload = udpateScreenSize; 556 | 557 | // this is fine. 558 | setTimeout(udpateScreenSize, 10); 559 | setTimeout(udpateScreenSize, 100); 560 | setTimeout(udpateScreenSize, 200); 561 | setTimeout(udpateScreenSize, 400); 562 | setTimeout(udpateScreenSize, 800); 563 | setInterval(udpateScreenSize, 3000); 564 | -------------------------------------------------------------------------------- /static/js/keycode.js: -------------------------------------------------------------------------------- 1 | var _keyCodes = { 2 | 0: "\\", 3 | 8: "backspace", 4 | 9: "tab", 5 | 12: "num", 6 | 13: "enter", 7 | 16: "shift", 8 | 17: "ctrl", 9 | 18: "alt", 10 | 19: "pause", 11 | 20: "caps", 12 | 27: "esc", 13 | 32: "space", 14 | 33: "pageup", 15 | 34: "pagedown", 16 | 35: "end", 17 | 36: "home", 18 | 37: "left", 19 | 38: "up", 20 | 39: "right", 21 | 40: "down", 22 | 44: "print", 23 | 45: "insert", 24 | 46: "delete", 25 | 48: "0", 26 | 49: "1", 27 | 50: "2", 28 | 51: "3", 29 | 52: "4", 30 | 53: "5", 31 | 54: "6", 32 | 55: "7", 33 | 56: "8", 34 | 57: "9", 35 | 65: "a", 36 | 66: "b", 37 | 67: "c", 38 | 68: "d", 39 | 69: "e", 40 | 70: "f", 41 | 71: "g", 42 | 72: "h", 43 | 73: "i", 44 | 74: "j", 45 | 75: "k", 46 | 76: "l", 47 | 77: "m", 48 | 78: "n", 49 | 79: "o", 50 | 80: "p", 51 | 81: "q", 52 | 82: "r", 53 | 83: "s", 54 | 84: "t", 55 | 85: "u", 56 | 86: "v", 57 | 87: "w", 58 | 88: "x", 59 | 89: "y", 60 | 90: "z", 61 | 91: "cmd", 62 | 92: "cmd", 63 | 93: "cmd", 64 | 96: "num_0", 65 | 97: "num_1", 66 | 98: "num_2", 67 | 99: "num_3", 68 | 100: "num_4", 69 | 101: "num_5", 70 | 102: "num_6", 71 | 103: "num_7", 72 | 104: "num_8", 73 | 105: "num_9", 74 | 106: "num_multiply", 75 | 107: "num_add", 76 | 108: "num_enter", 77 | 109: "num_subtract", 78 | 110: "num_decimal", 79 | 111: "num_divide", 80 | 124: "print", 81 | 144: "num", 82 | 145: "scroll", 83 | 186: ";", 84 | 187: "=", 85 | 188: ",", 86 | 189: "-", 87 | 190: ".", 88 | 191: "/", 89 | 192: "`", 90 | 219: "[", 91 | 220: "\\", 92 | 221: "]", 93 | 222: "\'", 94 | 223: "`", 95 | 224: "cmd", 96 | 225: "alt", 97 | 57392: "ctrl", 98 | 63289: "num", 99 | 59: ";" 100 | }; 101 | 102 | function logKeyCode(ev) { 103 | var s = ""; 104 | if (ev.ctrlKey) { 105 | s += "CTRL-"; 106 | } 107 | if (ev.altKey) { 108 | s += "ALT-"; 109 | } 110 | if (ev.shiftKey) { 111 | s += "SHIFT-"; 112 | } 113 | s += _keyCodes[ev.keyCode]; 114 | console.log(s + " down."); 115 | } 116 | 117 | function logCharCode(ev) { 118 | var s = ""; 119 | if (ev.ctrlKey) { 120 | s += "CTRL-"; 121 | } 122 | if (ev.altKey) { 123 | s += "ALT-"; 124 | } 125 | if (ev.shiftKey) { 126 | s += "SHIFT-"; 127 | } 128 | s += String.fromCharCode(ev.which); 129 | console.log(s + " pressed."); 130 | } 131 | -------------------------------------------------------------------------------- /static/js/params.js: -------------------------------------------------------------------------------- 1 | /* 2 | * params.js 3 | * Copyright (c) 2014 alloyed, ISC license. 4 | */ 5 | 6 | /* 7 | * Reads query string of current URL, and puts the key-value pairs into a js 8 | * object. 9 | * Special cases: 10 | * * empty values are assumed to be true. 11 | * * keys with dashes get converted to underscores. 12 | */ 13 | function getJsonFromUrl() { 14 | var result = {} 15 | var query = location.search.substr(1); 16 | query.split("&").forEach(function(part) { 17 | var item = part.split("="); 18 | var key = item[0].replace('-', '_'); 19 | var val = item[1]; 20 | if (val === undefined) { 21 | val = true; 22 | } 23 | val = decodeURIComponent(val); 24 | if (val === "false") { 25 | val = false; 26 | } 27 | result[key] = val; 28 | }); 29 | return result; 30 | } 31 | 32 | /* 33 | * The inverse of getJsonFromUrl(). Returns a query string corresponding to 34 | * the js object obj. Underscores are converted back to dashes, and true values 35 | * are turned empty. 36 | */ 37 | function getUrlFromJson(obj) { 38 | var result = ""; 39 | var sep = '?'; 40 | for (var key in obj) { 41 | var val = obj[key]; 42 | var pair = encodeURIComponent(key.replace('_', '-')); 43 | 44 | if (val !== true) { 45 | pair += "=" + encodeURIComponent(obj[key]); 46 | } 47 | result += sep + pair; 48 | sep = '&'; 49 | } 50 | return obj; 51 | } 52 | 53 | function merge( /* & arguments */ ) { 54 | var result = {}; 55 | for (var i = 0; i < arguments.length; i++) { 56 | var obj = arguments[i]; 57 | for (var key in obj) { 58 | if (obj[key] !== undefined) { 59 | result[key] = obj[key]; 60 | } 61 | } 62 | } 63 | return result; 64 | } 65 | 66 | getParams = (function() { 67 | var params = null; 68 | return function() { 69 | if (params !== null) { 70 | return params; 71 | } 72 | var stored = localStorage.getItem("settings"); 73 | var params_stored = {}; 74 | if (stored) { 75 | params_stored = JSON.parse(stored); 76 | } 77 | var params_url = getJsonFromUrl(); 78 | 79 | params = merge(config, params_stored, params_url); 80 | console.log(params); 81 | 82 | if (params.store) { 83 | delete params.store; 84 | localStorage.setItem('settings', JSON.stringify(params)); 85 | } 86 | return params; 87 | }; 88 | })(); 89 | -------------------------------------------------------------------------------- /static/js/stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | var Stats=function(){var l=Date.now(),m=l,g=0,n=Infinity,o=0,h=0,p=Infinity,q=0,r=0,s=0,f=document.createElement("div");f.id="stats";f.addEventListener("mousedown",function(b){b.preventDefault();t(++s%2)},!1);f.style.cssText="width:80px;opacity:0.9;cursor:pointer";var a=document.createElement("div");a.id="fps";a.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#002";f.appendChild(a);var i=document.createElement("div");i.id="fpsText";i.style.cssText="color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; 3 | i.innerHTML="FPS";a.appendChild(i);var c=document.createElement("div");c.id="fpsGraph";c.style.cssText="position:relative;width:74px;height:30px;background-color:#0ff";for(a.appendChild(c);74>c.children.length;){var j=document.createElement("span");j.style.cssText="width:1px;height:30px;float:left;background-color:#113";c.appendChild(j)}var d=document.createElement("div");d.id="ms";d.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none";f.appendChild(d);var k=document.createElement("div"); 4 | k.id="msText";k.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";k.innerHTML="MS";d.appendChild(k);var e=document.createElement("div");e.id="msGraph";e.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0";for(d.appendChild(e);74>e.children.length;)j=document.createElement("span"),j.style.cssText="width:1px;height:30px;float:left;background-color:#131",e.appendChild(j);var t=function(b){s=b;switch(s){case 0:a.style.display= 5 | "block";d.style.display="none";break;case 1:a.style.display="none",d.style.display="block"}};return{REVISION:11,domElement:f,setMode:t,begin:function(){l=Date.now()},end:function(){var b=Date.now();g=b-l;n=Math.min(n,g);o=Math.max(o,g);k.textContent=g+" MS ("+n+"-"+o+")";var a=Math.min(30,30-30*(g/200));e.appendChild(e.firstChild).style.height=a+"px";r++;b>m+1E3&&(h=Math.round(1E3*r/(b-m)),p=Math.min(p,h),q=Math.max(q,h),i.textContent=h+" FPS ("+p+"-"+q+")",a=Math.min(30,30-30*(h/100)),c.appendChild(c.firstChild).style.height= 6 | a+"px",m=b,r=0);return b},update:function(){l=this.end()}}}; 7 | -------------------------------------------------------------------------------- /static/online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/white-rabbit-dfplex/dfplex/52a7c0d8a9941dd52d7c6fa6606fcbe5c0780229/static/online.png --------------------------------------------------------------------------------