├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── appveyor.yml ├── docs ├── colors.js ├── map-render.png └── strategic-view.png ├── network ├── Cargo.toml └── src │ ├── diskcache.rs │ ├── event.rs │ ├── lib.rs │ ├── memcache │ ├── memory.rs │ └── mod.rs │ ├── request.rs │ └── tokio │ ├── http.rs │ ├── mod.rs │ ├── types.rs │ ├── utils.rs │ └── ws.rs ├── rustfmt.toml ├── scripts ├── build.sh └── predeploy.ps1 └── ui ├── Cargo.toml └── src ├── bin └── screeps-rs-client.rs ├── rust ├── app.rs ├── glium_backend │ └── mod.rs ├── layout │ ├── left_panel.rs │ ├── login_screen.rs │ ├── mod.rs │ └── room_view.rs ├── lib.rs ├── map_view_utils │ └── mod.rs ├── network_integration.rs ├── rendering │ ├── constants.rs │ ├── macros.rs │ ├── map_view.rs │ ├── mod.rs │ ├── render_cache.rs │ └── types.rs ├── ui_state │ └── mod.rs ├── widgets │ ├── mod.rs │ └── text.rs └── window_management │ ├── glutin_glue.rs │ ├── mod.rs │ ├── setup.rs │ └── window_loop.rs └── ttf └── Akashi.ttf /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | target 4 | /build-out/ 5 | 6 | # Packages # 7 | ############ 8 | *.7z 9 | *.dmg 10 | *.gz 11 | *.iso 12 | *.jar 13 | *.rar 14 | *.tar 15 | *.zip 16 | 17 | # OS generated files # 18 | ###################### 19 | .DS_Store 20 | ehthumbs.db 21 | Icon? 22 | Thumbs.db 23 | 24 | # Project files # 25 | ################# 26 | .classpath 27 | .externalToolBuilders 28 | .idea 29 | .project 30 | .settings 31 | build 32 | dist 33 | nbproject 34 | atlassian-ide-plugin.xml 35 | build.xml 36 | nb-configuration.xml 37 | *.iml 38 | *.ipr 39 | *.iws 40 | *.sublime-project 41 | *.sublime-workspace 42 | .env 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | dist: trusty 4 | rust: nightly 5 | cache: 6 | cargo: true 7 | directories: 8 | - $HOME/openssl 9 | matrix: 10 | include: 11 | - env: TARGET=x86_64-unknown-linux-gnu TARGET_DESC=linux-x86_64 12 | - env: TARGET=i686-unknown-linux-gnu TARGET_DESC=linux-i686 BUILD_OPENSSL_VERSION=1.0.2k 13 | addons: 14 | apt: 15 | packages: 16 | - gcc-multilib 17 | - g++-multilib 18 | - g++-4.8-multilib 19 | # - env: TARGET=x86_64-unknown-linux-gnu TEST_ONLY=true 20 | # - env: TARGET=i686-unknown-linux-musl DEPLOY_ONLY=true TARGET_DESC=linux-i686 21 | # - env: TARGET=x86_64-unknown-linux-musl DEPLOY_ONLY=true TARGET_DESC=linux-x86_64 22 | # - env: TARGET=i686-apple-darwin DEPLOY_ONLY=true TARGET_DESC=apple-i686 23 | # os: osx 24 | - env: TARGET=x86_64-apple-darwin TARGET_DESC=apple-x86_64 25 | os: osx 26 | before_script: 27 | - | 28 | # set RUN_TEST 29 | if [[ (($TEST_ONLY = true || -z $DEPLOY_ONLY) && -z $TRAVIS_TAG) ]]; then 30 | RUN_TEST=true 31 | echo "tests enabled" 32 | else 33 | RUN_TEST=false 34 | fi 35 | - | 36 | # set RUN_DEPLOY 37 | if [[ ($DEPLOY_ONLY = true || -z $TEST_ONLY) && -n $TRAVIS_TAG ]]; then 38 | RUN_DEPLOY=true 39 | echo "deploy enabled" 40 | else 41 | RUN_DEPLOY=false 42 | fi 43 | - export PATH="$PATH:$HOME/.cargo/bin" 44 | - | 45 | # install target toolchain 46 | if $RUN_TEST || $RUN_DEPLOY; then 47 | rustup target add "$TARGET" || true 48 | fi 49 | - c++ --version 50 | - | 51 | # build openssl 52 | if [ -n "$BUILD_OPENSSL_VERSION" ]; then 53 | echo "building openssl" 54 | ./scripts/build.sh 55 | fi 56 | - | 57 | # set openssl configuration 58 | if [ -n "${BUILD_OPENSSL_VERSION}" -a -d "$HOME/openssl/lib" ]; then 59 | echo "Building using openssl-${BUILD_OPENSSL_VERSION}" 60 | export OPENSSL_DIR="${HOME}/openssl" 61 | export LD_LIBRARY_PATH="${HOME}/openssl/lib:${LD_LIBRARY_PATH}" 62 | export PATH="${HOME}/openssl/bin:${PATH}" 63 | fi 64 | script: 65 | - | 66 | # cargo build 67 | if $RUN_TEST; then 68 | cargo build --target "$TARGET" --verbose -j 1 69 | fi 70 | - | 71 | # cargo test 72 | if $RUN_TEST; then 73 | cargo test --target "$TARGET" -p screeps-rs-network --verbose -j 1 74 | cargo test --target "$TARGET" -p screeps-rs-ui --verbose -j 1 75 | fi 76 | - | 77 | # cargo build --release 78 | if $RUN_DEPLOY; then 79 | cargo build --target "$TARGET" -p screeps-rs --verbose --release -j 1 80 | fi 81 | before_deploy: 82 | - cargo build --target "$TARGET" --verbose --release -j 1 83 | - tar -C "target/$TARGET/release/" -czf "${TRAVIS_TAG}-${TARGET_DESC}.tar.gz" "screeps-rs-client" 84 | deploy: 85 | provider: releases 86 | api_key: 87 | secure: rW0srqf05xxlzsgiH0+4HfycQQUWHWldBj5PKno+GpVXF/wPvAekHmhKzq41WC+/j7WpQnLMzI3LDMR9ZLnXjvv5UmOPeN/G90q8rHPocBHk9qTPA24CNNR5/aW26GuQygWhOwItkbdI41E9rQ6DmqnOehw/eVt6XcFax9Bs2X4loDOL6++QuH4IkloFyegQVJxGLCO4wUnKqjNej7dR+EJSlNgHXzpuRpAvpASvQAdf7gi23PFokuMn/sJrqkPArwDWsb1+XRlwVP9GZkOn6aRZxmlb9ijx8cJ3IyD/DqwMT8L8lxGL4qY57W6tL548x1fbEaiMMAEbCjjukXIMuOU1sxR0ZNkWLWVVBgISlAN1eOuFTMZOvexaF8eU5FvdLAKWbH5qWG02p01dNQmJr/1tdfQyWpmr4rskUGTqUNqa6owJQ7d1aTWt26Yo9ZDnun9YpyY1xg6upWU+wi+UVkX/4kSl9TX5HEmSMsJ8ybe4vYbS2KZKDJr5BcUzVORraPBEq0hibyiQNoRD8vaLwloSbvfRLAE2/rSbDtBk2TSwbP5/q/VlSqEN2saNgco8TSzzdDOYdUUETgSHBQO1VPe11NK4cZDF1cBWWQa31t3HpfCVXGG6gmRLkZn/vfCy5qJZNs38Dtq+fPHxZ+EJrPkydJP4v+9eftBHKkjNI0M= 88 | file: "${TRAVIS_TAG}-${TARGET_DESC}.tar.gz" 89 | on: 90 | repo: daboross/screeps-rs 91 | tags: true 92 | condition: ("$DEPLOY_ONLY" = true || -z "$TEST_ONLY") 93 | skip_cleanup: true 94 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "network", 4 | "ui", 5 | ] 6 | 7 | [profile.release] 8 | opt-level = 3 9 | lto = true 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License for source files in src/rust and src/maps 2 | ================================================= 3 | 4 | Rust Screeps Conrod Client 5 | 6 | Copyright (c) 2017 David Ross 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | License for Akashi.ttf in src/ttf/ 15 | ==================================== 16 | 17 | Copyright (c) 2007 - 2013 Ten by Twenty http://tenbytwenty.com. 18 | 19 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 20 | This license is copied below, and is also available with a FAQ at: 21 | http://scripts.sil.org/OFL 22 | 23 | 24 | ----------------------------------------------------------- 25 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 26 | ----------------------------------------------------------- 27 | 28 | PREAMBLE 29 | The goals of the Open Font License (OFL) are to stimulate worldwide 30 | development of collaborative font projects, to support the font creation 31 | efforts of academic and linguistic communities, and to provide a free and 32 | open framework in which fonts may be shared and improved in partnership 33 | with others. 34 | 35 | The OFL allows the licensed fonts to be used, studied, modified and 36 | redistributed freely as long as they are not sold by themselves. The 37 | fonts, including any derivative works, can be bundled, embedded, 38 | redistributed and/or sold with any software provided that any reserved 39 | names are not used by derivative works. The fonts and derivatives, 40 | however, cannot be released under any other type of license. The 41 | requirement for fonts to remain under this license does not apply 42 | to any document created using the fonts or their derivatives. 43 | 44 | DEFINITIONS 45 | "Font Software" refers to the set of files released by the Copyright 46 | Holder(s) under this license and clearly marked as such. This may 47 | include source files, build scripts and documentation. 48 | 49 | "Reserved Font Name" refers to any names specified as such after the 50 | copyright statement(s). 51 | 52 | "Original Version" refers to the collection of Font Software components as 53 | distributed by the Copyright Holder(s). 54 | 55 | "Modified Version" refers to any derivative made by adding to, deleting, 56 | or substituting -- in part or in whole -- any of the components of the 57 | Original Version, by changing formats or by porting the Font Software to a 58 | new environment. 59 | 60 | "Author" refers to any designer, engineer, programmer, technical 61 | writer or other person who contributed to the Font Software. 62 | 63 | PERMISSION & CONDITIONS 64 | Permission is hereby granted, free of charge, to any person obtaining 65 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 66 | redistribute, and sell modified and unmodified copies of the Font 67 | Software, subject to the following conditions: 68 | 69 | 1) Neither the Font Software nor any of its individual components, 70 | in Original or Modified Versions, may be sold by itself. 71 | 72 | 2) Original or Modified Versions of the Font Software may be bundled, 73 | redistributed and/or sold with any software, provided that each copy 74 | contains the above copyright notice and this license. These can be 75 | included either as stand-alone text files, human-readable headers or 76 | in the appropriate machine-readable metadata fields within text or 77 | binary files as long as those fields can be easily viewed by the user. 78 | 79 | 3) No Modified Version of the Font Software may use the Reserved Font 80 | Name(s) unless explicit written permission is granted by the corresponding 81 | Copyright Holder. This restriction only applies to the primary font name as 82 | presented to the users. 83 | 84 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 85 | Software shall not be used to promote, endorse or advertise any 86 | Modified Version, except to acknowledge the contribution(s) of the 87 | Copyright Holder(s) and the Author(s) or with their explicit written 88 | permission. 89 | 90 | 5) The Font Software, modified or unmodified, in part or in whole, 91 | must be distributed entirely under this license, and must not be 92 | distributed under any other license. The requirement for fonts to 93 | remain under this license does not apply to any document created 94 | using the Font Software. 95 | 96 | TERMINATION 97 | This license becomes null and void if any of the above conditions are 98 | not met. 99 | 100 | DISCLAIMER 101 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 102 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 103 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 104 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 105 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 106 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 107 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 108 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 109 | OTHER DEALINGS IN THE FONT SOFTWARE. 110 | 111 | All products may be used for personal or commercial purposes, however they may not be redistributed or sold. 112 | 113 | If you've enjoyed the free products available here at tenbytwenty.com we'd really appreciate a small contribution toward the running of the site and the development of future products. 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | screeps-rs 2 | ========== 3 | [![Linux Build Status][travis-image]][travis-builds] 4 | [![Windows Build Status][appveyor-image]][appveyor-builds] 5 | 6 | WIP native screeps client using [Rust] and [conrod]. 7 | 8 | Screeps is a true programming MMO where users upload JavaScript code to power their online empires. 9 | 10 | ![map rendering screenshot][map-image] 11 | 12 | ![zoomed out screenshot][strategic-view] 13 | 14 | This client is built on three main projects: 15 | - [`rust-screeps-api`] implements HTTP calls, endpoints and json result parsing 16 | - [`screeps-rs-network`] implements result caching, keeping track of http and websocket connections, and providing an 'event' api 17 | - [`screeps-rs-ui`] implements rendering and a UI 18 | 19 | [`rust-screeps-api`] can: 20 | 21 | - Connect to screeps.com with HTTP calls and websocket connections 22 | - Authenticate 23 | - Retrieve room terrain, map room overviews, basic user information and room details. 24 | 25 | [`screeps-rs`] can: 26 | 27 | - Connect to screeps.com 28 | - Login through a UI 29 | - Render basic room terrain, map view, and information of the logged in user. 30 | 31 | Eventually, this will be able to connect to both the [official server][screeps] and any [private server][screeps-os] instances run by users. 32 | 33 | Running: 34 | - If you're on Ubuntu 17.10+, or on another Wayland Linux: (see [glutin#949]) 35 | - install "libegl1-mesa-dev" 36 | - soft-link `libwayland-egl.so.1` to `libwayland-egl.so` in your system's lib dir. On ubuntu: 37 | 38 | ``` 39 | cd /usr/lib/x86_64-linux-gnu/ 40 | sudo ln -s libwayland-egl.so.1 libwayland-egl.so 41 | ``` 42 | 43 | Neighbor projects: 44 | 45 | - APIs: 46 | - [`python-screeps`] implements a compact screeps API interface in python 47 | - [`node-screeps-api`] implements an interface for the screeps API in node.js 48 | - Clients: 49 | - [`ricochet1k/screeps-client`] implements a full screeps room viewer in browser JavaScript 50 | - [`ags131/screeps-client`] implements a slightly-less-full screeps room viewer in browser JavaScript 51 | - [`screeps-silica`] is directly connected to screeps-rs, using Scala to accomplish the same goals 52 | - [`Screeps3D`] is a native 3D screeps client built using Unity3D 53 | 54 | [`screeps-rs`] uses the `Akashi` font. It is included with permission from [Ten by Twenty][ten-by-twenty]. 55 | 56 | [travis-image]: https://travis-ci.org/daboross/screeps-rs.svg?branch=master 57 | [travis-builds]: https://travis-ci.org/daboross/screeps-rs 58 | [appveyor-image]: https://ci.appveyor.com/api/projects/status/github/daboross/screeps-rs?branch=master&svg=true 59 | [appveyor-builds]: https://ci.appveyor.com/project/daboross/screeps-rs 60 | [rust]: https://www.rust-lang.org/ 61 | [conrod]: https://github.com/PistonDevelopers/conrod/ 62 | [`rust-screeps-api`]: https://github.com/daboross/rust-screeps-api 63 | [`screeps-rs-network`]: network/ 64 | [`screeps-rs-ui`]: ui/ 65 | [`screeps-rs`]: https://github.com/daboross/screeps-rs 66 | [`python-screeps`]: https://github.com/screepers/python-screeps/ 67 | [`node-screeps-api`]: https://github.com/screepers/node-screeps-api 68 | [`screeps-silica`]: https://github.com/daboross/screeps-silica/ 69 | [`ricochet1k/screeps-client`]: https://github.com/ricochet1k/screeps-client 70 | [`ags131/screeps-client`]: https://github.com/ags131/screeps-client 71 | [screeps]: https://screeps.com 72 | [screeps-os]: https://github.com/screeps/screeps/ 73 | [ten-by-twenty]: http://tenbytwenty.com/ 74 | [map-image]: docs/map-render.png 75 | [strategic-view]: docs/strategic-view.png 76 | [tomaka/glutin#949]: https://github.com/tomaka/glutin/issues/949 77 | [`Screeps3D`]: https://github.com/bonzaiferroni/Screeps3D 78 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Based on the "trust" template v0.1.1 2 | # https://github.com/japaric/trust/tree/v0.1.1 3 | 4 | environment: 5 | matrix: 6 | - TARGET: i686-pc-windows-msvc 7 | TARGET_DESC: windows-i686 8 | RUST_VERSION: nightly 9 | - TARGET: x86_64-pc-windows-msvc 10 | TARGET_DESC: windows-x86_64 11 | RUST_VERSION: nightly 12 | 13 | matrix: 14 | fast_finish: true 15 | allow_failures: 16 | - RUST_VERSION: nightly 17 | 18 | install: 19 | - curl -sSf -o rustup-init.exe https://win.rustup.rs/ 20 | - rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION% 21 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 22 | - rustc -Vv 23 | - cargo -V 24 | 25 | test_script: 26 | - if [%APPVEYOR_REPO_TAG%] == [false] (cargo build --verbose --target %TARGET%) 27 | - if [%APPVEYOR_REPO_TAG%] == [false] (cargo test --verbose -p screeps-rs-network --target %TARGET%) 28 | - if [%APPVEYOR_REPO_TAG%] == [false] (cargo test --verbose -p screeps-rs-ui --target %TARGET%) 29 | 30 | cache: 31 | - target 32 | 33 | # Disable the appveyor build step so we can just build the rust project instead. 34 | build: off 35 | 36 | before_deploy: 37 | - cargo build --release -p screeps-rs-ui --target %TARGET% 38 | - ps: scripts\predeploy.ps1 39 | 40 | deploy: 41 | provider: GitHub 42 | artifact: /.*\.zip/ 43 | auth_token: 44 | secure: X18qThheIxFsNHaDdLRJPSZN6e9AlZ8NyNogB+/vISdgUsRHTJYdPF4DHXbA0DxH 45 | on: 46 | appveyor_repo_tag: true 47 | -------------------------------------------------------------------------------- /docs/colors.js: -------------------------------------------------------------------------------- 1 | // These are the colors the official client uses for differentl minerals 2 | global.RES_COLORS = { 3 | H: '#989898', 4 | O: '#989898', 5 | U: '#48C5E5', 6 | L: '#24D490', 7 | K: '#9269EC', 8 | Z: '#D9B478', 9 | X: '#F26D6F', 10 | energy: '#FEE476', 11 | power: '#F1243A', 12 | 13 | OH: '#B4B4B4', 14 | ZK: '#B4B4B4', 15 | UL: '#B4B4B4', 16 | G: '#FFFFFF', 17 | 18 | UH: '#50D7F9', 19 | UO: '#50D7F9', 20 | KH: '#A071FF', 21 | KO: '#A071FF', 22 | LH: '#00F4A2', 23 | LO: '#00F4A2', 24 | ZH: '#FDD388', 25 | ZO: '#FDD388', 26 | GH: '#FFFFFF', 27 | GO: '#FFFFFF', 28 | 29 | UH2O: '#50D7F9', 30 | UHO2: '#50D7F9', 31 | KH2O: '#A071FF', 32 | KHO2: '#A071FF', 33 | LH2O: '#00F4A2', 34 | LHO2: '#00F4A2', 35 | ZH2O: '#FDD388', 36 | ZHO2: '#FDD388', 37 | GH2O: '#FFFFFF', 38 | GHO2: '#FFFFFF', 39 | 40 | XUH2O: '#50D7F9', 41 | XUHO2: '#50D7F9', 42 | XKH2O: '#A071FF', 43 | XKHO2: '#A071FF', 44 | XLH2O: '#00F4A2', 45 | XLHO2: '#00F4A2', 46 | XZH2O: '#FDD388', 47 | XZHO2: '#FDD388', 48 | XGH2O: '#FFFFFF', 49 | XGHO2: '#FFFFFF' 50 | } 51 | -------------------------------------------------------------------------------- /docs/map-render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daboross/screeps-rs/5c92cbd44f4342e206baa538d8897cb4de942361/docs/map-render.png -------------------------------------------------------------------------------- /docs/strategic-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daboross/screeps-rs/5c92cbd44f4342e206baa538d8897cb4de942361/docs/strategic-view.png -------------------------------------------------------------------------------- /network/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screeps-rs-network" 3 | version = "0.1.0" 4 | authors = ["David Ross "] 5 | description = "Networking and caching for a work in progress native Screeps client." 6 | 7 | repository = "https://github.com/daboross/screeps-rs" 8 | 9 | readme = "../README.md" 10 | 11 | keywords = [] 12 | categories = ["games", "networking"] 13 | license = "MIT" 14 | 15 | [dependencies] 16 | # Networking 17 | futures = "0.1" 18 | futures-cpupool = "0.1" 19 | tokio-core = "0.1" 20 | hyper = "0.11" 21 | hyper-tls = "0.1" 22 | websocket = "0.20" 23 | url = "1.0" 24 | screeps-api = { version = "0.4", default-features = false } 25 | # Caching 26 | time = "0.1" 27 | bincode = "1.0" 28 | sled = "0.15" 29 | directories = "0.8" 30 | serde = "1.0" 31 | serde_json = "1.0" 32 | serde_derive = "1.0" 33 | arrayvec = { version = "0.4", features = ["serde-1"] } 34 | # Logging 35 | log = "0.4" 36 | -------------------------------------------------------------------------------- /network/src/diskcache.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use std::borrow::Cow; 3 | use std::{fs, io}; 4 | 5 | use screeps_api::{RoomName, TerrainGrid}; 6 | use screeps_api::data::room_name::RoomNameAbsoluteCoordinates; 7 | use futures_cpupool::CpuPool; 8 | use futures::{future, stream, Future, Stream}; 9 | use tokio_core::reactor; 10 | 11 | use {directories, bincode, sled, time}; 12 | 13 | // TODO: cache per server connection. 14 | const OLD_DB_FILE_NAME: &'static str = "cache"; 15 | 16 | const DB_FILE_NAME: &'static str = "cache-v0.2"; 17 | 18 | #[inline(always)] 19 | fn keep_terrain_for() -> time::Duration { 20 | time::Duration::days(1) 21 | } 22 | 23 | mod errors { 24 | use std::{fmt, io}; 25 | use sled; 26 | 27 | #[derive(Debug)] 28 | pub enum CreationError { 29 | DirectoryCreation(io::Error), 30 | DatabaseDeletion(io::Error), 31 | Sled(sled::Error<()>), 32 | } 33 | 34 | impl CreationError { 35 | pub fn directory_creation(e: io::Error) -> Self { 36 | CreationError::DirectoryCreation(e) 37 | } 38 | 39 | pub fn database_deletion(e: io::Error) -> Self { 40 | CreationError::DatabaseDeletion(e) 41 | } 42 | } 43 | 44 | impl From> for CreationError { 45 | fn from(e: sled::Error<()>) -> Self { 46 | CreationError::Sled(e) 47 | } 48 | } 49 | 50 | impl fmt::Display for CreationError { 51 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 52 | match *self { 53 | CreationError::DirectoryCreation(ref e) => write!(f, "error creating cache directory: {}", e), 54 | CreationError::DatabaseDeletion(ref e) => write!(f, "error deleting corrupted cache database: {}", e), 55 | CreationError::Sled(ref e) => write!(f, "sled database error: {:?}", e), 56 | } 57 | } 58 | } 59 | } 60 | 61 | pub use self::errors::CreationError; 62 | 63 | #[derive(Clone)] 64 | pub struct Cache { 65 | database: sled::Tree, 66 | access_pool: CpuPool, 67 | } 68 | 69 | impl Cache { 70 | pub fn load() -> Result { 71 | let dirs = directories::ProjectDirs::from("net.daboross", "OpenScreeps", "screeps-rs"); 72 | let mut path = dirs.cache_dir().to_owned(); 73 | 74 | fs::create_dir_all(&path).map_err(CreationError::directory_creation)?; 75 | 76 | path.push(OLD_DB_FILE_NAME); 77 | 78 | if let Err(e) = fs::remove_dir_all(&path) { 79 | if e.kind() != io::ErrorKind::NotFound { 80 | warn!( 81 | "error deleting old cache directory ({}): {}", 82 | path.display(), 83 | e 84 | ); 85 | } 86 | } 87 | 88 | path.pop(); 89 | 90 | path.push(DB_FILE_NAME); 91 | 92 | debug!("Opening cache from file {}", path.display()); 93 | 94 | let config = sled::ConfigBuilder::default().path(&path).build(); 95 | 96 | let database_result = match sled::Tree::start(config.clone()) { 97 | Err(sled::Error::Corruption { .. }) => { 98 | warn!("deleting corrupted database: {}", path.display()); 99 | fs::remove_file(path).map_err(CreationError::database_deletion)?; 100 | sled::Tree::start(config) 101 | } 102 | x => x, 103 | }; 104 | 105 | let database = database_result?; 106 | 107 | Ok(Cache { 108 | database: database, 109 | access_pool: CpuPool::new(3), 110 | }) 111 | } 112 | 113 | pub fn start_cache_clean_task(&self, handle: &reactor::Handle) -> io::Result<()> { 114 | let pool = self.access_pool.clone(); 115 | let db = self.database.clone(); 116 | // run once on app startup, then once every hour. 117 | let stream = stream::once(Ok(())).chain(reactor::Interval::new( 118 | Duration::from_secs(60 * 60), 119 | handle, 120 | )?); 121 | 122 | handle.spawn( 123 | stream 124 | .then(move |result| { 125 | if let Err(e) = result { 126 | warn!("error with cache cleanup interval: {:?}", e); 127 | } 128 | 129 | let db = db.clone(); 130 | 131 | pool.spawn_fn(move || { 132 | let result = cleanup_database(&db); 133 | if let Err(e) = result { 134 | warn!("error cleaning up database file: {:?}", e); 135 | } 136 | 137 | future::ok(()) 138 | }) 139 | }) 140 | .fold((), |(), _| future::ok(())), 141 | ); 142 | 143 | Ok(()) 144 | } 145 | 146 | pub fn set_terrain( 147 | &self, 148 | server: &str, 149 | shard: Option<&str>, 150 | room: RoomName, 151 | data: &TerrainGrid, 152 | ) -> impl Future> { 153 | let key = ShardCacheKey::terrain(server, shard, room).encode(); 154 | 155 | let to_store = CacheEntry { 156 | fetched: time::get_time(), 157 | data: data, 158 | }; 159 | 160 | let value = 161 | bincode::serialize(&to_store).expect("expected serializing data using bincode to unequivocally succeed."); 162 | 163 | let sent_database = self.database.clone(); 164 | 165 | self.access_pool 166 | .spawn_fn(move || sent_database.set(key, value)) 167 | } 168 | 169 | pub fn get_terrain( 170 | &self, 171 | server: &str, 172 | shard: Option<&str>, 173 | room: RoomName, 174 | ) -> impl Future, Error = sled::Error<()>> { 175 | let key = ShardCacheKey::terrain(server, shard, room).encode(); 176 | 177 | let sent_database = self.database.clone(); 178 | 179 | self.access_pool.spawn_fn(move || { 180 | let parsed = match sent_database.get(&key)? { 181 | Some(db_vector) => match bincode::deserialize_from::<_, CacheEntry<_>>(&mut &*db_vector) { 182 | Ok(v) => Some(v.data), 183 | Err(e) => { 184 | warn!( 185 | "cache database entry found corrupted.\ 186 | \nEntry: (terrain:{})\ 187 | \nDecode error: {}\ 188 | \nRemoving data.", 189 | room, e 190 | ); 191 | 192 | sent_database.del(&key)?; 193 | 194 | None 195 | } 196 | }, 197 | None => None, 198 | }; 199 | 200 | Ok(parsed) 201 | }) 202 | } 203 | } 204 | 205 | fn cleanup_database(db: &sled::Tree) -> Result<(), sled::Error<()>> { 206 | let to_remove = db.iter() 207 | .filter_map(|result| { 208 | let (key, value) = match result { 209 | Ok(v) => v, 210 | Err(e) => return Some(Err(e)), 211 | }; 212 | 213 | let parsed_key = match ShardCacheKey::decode(&key) { 214 | Ok(v) => v, 215 | Err(e) => { 216 | warn!( 217 | "when clearing old cache: unknown key '{:?}' found (read error: {}). Deleting.", 218 | key, e 219 | ); 220 | return Some(Ok(key)); 221 | } 222 | }; 223 | 224 | let now = time::get_time(); 225 | 226 | let keep_result = match parsed_key.key { 227 | CacheKeyInner::Terrain(_) => bincode::deserialize::>(&value) 228 | .map(|entry| now - entry.fetched < keep_terrain_for()), 229 | }; 230 | 231 | match keep_result { 232 | Ok(true) => { 233 | trace!("keeping cache entry ({:?})", parsed_key); 234 | None 235 | } 236 | Ok(false) => { 237 | debug!("removing cache entry ({:?}): old data.", parsed_key); 238 | Some(Ok(key)) 239 | } 240 | Err(e) => { 241 | debug!( 242 | "removing cache entry ({:?}): invalid data ({})", 243 | parsed_key, e 244 | ); 245 | Some(Ok(key)) 246 | } 247 | } 248 | }) 249 | .collect::, _>>()?; 250 | 251 | for key in to_remove { 252 | db.del(&key)?; 253 | } 254 | 255 | Ok(()) 256 | } 257 | 258 | #[derive(Clone, Debug, Serialize, Deserialize)] 259 | struct CacheEntry { 260 | #[serde(with = "timespec_serialize_seconds")] 261 | fetched: time::Timespec, 262 | data: T, 263 | } 264 | 265 | #[derive(Clone, Debug, Serialize, Deserialize)] 266 | enum CacheKeyInner { 267 | // NOTE: whenever adding a variant, the length return in 'encode' must be tested and updated. 268 | Terrain(RoomNameAbsoluteCoordinates), 269 | } 270 | 271 | #[derive(Clone, Debug, Serialize, Deserialize)] 272 | struct ShardCacheKey<'a> { 273 | server: Cow<'a, str>, 274 | shard: Option>, 275 | key: CacheKeyInner, 276 | } 277 | 278 | impl ShardCacheKey<'static> { 279 | fn decode(bytes: &[u8]) -> Result { 280 | bincode::deserialize(bytes) 281 | } 282 | } 283 | 284 | impl<'a> ShardCacheKey<'a> { 285 | fn terrain(server: T, shard: Option, room_name: RoomName) -> Self 286 | where 287 | T: Into>, 288 | U: Into>, 289 | { 290 | ShardCacheKey { 291 | server: server.into(), 292 | shard: shard.map(Into::into), 293 | key: CacheKeyInner::Terrain(room_name.into()), 294 | } 295 | } 296 | 297 | /// Returns bytes representing this cache key, encoded using `bincode`. 298 | fn encode(&self) -> Vec { 299 | bincode::serialize(self) 300 | .expect("expected writing cache key with infinite size to be within that infinite size.") 301 | } 302 | } 303 | 304 | mod timespec_serialize_seconds { 305 | use time::Timespec; 306 | use serde::{Deserialize, Deserializer, Serializer}; 307 | 308 | pub fn serialize(date: &Timespec, serializer: S) -> Result 309 | where 310 | S: Serializer, 311 | { 312 | serializer.serialize_i64(date.sec) 313 | } 314 | 315 | pub fn deserialize<'de, D>(deserializer: D) -> Result 316 | where 317 | D: Deserializer<'de>, 318 | { 319 | Ok(Timespec::new(i64::deserialize(deserializer)?, 0)) 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /network/src/event.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use std::cell::RefCell; 3 | use std::collections::HashMap; 4 | 5 | use {screeps_api, time, websocket}; 6 | 7 | use screeps_api::RoomName; 8 | use screeps_api::websocket::types::room::objects::KnownRoomObject; 9 | 10 | #[derive(Default, Debug)] 11 | pub struct MapCacheData { 12 | // TODO: should we be re-fetching terrain at some point, or is it alright to leave it forever in memory? 13 | // The client can always restart to clear this. 14 | /// Terrains. Terrain will be None if the room name in question is not a valid room name. 15 | pub terrain: HashMap)>, 16 | /// Map views, the Timespec is when the data was fetched. 17 | pub map_views: HashMap, 18 | /// Single current known view of a room. 19 | /// 20 | /// TODO: keep track of history of room's we've subscribed to in the past and what tick each data was updated. 21 | /// TODO: possibly keep history for each for an 'instant replay' functionality. 22 | /// TODO: handle unknown room objects better: given that we know they have at least an 'x' and 'y' property, we 23 | /// could definitely do a question mark in the UI with a drop-down for JSON properties the object has. 24 | pub detail_view: Option<(RoomName, HashMap)>, 25 | } 26 | 27 | pub type MapCache = Rc>; 28 | 29 | #[derive(Debug)] 30 | pub enum NetworkEvent { 31 | Login { 32 | username: String, 33 | result: Result<(), screeps_api::Error>, 34 | }, 35 | MyInfo { 36 | result: Result, 37 | }, 38 | ShardList { 39 | result: Result>, screeps_api::Error>, 40 | }, 41 | RoomTerrain { 42 | room_name: screeps_api::RoomName, 43 | result: Result, 44 | }, 45 | WebsocketHttpError { 46 | error: screeps_api::Error, 47 | }, 48 | WebsocketError { 49 | error: websocket::WebSocketError, 50 | }, 51 | WebsocketParseError { 52 | error: screeps_api::websocket::parsing::ParseError, 53 | }, 54 | MapView { 55 | room_name: screeps_api::RoomName, 56 | result: screeps_api::websocket::RoomMapViewUpdate, 57 | }, 58 | RoomView { 59 | room_name: screeps_api::RoomName, 60 | result: screeps_api::websocket::RoomUpdate, 61 | }, 62 | } 63 | 64 | impl NetworkEvent { 65 | pub fn error(&self) -> Option<&screeps_api::Error> { 66 | match *self { 67 | NetworkEvent::Login { ref result, .. } => result.as_ref().err(), 68 | NetworkEvent::MyInfo { ref result, .. } => result.as_ref().err(), 69 | NetworkEvent::ShardList { ref result, .. } => result.as_ref().err(), 70 | NetworkEvent::RoomTerrain { ref result, .. } => result.as_ref().err(), 71 | NetworkEvent::WebsocketHttpError { ref error } => Some(error), 72 | NetworkEvent::MapView { .. } 73 | | NetworkEvent::RoomView { .. } 74 | | NetworkEvent::WebsocketError { .. } 75 | | NetworkEvent::WebsocketParseError { .. } => None, 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /network/src/lib.rs: -------------------------------------------------------------------------------- 1 | // impl Trait 2 | #![feature(conservative_impl_trait)] 3 | 4 | // Network 5 | extern crate futures; 6 | extern crate hyper; 7 | extern crate hyper_tls; 8 | extern crate screeps_api; 9 | extern crate tokio_core; 10 | extern crate url; 11 | extern crate websocket; 12 | 13 | // Caching 14 | extern crate bincode; 15 | extern crate directories; 16 | extern crate futures_cpupool; 17 | extern crate serde; 18 | #[macro_use] 19 | extern crate serde_derive; 20 | extern crate serde_json; 21 | extern crate sled; 22 | extern crate time; 23 | 24 | // Logging 25 | #[macro_use] 26 | extern crate log; 27 | 28 | pub mod request; 29 | pub mod event; 30 | pub mod memcache; 31 | pub mod diskcache; 32 | pub mod tokio; 33 | 34 | use std::fmt; 35 | pub use url::Url; 36 | 37 | pub use request::{LoginDetails, NotLoggedIn, Request, SelectedRooms}; 38 | pub use event::{MapCache, MapCacheData, NetworkEvent}; 39 | pub use memcache::{ErrorEvent, LoginState, MemCache}; 40 | pub use tokio::Handler as TokioHandler; 41 | 42 | /// The backend connection handler for handling requests. Interface for `memcache` module to use. 43 | pub trait ScreepsConnection { 44 | /// Send a request. Any and all errors will be returned in the future via poll() 45 | fn send(&mut self, r: Request); 46 | 47 | /// Get the next available event if any, or return None if nothing new has happened. 48 | /// 49 | /// Should not error if any threads have disconnected. 50 | fn poll(&mut self) -> Option; 51 | } 52 | 53 | /// An error for the `Notify` trait to output. 54 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 55 | pub struct Disconnected; 56 | 57 | pub trait Notify: Clone + Send + 'static { 58 | fn wakeup(&self) -> Result<(), Disconnected>; 59 | } 60 | 61 | #[derive(Clone, Hash, PartialEq, Eq)] 62 | pub struct ConnectionSettings { 63 | /// Connection URL (including /api/) 64 | pub api_url: url::Url, 65 | /// Username to login with 66 | pub username: String, 67 | /// Password to login with 68 | pub password: String, 69 | /// Shard to process requests on 70 | pub shard: Option, 71 | } 72 | 73 | impl ConnectionSettings { 74 | pub fn new>>(username: String, password: String, shard: T) -> ConnectionSettings { 75 | ConnectionSettings::with_url( 76 | screeps_api::DEFAULT_OFFICIAL_API_URL 77 | .parse() 78 | .expect("expected hardcoded url to parse"), 79 | username, 80 | password, 81 | shard, 82 | ) 83 | } 84 | 85 | pub fn with_url>>( 86 | api_url: Url, 87 | username: String, 88 | password: String, 89 | shard: T, 90 | ) -> ConnectionSettings { 91 | ConnectionSettings { 92 | api_url: api_url, 93 | username: username, 94 | password: password, 95 | shard: shard.into(), 96 | } 97 | } 98 | } 99 | 100 | impl fmt::Debug for ConnectionSettings { 101 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 102 | f.debug_struct("ConnectionSettings") 103 | .field("username", &self.username) 104 | .field("password", &"") 105 | .field("shard", &self.shard) 106 | .finish() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /network/src/memcache/memory.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use std::sync::Arc; 3 | use std::cell::Ref; 4 | use std::cell::RefCell; 5 | use std::collections::HashMap; 6 | 7 | use screeps_api::{self, RoomName}; 8 | use time::{self, Duration}; 9 | 10 | use super::{ErrorEvent, LoginState}; 11 | use event::{MapCacheData, NetworkEvent}; 12 | use request::{Request, SelectedRooms}; 13 | use {ConnectionSettings, ScreepsConnection}; 14 | 15 | #[derive(Copy, Clone, Debug)] 16 | struct TimeoutValue { 17 | /// T retrieved, time it was retrieved. 18 | value: Option<(T, time::Timespec)>, 19 | /// Last call made to server, set to None when a value or error is received. 20 | last_send: Option, 21 | } 22 | 23 | impl Default for TimeoutValue { 24 | fn default() -> Self { 25 | TimeoutValue { 26 | value: None, 27 | last_send: None, 28 | } 29 | } 30 | } 31 | 32 | impl TimeoutValue { 33 | fn event(&mut self, result: Result) -> Result<(), E> { 34 | self.last_send = None; 35 | match result { 36 | Ok(v) => { 37 | self.value = Some((v, time::get_time())); 38 | Ok(()) 39 | } 40 | Err(e) => Err(e), 41 | } 42 | } 43 | 44 | /// Gets whether or not we should launch a request for this resource. This is somewhat 45 | /// independent of whether we already have an old copy or not. 46 | /// 47 | /// If cache_for is None, values will be held indefinitely without re-requesting. 48 | fn should_request(&self, cache_for: Option, timeout_for_request: Duration) -> bool { 49 | let now = time::get_time(); 50 | 51 | match self.value { 52 | Some((_, last_request)) => match cache_for { 53 | Some(cache_for) => if last_request + cache_for > now { 54 | false 55 | } else { 56 | match self.last_send { 57 | Some(send_time) => send_time + timeout_for_request < now, 58 | None => true, 59 | } 60 | }, 61 | None => false, 62 | }, 63 | None => match self.last_send { 64 | Some(t) => t + timeout_for_request < now, 65 | None => true, 66 | }, 67 | } 68 | } 69 | 70 | fn requested(&mut self) { 71 | self.last_send = Some(time::get_time()); 72 | } 73 | 74 | /// Gets the value if there is any. This is independent of whether or not we should make a new request. 75 | fn get(&self) -> Option<&T> { 76 | self.value.as_ref().map(|tuple| &tuple.0) 77 | } 78 | 79 | /// Resets the value to None. 80 | fn reset(&mut self) { 81 | self.value = None; 82 | self.last_send = None; 83 | } 84 | } 85 | 86 | #[derive(Default, Debug)] 87 | pub struct MemCache { 88 | login: TimeoutValue<()>, 89 | my_info: TimeoutValue, 90 | shard_list: TimeoutValue>>, 91 | rooms: Rc>, 92 | requested_rooms: HashMap, 93 | last_requested_room_info: Option, 94 | last_requested_focus_room: Option, 95 | } 96 | 97 | pub struct NetworkedMemCache<'a, T: ScreepsConnection + 'a> { 98 | cache: &'a mut MemCache, 99 | handler: &'a mut T, 100 | } 101 | 102 | impl MemCache { 103 | pub fn new() -> Self { 104 | Self::default() 105 | } 106 | 107 | fn event(&mut self, event: NetworkEvent) -> Result<(), ErrorEvent> { 108 | match event { 109 | NetworkEvent::Login { 110 | username: _, 111 | result, 112 | } => self.login.event(result)?, 113 | NetworkEvent::MyInfo { result } => self.my_info.event(result)?, 114 | NetworkEvent::ShardList { result } => self.shard_list.event(result)?, 115 | NetworkEvent::RoomTerrain { room_name, result } => { 116 | let terrain = match result { 117 | Ok(terrain) => Some(terrain), 118 | Err(err) => { 119 | if let &screeps_api::ErrorKind::Api(screeps_api::error::ApiError::InvalidRoom) = err.kind() { 120 | None 121 | } else { 122 | return Err(err.into()); 123 | } 124 | } 125 | }; 126 | self.rooms 127 | .borrow_mut() 128 | .terrain 129 | .insert(room_name, (time::get_time(), terrain)); 130 | } 131 | NetworkEvent::MapView { room_name, result } => { 132 | self.rooms 133 | .borrow_mut() 134 | .map_views 135 | .insert(room_name, (time::get_time(), result)); 136 | } 137 | NetworkEvent::RoomView { room_name, result } => { 138 | use serde_json; 139 | use std::collections::hash_map::Entry::*; 140 | 141 | let mut data = self.rooms.borrow_mut(); 142 | 143 | let mut new_detail_view = None; 144 | 145 | match data.detail_view.as_mut() { 146 | Some(&mut (name, ref mut map)) if name == room_name => { 147 | for (id, obj_update) in result.objects.into_iter() { 148 | if obj_update.is_null() { 149 | map.remove(&id); 150 | } else { 151 | match map.entry(id.clone()) { 152 | Occupied(entry) => { 153 | let obj_data = entry.into_mut(); 154 | 155 | obj_data.update(obj_update.clone()).map_err(|e| { 156 | ErrorEvent::room_view(format!( 157 | "update for id {} in room {} did not \ 158 | parse: existing value: {:?}, failed \ 159 | update: {:?}, error: {}", 160 | id, room_name, obj_data, obj_update, e 161 | )) 162 | })?; 163 | } 164 | Vacant(entry) => { 165 | entry.insert(serde_json::from_value(obj_update.clone()).map_err(|e| { 166 | ErrorEvent::room_view(format!( 167 | "data for id {} in room {} did not \ 168 | parse: failed json: {:?}, error: {}", 169 | id, room_name, obj_update, e 170 | )) 171 | })?); 172 | } 173 | } 174 | } 175 | } 176 | } 177 | _ => { 178 | let new_map = result 179 | .objects 180 | .into_iter() 181 | .map(|(id, obj_json)| { 182 | let data = serde_json::from_value(obj_json.clone()).map_err(|e| { 183 | ErrorEvent::room_view(format!( 184 | "data for id {} in room {} did not parse: \ 185 | failed json: {:?}, error: {}", 186 | id, room_name, obj_json, e 187 | )) 188 | })?; 189 | Ok((id, data)) 190 | }) 191 | .collect::, ErrorEvent>>()?; 192 | 193 | new_detail_view = Some(new_map); 194 | } 195 | } 196 | 197 | if let Some(view) = new_detail_view { 198 | data.detail_view = Some((room_name, view)); 199 | } 200 | } 201 | NetworkEvent::WebsocketError { error } => return Err(ErrorEvent::WebsocketError(error)), 202 | NetworkEvent::WebsocketHttpError { error } => return Err(ErrorEvent::ErrorOccurred(error)), 203 | NetworkEvent::WebsocketParseError { error } => return Err(ErrorEvent::WebsocketParse(error)), 204 | } 205 | 206 | Ok(()) 207 | } 208 | 209 | pub fn login_state(&self) -> LoginState { 210 | match self.login.get() { 211 | Some(_) => LoginState::LoggedIn, 212 | None => match self.login.should_request(None, Duration::seconds(90)) { 213 | false => LoginState::TryingToLogin, 214 | true => LoginState::NotLoggedIn, 215 | }, 216 | } 217 | } 218 | 219 | pub fn align<'a, T, F, E>( 220 | &'a mut self, 221 | handler: &'a mut T, 222 | mut error_callback: F, 223 | mut additional_event_receiver: E, 224 | ) -> NetworkedMemCache<'a, T> 225 | where 226 | T: ScreepsConnection, 227 | F: FnMut(ErrorEvent), 228 | E: FnMut(&NetworkEvent), 229 | { 230 | while let Some(evt) = handler.poll() { 231 | debug!("[cache] Got event {:?}", evt); 232 | additional_event_receiver(&evt); 233 | if let Err(e) = self.event(evt) { 234 | if let ErrorEvent::NotLoggedIn = e { 235 | self.login.reset(); 236 | } 237 | error_callback(e); 238 | } 239 | } 240 | 241 | NetworkedMemCache { 242 | cache: self, 243 | handler: handler, 244 | } 245 | } 246 | } 247 | 248 | impl<'a, C: ScreepsConnection> NetworkedMemCache<'a, C> { 249 | pub fn login(&mut self) { 250 | self.handler.send(Request::login()); 251 | } 252 | 253 | pub fn login_state(&self) -> LoginState { 254 | self.cache.login_state() 255 | } 256 | 257 | pub fn update_settings(&mut self, settings: ConnectionSettings) { 258 | self.handler.send(Request::ChangeSettings { 259 | settings: Arc::new(settings), 260 | }) 261 | } 262 | 263 | pub fn my_info(&mut self) -> Option<&screeps_api::MyInfo> { 264 | let holder = &mut self.cache.my_info; 265 | if holder.should_request(Some(Duration::minutes(10)), Duration::seconds(90)) { 266 | self.handler.send(Request::MyInfo); 267 | holder.requested(); 268 | } 269 | 270 | holder.get() 271 | } 272 | 273 | pub fn shard_list(&mut self) -> Option> { 274 | let holder = &mut self.cache.shard_list; 275 | if holder.should_request(Some(Duration::hours(6)), Duration::seconds(90)) { 276 | self.handler.send(Request::ShardList); 277 | holder.requested(); 278 | } 279 | 280 | holder.get().map(|o| o.as_ref().map(AsRef::as_ref)) 281 | } 282 | 283 | pub fn view_rooms(&mut self, rooms: SelectedRooms, focused: Option) -> &Rc> { 284 | if Some(rooms) != self.cache.last_requested_room_info { 285 | let borrowed = Ref::map(self.cache.rooms.borrow(), |cache| &cache.terrain); 286 | let rerequest_if_before = time::get_time() - Duration::seconds(90); 287 | for room_name in rooms { 288 | if !borrowed.contains_key(&room_name) { 289 | let resend = match self.cache.requested_rooms.get(&room_name) { 290 | Some(v) => v < &rerequest_if_before, 291 | None => true, 292 | }; 293 | 294 | if resend { 295 | self.cache 296 | .requested_rooms 297 | .insert(room_name, time::get_time()); 298 | self.handler.send(Request::room_terrain(room_name)); 299 | } 300 | } 301 | } 302 | self.handler.send(Request::subscribe_map_view(rooms)); 303 | } 304 | if focused != self.cache.last_requested_focus_room { 305 | self.handler.send(Request::focus_room(focused)); 306 | } 307 | &self.cache.rooms 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /network/src/memcache/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use {screeps_api, websocket}; 4 | 5 | pub use self::memory::{MemCache, NetworkedMemCache}; 6 | 7 | mod memory; 8 | 9 | pub enum ErrorEvent { 10 | NotLoggedIn, 11 | ErrorOccurred(screeps_api::Error), 12 | WebsocketError(websocket::WebSocketError), 13 | WebsocketParse(screeps_api::websocket::parsing::ParseError), 14 | RoomViewError(String), // TODO: granularity here. 15 | } 16 | 17 | impl From for ErrorEvent { 18 | fn from(_: screeps_api::NoToken) -> ErrorEvent { 19 | ErrorEvent::NotLoggedIn 20 | } 21 | } 22 | 23 | impl From for ErrorEvent { 24 | fn from(_: super::NotLoggedIn) -> ErrorEvent { 25 | ErrorEvent::NotLoggedIn 26 | } 27 | } 28 | 29 | impl From for ErrorEvent { 30 | fn from(err: screeps_api::Error) -> ErrorEvent { 31 | ErrorEvent::ErrorOccurred(err) 32 | } 33 | } 34 | 35 | impl From for ErrorEvent { 36 | fn from(err: screeps_api::websocket::parsing::ParseError) -> ErrorEvent { 37 | ErrorEvent::WebsocketParse(err) 38 | } 39 | } 40 | 41 | impl ErrorEvent { 42 | fn room_view(data: String) -> Self { 43 | ErrorEvent::RoomViewError(data) 44 | } 45 | } 46 | 47 | impl fmt::Display for ErrorEvent { 48 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 49 | match *self { 50 | ErrorEvent::NotLoggedIn => write!( 51 | f, 52 | "network connection attempted that is not available without logging in." 53 | ), 54 | ErrorEvent::ErrorOccurred(ref e) => e.fmt(f), 55 | ErrorEvent::WebsocketError(ref e) => e.fmt(f), 56 | ErrorEvent::WebsocketParse(ref e) => e.fmt(f), 57 | ErrorEvent::RoomViewError(ref e) => e.fmt(f), 58 | } 59 | } 60 | } 61 | 62 | #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] 63 | pub enum LoginState { 64 | NotLoggedIn, 65 | TryingToLogin, 66 | LoggedIn, 67 | } 68 | -------------------------------------------------------------------------------- /network/src/request.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | use std::sync::Arc; 3 | use std::fmt; 4 | 5 | use screeps_api::RoomName; 6 | 7 | use ConnectionSettings; 8 | use self::Request::*; 9 | 10 | /// Error for not being logged in, and trying to send a query requiring authentication. 11 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 12 | pub struct NotLoggedIn; 13 | 14 | /// Login username/password. 15 | #[derive(Clone, Hash, PartialEq, Eq)] 16 | pub struct LoginDetails { 17 | inner: Arc<(String, String)>, 18 | } 19 | 20 | impl LoginDetails { 21 | /// Creates a new login detail struct. 22 | pub fn new(username: String, password: String) -> Self { 23 | LoginDetails { 24 | inner: Arc::new((username, password)), 25 | } 26 | } 27 | 28 | /// Gets the username. 29 | pub fn username(&self) -> &str { 30 | &self.inner.0 31 | } 32 | 33 | /// Gets the password. 34 | pub fn password(&self) -> &str { 35 | &self.inner.1 36 | } 37 | } 38 | 39 | impl fmt::Debug for LoginDetails { 40 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 41 | f.debug_struct("LoginDetails") 42 | .field("username", &self.username()) 43 | .field("password", &"") 44 | .finish() 45 | } 46 | } 47 | 48 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] 49 | pub struct SelectedRooms { 50 | pub start: RoomName, 51 | pub end: RoomName, 52 | } 53 | 54 | impl SelectedRooms { 55 | #[inline] 56 | pub fn new(rooms: Range) -> Self { 57 | use std::cmp::{max, min}; 58 | 59 | let start = RoomName { 60 | x_coord: min(rooms.start.x_coord, rooms.end.x_coord), 61 | y_coord: min(rooms.start.y_coord, rooms.end.y_coord), 62 | }; 63 | let end = RoomName { 64 | x_coord: max(rooms.start.x_coord, rooms.end.x_coord), 65 | y_coord: max(rooms.start.y_coord, rooms.end.y_coord), 66 | }; 67 | 68 | SelectedRooms { 69 | start: start, 70 | end: end, 71 | } 72 | } 73 | 74 | #[inline] 75 | pub fn contains(&self, room: &RoomName) -> bool { 76 | (self.start.x_coord < room.x_coord && room.x_coord < self.end.x_coord) 77 | && (self.start.y_coord < room.y_coord && room.y_coord < self.end.y_coord) 78 | } 79 | } 80 | 81 | impl IntoIterator for SelectedRooms { 82 | type Item = RoomName; 83 | type IntoIter = IterSelectedRooms; 84 | 85 | fn into_iter(mut self) -> IterSelectedRooms { 86 | if self.start.x_coord > self.end.x_coord { 87 | ::std::mem::swap(&mut self.start.x_coord, &mut self.end.x_coord); 88 | } 89 | if self.start.y_coord > self.end.y_coord { 90 | ::std::mem::swap(&mut self.start.x_coord, &mut self.end.x_coord); 91 | } 92 | 93 | IterSelectedRooms { 94 | start_x: self.start.x_coord, 95 | current: self.start, 96 | end: self.end, 97 | } 98 | } 99 | } 100 | 101 | pub struct IterSelectedRooms { 102 | start_x: i32, 103 | current: RoomName, 104 | end: RoomName, 105 | } 106 | 107 | impl Iterator for IterSelectedRooms { 108 | type Item = RoomName; 109 | 110 | fn next(&mut self) -> Option { 111 | match ( 112 | self.current.x_coord == self.end.x_coord, 113 | self.current.y_coord == self.end.y_coord, 114 | ) { 115 | (false, _) => { 116 | let item = self.current; 117 | self.current.x_coord += 1; 118 | Some(item) 119 | } 120 | (true, false) => { 121 | let item = self.current; 122 | self.current.y_coord += 1; 123 | self.current.x_coord = self.start_x; 124 | Some(item) 125 | } 126 | (true, true) => None, 127 | } 128 | } 129 | } 130 | 131 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 132 | pub enum Request { 133 | Login, 134 | MyInfo, 135 | ShardList, 136 | ChangeSettings { settings: Arc }, 137 | Exit, 138 | RoomTerrain { room_name: RoomName }, 139 | SetMapSubscribes { rooms: SelectedRooms }, 140 | SetFocusRoom { room: Option }, 141 | } 142 | 143 | impl Request { 144 | pub fn login() -> Self { 145 | Login 146 | } 147 | 148 | pub fn my_info() -> Self { 149 | MyInfo 150 | } 151 | 152 | pub fn shard_list() -> Self { 153 | ShardList 154 | } 155 | 156 | pub fn room_terrain(room_name: RoomName) -> Self { 157 | RoomTerrain { 158 | room_name: room_name, 159 | } 160 | } 161 | 162 | pub fn subscribe_map_view(rooms: SelectedRooms) -> Self { 163 | SetMapSubscribes { rooms: rooms } 164 | } 165 | 166 | pub fn focus_room(room_name: Option) -> Self { 167 | SetFocusRoom { room: room_name } 168 | } 169 | 170 | pub fn change_settings(settings: ConnectionSettings) -> Self { 171 | ChangeSettings { 172 | settings: Arc::new(settings), 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /network/src/tokio/http.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use std::rc::Rc; 3 | use std::cell::{Ref, RefCell}; 4 | use std::ops::Deref; 5 | use std::sync::Arc; 6 | 7 | use std::sync::mpsc::Sender as StdSender; 8 | use futures::sync::mpsc::Sender as BoundedFuturesSender; 9 | 10 | use futures::{future, Future, Sink}; 11 | use tokio_core::reactor::{Handle, Timeout}; 12 | use hyper::StatusCode; 13 | 14 | use screeps_api::{self, TokenStorage}; 15 | 16 | use hyper; 17 | 18 | use event::NetworkEvent; 19 | 20 | use diskcache; 21 | use {ConnectionSettings, Notify}; 22 | 23 | use super::types::HttpRequest; 24 | use super::utils; 25 | 26 | pub struct Executor { 27 | pub handle: Handle, 28 | pub send_results: StdSender, 29 | pub notify: N, 30 | pub executor_return: BoundedFuturesSender>, 31 | pub settings: Rc>>, 32 | pub client: screeps_api::Api, 33 | pub disk_cache: diskcache::Cache, 34 | } 35 | 36 | impl<'a, N, C, H, T> utils::HasClient<'a, C, H, T> for Executor 37 | where 38 | C: hyper::client::Connect, 39 | H: screeps_api::HyperClient, 40 | T: TokenStorage, 41 | { 42 | type SettingsDeref = Ref<'a, ConnectionSettings>; 43 | 44 | fn settings(&'a self) -> Self::SettingsDeref { 45 | Ref::map(self.settings.borrow(), Deref::deref) 46 | } 47 | 48 | fn api(&'a self) -> &'a screeps_api::Api { 49 | &self.client 50 | } 51 | } 52 | 53 | enum HttpExecError { 54 | Continue(Executor), 55 | Exit, 56 | } 57 | 58 | impl Executor 59 | where 60 | C: hyper::client::Connect, 61 | H: screeps_api::HyperClient + 'static + Clone, 62 | T: TokenStorage, 63 | N: Notify, 64 | { 65 | fn exec_network( 66 | self, 67 | request: HttpRequest, 68 | ) -> Box> + 'static> { 69 | match request { 70 | HttpRequest::Login => { 71 | let username; 72 | Box::new( 73 | { 74 | let settings = self.settings.borrow(); 75 | username = settings.username.to_owned(); 76 | self.client.login(&*settings.username, &*settings.password) 77 | }.then(move |result| { 78 | let event = NetworkEvent::Login { 79 | username: username, 80 | result: result.map(|logged_in| logged_in.return_to(&self.client.tokens)), 81 | }; 82 | 83 | future::ok((self, HttpRequest::Login, event)) 84 | }), 85 | ) 86 | } 87 | HttpRequest::MyInfo => { 88 | let execute = |executor: Self| match executor.client.my_info() { 89 | Ok(future) => Ok(future.then(move |result| { 90 | future::ok(( 91 | executor, 92 | HttpRequest::MyInfo, 93 | NetworkEvent::MyInfo { result: result }, 94 | )) 95 | })), 96 | Err(e) => Err((executor, e)), 97 | }; 98 | 99 | let handle_err = |executor: Self, login_error| { 100 | future::ok(( 101 | executor, 102 | HttpRequest::MyInfo, 103 | NetworkEvent::MyInfo { 104 | result: Err(login_error), 105 | }, 106 | )) 107 | }; 108 | 109 | utils::execute_or_login_and_execute(self, execute, handle_err) 110 | } 111 | HttpRequest::ShardList => Box::new(self.client.shard_list().then(move |result| { 112 | future::ok(( 113 | self, 114 | HttpRequest::ShardList, 115 | NetworkEvent::ShardList { 116 | result: match result { 117 | Ok(v) => Ok(Some(v)), 118 | Err(e) => match *e.kind() { 119 | screeps_api::ErrorKind::StatusCode(hyper::StatusCode::NotFound) => Ok(None), 120 | _ => Err(e), 121 | }, 122 | }, 123 | }, 124 | )) 125 | })), 126 | HttpRequest::RoomTerrain { room_name } => { 127 | let cache_req = self.disk_cache.get_terrain( 128 | self.client.url.as_ref(), 129 | self.settings.borrow().shard.as_ref().map(|s| &**s), 130 | room_name, 131 | ); 132 | Box::new(cache_req.then(move |result| { 133 | match result { 134 | Ok(Some(terrain)) => { 135 | Box::new(future::ok((self, Ok(terrain)))) as Box> 136 | } 137 | other => { 138 | if let Err(e) = other { 139 | // TODO: switch to Display when supported (in statement below as well) 140 | warn!("error occurred fetching terrain cache: {:?}", e); 141 | } 142 | let request = self.client.room_terrain( 143 | self.settings.borrow().shard.as_ref().map(|s| &**s), 144 | room_name.to_string(), 145 | ); 146 | Box::new(request.map(|data| data.terrain).then(move |result| { 147 | if let Ok(ref data) = result { 148 | self.handle.spawn( 149 | self.disk_cache 150 | .set_terrain( 151 | self.client.url.as_ref(), 152 | self.settings.borrow().shard.as_ref().map(|s| &**s), 153 | room_name, 154 | data, 155 | ) 156 | .then(|result| { 157 | if let Err(e) = result { 158 | warn!("error occurred storing to terrain cache: {:?}", e); 159 | } 160 | Ok(()) 161 | }), 162 | ); 163 | } 164 | future::ok((self, result)) 165 | })) as Box> 166 | } 167 | }.and_then(move |(executor, result)| { 168 | future::ok(( 169 | executor, 170 | HttpRequest::RoomTerrain { 171 | room_name: room_name, 172 | }, 173 | NetworkEvent::RoomTerrain { 174 | room_name: room_name, 175 | result: result, 176 | }, 177 | )) 178 | }) 179 | })) 180 | } 181 | HttpRequest::ChangeSettings { settings } => { 182 | { 183 | // TODO: this is full of possible race conditions if we have other 184 | // requests executing concurrently with this settings change... While 185 | // we do reset current login tokens, that probably isn't enough! 186 | // 187 | // But.. we don't actually support that right now in the UI, so this 188 | // is a low priority thing. The only time we can change settings is 189 | // if the only request made so far is a login. 190 | let mut current = self.settings.borrow_mut(); 191 | match ( 192 | settings.api_url == current.api_url, 193 | settings.username == current.username, 194 | settings.password == current.password, 195 | settings.shard == current.shard, 196 | ) { 197 | // Nothing's changed 198 | (true, true, true, true) => (), 199 | // Only the shard and/or password have changed 200 | (true, true, false, _) | (true, true, _, false) => *current = settings.clone(), 201 | // Username or server has changed 202 | (true, false, ..) | (false, ..) => { 203 | *current = settings.clone(); 204 | while let Some(_) = self.client.tokens.take_token() {} 205 | } 206 | } 207 | } 208 | Box::new(future::err(HttpExecError::Continue(self))) 209 | } 210 | HttpRequest::Exit => Box::new(future::err(HttpExecError::Exit)), 211 | } 212 | } 213 | 214 | pub fn execute(self, request: HttpRequest) -> impl Future + 'static { 215 | self.exec_network(request).then( 216 | move |result| -> Box + 'static> { 217 | let exec = match result { 218 | Ok((exec, request, event)) => { 219 | if let Some(err) = event.error() { 220 | if let screeps_api::ErrorKind::StatusCode(ref status) = *err.kind() { 221 | if *status == StatusCode::TooManyRequests { 222 | debug!("starting 5-second timeout from TooManyRequests error."); 223 | 224 | let timeout = Timeout::new(Duration::from_secs(5), &exec.handle).expect( 225 | "expected Timeout::new() to only fail if tokio \ 226 | core has been stopped", 227 | ); 228 | 229 | return Box::new(timeout.then(|_| { 230 | debug!("5-second timeout finished."); 231 | 232 | exec.execute(request) 233 | })); 234 | } 235 | } 236 | } 237 | 238 | match exec.send_results.send(event) { 239 | Ok(_) => { 240 | trace!("successfully finished a request."); 241 | let result = exec.notify.wakeup(); 242 | if let Err(_) = result { 243 | warn!("failed to wake up main event loop after sending result successfully.") 244 | } 245 | } 246 | Err(_) => { 247 | warn!("failed to send the result of a request."); 248 | } 249 | } 250 | exec 251 | } 252 | Err(HttpExecError::Continue(exec)) => exec, 253 | Err(HttpExecError::Exit) => return Box::new(future::err(())), 254 | }; 255 | 256 | Box::new(exec.executor_return.clone().send(exec).then(|result| { 257 | if let Err(_) = result { 258 | warn!("couldn't return connection token after finishing a request.") 259 | }; 260 | future::ok(()) 261 | })) 262 | }, 263 | ) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /network/src/tokio/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, thread}; 2 | use std::cell::RefCell; 3 | use std::sync::Arc; 4 | use std::rc::Rc; 5 | 6 | use std::sync::mpsc as std_mpsc; 7 | use std::sync::mpsc::Sender as StdSender; 8 | use std::sync::mpsc::Receiver as StdReceiver; 9 | 10 | use futures::sync::mpsc as futures_mpsc; 11 | use futures::sync::mpsc::UnboundedSender as FuturesSender; 12 | use futures::sync::mpsc::UnboundedReceiver as FuturesReceiver; 13 | 14 | use futures::{future, Future, Stream}; 15 | use tokio_core::reactor::{Core, Remote}; 16 | 17 | use screeps_api::{self, ArcTokenStorage}; 18 | 19 | use {hyper, hyper_tls, tokio_core}; 20 | 21 | use event::NetworkEvent; 22 | use request::Request; 23 | use {ConnectionSettings, Notify, ScreepsConnection}; 24 | use diskcache; 25 | 26 | mod types; 27 | mod http; 28 | mod ws; 29 | mod utils; 30 | 31 | use self::types::{GenericRequest, HttpRequest, WebsocketRequest}; 32 | 33 | pub struct Handler { 34 | /// Receiver and sender interacting with the current threaded handler. 35 | /// 36 | /// Use std sync channel for (tokio -> main thread), and a futures channel for (main thread -> tokio): 37 | /// - neither have any specific requirements for where the sender is called, but both require that the 38 | /// polling receiver be in the 'right context'. This way, it just works. 39 | handles: Option, 40 | /// Tokens saved. 41 | tokens: ArcTokenStorage, 42 | /// Settings 43 | settings: Arc, 44 | /// Disk cache database Handle 45 | disk_cache: diskcache::Cache, 46 | /// Window proxy in case we need to restart handler thread. 47 | notify: N, 48 | } 49 | 50 | #[derive(Debug)] 51 | struct HandlerHandles { 52 | remote: Remote, 53 | http_send: FuturesSender, 54 | ws_send: FuturesSender, 55 | recv: StdReceiver, 56 | } 57 | 58 | impl HandlerHandles { 59 | fn new( 60 | remote: Remote, 61 | http_send: FuturesSender, 62 | ws_send: FuturesSender, 63 | recv: StdReceiver, 64 | ) -> Self { 65 | HandlerHandles { 66 | remote: remote, 67 | http_send: http_send, 68 | ws_send: ws_send, 69 | recv: recv, 70 | } 71 | } 72 | 73 | fn send(&mut self, request: Request) -> Result<(), Request> { 74 | match request.into() { 75 | GenericRequest::Http(r) => self.http_send 76 | .unbounded_send(r) 77 | .map_err(|e| e.into_inner().into()), 78 | GenericRequest::Websocket(r) => self.ws_send 79 | .unbounded_send(r) 80 | .map_err(|e| e.into_inner().into()), 81 | GenericRequest::Both(hr, wr) => self.http_send 82 | .unbounded_send(hr) 83 | .map_err(|e| e.into_inner().into()) 84 | .and_then(|()| { 85 | self.ws_send 86 | .unbounded_send(wr) 87 | .map_err(|e| e.into_inner().into()) 88 | }), 89 | } 90 | } 91 | } 92 | 93 | impl Handler { 94 | /// Creates a new handler, with the given settings and notify callback. 95 | pub fn new(settings: ConnectionSettings, notify: N) -> Self { 96 | Handler { 97 | settings: Arc::new(settings), 98 | handles: None, 99 | tokens: ArcTokenStorage::default(), 100 | // TODO: handle this gracefully 101 | disk_cache: diskcache::Cache::load().expect("loading the disk cache failed."), 102 | notify: notify, 103 | } 104 | } 105 | } 106 | 107 | impl Handler { 108 | fn start_handler(&mut self) { 109 | let mut queued: Option> = None; 110 | if let Some(handles) = self.handles.take() { 111 | let mut queued_vec = Vec::new(); 112 | while let Ok(v) = handles.recv.try_recv() { 113 | queued_vec.push(v); 114 | } 115 | queued = Some(queued_vec); 116 | } 117 | 118 | let (http_send_to_handler, handler_http_recv) = futures_mpsc::unbounded(); 119 | let (ws_send_to_handler, handler_ws_recv) = futures_mpsc::unbounded(); 120 | let (handler_send, recv_from_handler) = std_mpsc::channel(); 121 | 122 | if let Some(values) = queued { 123 | for v in values { 124 | // fake these coming from the new handler. 125 | handler_send 126 | .send(v) 127 | .expect("expected handles to still be in current scope"); 128 | } 129 | } 130 | 131 | let handler = ThreadedHandler::new( 132 | handler_http_recv, 133 | handler_ws_recv, 134 | handler_send, 135 | self.notify.clone(), 136 | self.tokens.clone(), 137 | self.settings.clone(), 138 | self.disk_cache.clone(), 139 | ); 140 | 141 | let remote = handler.start_async_and_get_remote(); 142 | 143 | self.handles = Some(HandlerHandles::new( 144 | remote, 145 | http_send_to_handler, 146 | ws_send_to_handler, 147 | recv_from_handler, 148 | )); 149 | } 150 | } 151 | 152 | impl ScreepsConnection for Handler { 153 | fn send(&mut self, request: Request) { 154 | // TODO: find out how to get panic info from the threaded thread, and report that we had to reconnect! 155 | let request_retry = match self.handles { 156 | Some(ref mut handles) => match handles.send(request) { 157 | Ok(()) => None, 158 | Err(request) => Some(request), 159 | }, 160 | None => Some(request), 161 | }; 162 | 163 | if let Some(request) = request_retry { 164 | self.start_handler(); 165 | let send = self.handles 166 | .as_mut() 167 | .expect("expected handles to exist after freshly restarting"); 168 | send.send(request) 169 | .expect("expected freshly started handler to still be running"); 170 | } 171 | } 172 | 173 | fn poll(&mut self) -> Option { 174 | let (evt, reset) = match self.handles { 175 | Some(ref mut handles) => match handles.recv.try_recv() { 176 | Ok(v) => (Some(v), false), 177 | Err(std_mpsc::TryRecvError::Empty) => (None, false), 178 | Err(std_mpsc::TryRecvError::Disconnected) => (None, true), 179 | }, 180 | None => (None, false), 181 | }; 182 | if reset { 183 | self.handles = None; 184 | } 185 | evt 186 | } 187 | } 188 | 189 | impl fmt::Debug for Handler { 190 | fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { 191 | fmt.debug_struct("Handler") 192 | .field("handles", &self.handles) 193 | .field("settings", &self.settings) 194 | .field("tokens", &self.tokens) 195 | .field("notify", &"") 196 | .finish() 197 | } 198 | } 199 | 200 | struct ThreadedHandler { 201 | http_recv: FuturesReceiver, 202 | ws_recv: FuturesReceiver, 203 | send: StdSender, 204 | notify: N, 205 | settings: Arc, 206 | tokens: ArcTokenStorage, 207 | disk_cache: diskcache::Cache, 208 | } 209 | impl ThreadedHandler { 210 | fn new( 211 | http_recv: FuturesReceiver, 212 | ws_recv: FuturesReceiver, 213 | send: StdSender, 214 | notify: N, 215 | tokens: ArcTokenStorage, 216 | settings: Arc, 217 | disk_cache: diskcache::Cache, 218 | ) -> Self { 219 | ThreadedHandler { 220 | http_recv: http_recv, 221 | ws_recv: ws_recv, 222 | send: send, 223 | notify: notify, 224 | settings: settings, 225 | tokens: tokens, 226 | disk_cache: disk_cache, 227 | } 228 | } 229 | 230 | fn start_async_and_get_remote(self) -> tokio_core::reactor::Remote { 231 | let (temp_sender, temp_receiver) = std_mpsc::channel(); 232 | thread::spawn(|| self.run(temp_sender)); 233 | temp_receiver 234 | .recv() 235 | .expect("expected newly created channel to not be dropped, perhaps tokio core panicked?") 236 | } 237 | 238 | fn run(self, send_remote_to: StdSender) { 239 | use futures::Sink; 240 | 241 | let ThreadedHandler { 242 | mut http_recv, 243 | ws_recv, 244 | send, 245 | notify, 246 | settings, 247 | tokens, 248 | disk_cache, 249 | } = self; 250 | 251 | let settings_rc = Rc::new(RefCell::new(settings.clone())); 252 | 253 | let mut core = Core::new().expect("expected tokio core to succeed startup."); 254 | 255 | { 256 | // move into scope to drop. 257 | let sender = send_remote_to; 258 | sender 259 | .send(core.remote()) 260 | .expect("expected sending remote to spawning thread to succeed."); 261 | } 262 | 263 | let handle = core.handle(); 264 | 265 | disk_cache 266 | .start_cache_clean_task(&handle) 267 | .expect("expected starting database cleanup interval to succeed"); 268 | 269 | let hyper = hyper::Client::configure() 270 | .connector( 271 | hyper_tls::HttpsConnector::new(4, &handle) 272 | .expect("expected HTTPS handler construction with default parameters to succeed."), 273 | ) 274 | .build(&handle); 275 | 276 | let mut client = screeps_api::Api::with_url_and_tokens(hyper, settings_rc.borrow().api_url.clone(), tokens) 277 | .expect("expected already parsed URL to parse as URL"); 278 | 279 | struct StopAndClearPool(HttpRequest); 280 | 281 | let ws_executor = ws::Executor::new( 282 | handle.clone(), 283 | send.clone(), 284 | client.clone(), 285 | settings, 286 | notify.clone(), 287 | ); 288 | 289 | // WS executor can just run in the background. Since there's only one 290 | // "executor", we don't need to restart it in the loop. 291 | handle.spawn(ws_executor.run(ws_recv)); 292 | 293 | // Loop so that we can "flush" the pool of pending executions whenever 294 | // we're changing settings. 295 | loop { 296 | let (mut exec_pool_send, mut exec_pool_recv) = futures_mpsc::channel(5); 297 | 298 | // fill with 5 tokens. 299 | for _ in 0..5 { 300 | let cloned_send = exec_pool_send.clone(); 301 | assert!( 302 | exec_pool_send 303 | .start_send(http::Executor { 304 | handle: handle.clone(), 305 | send_results: send.clone(), 306 | notify: notify.clone(), 307 | executor_return: cloned_send, 308 | settings: settings_rc.clone(), 309 | client: client.clone(), 310 | disk_cache: disk_cache.clone(), 311 | }) 312 | .expect("expected newly created channel to still be in scope") 313 | .is_ready() 314 | ); 315 | } 316 | 317 | // zip combines executors with requests so we'll 318 | // never be running more than 5 concurrent requests. 319 | let result = core.run( 320 | http_recv 321 | .by_ref() 322 | .zip(exec_pool_recv.by_ref()) 323 | .map_err(|()| panic!("expected futures::mpsc::sync::Receiver stream to never return an error.")) 324 | .for_each(|(request, executor)| { 325 | if let HttpRequest::ChangeSettings { .. } = request { 326 | exec_pool_send 327 | .clone() 328 | .start_send(executor) 329 | .expect("expected channel to still be in scope"); 330 | future::err(StopAndClearPool(request)) 331 | } else { 332 | // execute request returns the executor to the token pool at the end. 333 | handle.spawn(executor.execute(request)); 334 | future::ok(()) 335 | } 336 | }), 337 | ); 338 | let last_request = match result { 339 | Ok(()) => break, 340 | Err(StopAndClearPool(request_to_do)) => request_to_do, 341 | }; 342 | // HTTP executor receiving ChangeSettings will have already changed 343 | // the client's settings. 344 | // 345 | // Let's first just wait on all executors finishing their last requests, then process the 346 | // request we were waiting for, then restart the loop. 347 | core.run(exec_pool_recv.by_ref().take(4).for_each(|exec| { 348 | drop(exec); 349 | future::ok(()) 350 | })).expect("expected futures::mpsc::sync::Receiver stream to never return an error."); 351 | core.run( 352 | exec_pool_recv 353 | .by_ref() 354 | .into_future() 355 | .map_err(|((), _)| panic!("expected futures::mpsc::sync::Receiver to never return an error.")) 356 | .and_then(|(executor, _)| { 357 | executor 358 | .expect("expected pool to contain 5 executors") 359 | .execute(last_request) 360 | }), 361 | ).expect("expected Executor::execute to never return an errror"); 362 | 363 | // and now, in case the client URL has changed, update it. This is necessary 364 | // since each cloned executor has a cloned URL, and cannot update the original. 365 | let settings = settings_rc.borrow(); 366 | if settings.api_url != client.url { 367 | client.url = settings.api_url.clone(); 368 | } 369 | } 370 | 371 | info!("single threaded event loop exiting."); 372 | // let the client know that we have closed, ignoring errors. 373 | let _ = notify.wakeup(); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /network/src/tokio/types.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use screeps_api; 4 | 5 | use request::{Request, SelectedRooms}; 6 | use ConnectionSettings; 7 | 8 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 9 | pub enum HttpRequest { 10 | Login, 11 | MyInfo, 12 | ShardList, 13 | RoomTerrain { room_name: screeps_api::RoomName }, 14 | ChangeSettings { settings: Arc }, 15 | Exit, 16 | } 17 | 18 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 19 | pub enum WebsocketRequest { 20 | SetMapSubscribes { rooms: SelectedRooms }, 21 | SetFocusRoom { room: Option }, 22 | ChangeSettings { settings: Arc }, 23 | Exit, 24 | } 25 | 26 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 27 | pub enum GenericRequest { 28 | Http(HttpRequest), 29 | Websocket(WebsocketRequest), 30 | Both(HttpRequest, WebsocketRequest), 31 | } 32 | 33 | impl From for GenericRequest { 34 | fn from(r: Request) -> Self { 35 | match r { 36 | Request::Login => GenericRequest::Http(HttpRequest::Login), 37 | Request::MyInfo => GenericRequest::Http(HttpRequest::MyInfo), 38 | Request::ShardList => GenericRequest::Http(HttpRequest::ShardList), 39 | Request::RoomTerrain { room_name } => GenericRequest::Http(HttpRequest::RoomTerrain { 40 | room_name: room_name, 41 | }), 42 | Request::SetMapSubscribes { rooms } => { 43 | GenericRequest::Websocket(WebsocketRequest::SetMapSubscribes { rooms: rooms }) 44 | } 45 | Request::SetFocusRoom { room } => GenericRequest::Websocket(WebsocketRequest::SetFocusRoom { room: room }), 46 | Request::ChangeSettings { settings } => GenericRequest::Both( 47 | HttpRequest::ChangeSettings { 48 | settings: settings.clone(), 49 | }, 50 | WebsocketRequest::ChangeSettings { settings: settings }, 51 | ), 52 | Request::Exit => GenericRequest::Both(HttpRequest::Exit, WebsocketRequest::Exit), 53 | } 54 | } 55 | } 56 | 57 | impl Into for HttpRequest { 58 | fn into(self) -> Request { 59 | match self { 60 | HttpRequest::Login => Request::Login, 61 | HttpRequest::MyInfo => Request::MyInfo, 62 | HttpRequest::ShardList => Request::ShardList, 63 | HttpRequest::RoomTerrain { room_name } => Request::RoomTerrain { 64 | room_name: room_name, 65 | }, 66 | HttpRequest::ChangeSettings { settings } => Request::ChangeSettings { settings: settings }, 67 | HttpRequest::Exit => Request::Exit, 68 | } 69 | } 70 | } 71 | 72 | impl Into for WebsocketRequest { 73 | fn into(self) -> Request { 74 | match self { 75 | WebsocketRequest::SetMapSubscribes { rooms } => Request::SetMapSubscribes { rooms: rooms }, 76 | WebsocketRequest::SetFocusRoom { room } => Request::SetFocusRoom { room: room }, 77 | WebsocketRequest::ChangeSettings { settings } => Request::ChangeSettings { settings: settings }, 78 | WebsocketRequest::Exit => Request::Exit, 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /network/src/tokio/utils.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | use futures::Future; 3 | 4 | use screeps_api::{self, NoToken, TokenStorage}; 5 | 6 | use hyper; 7 | 8 | use ConnectionSettings; 9 | 10 | pub trait HasClient<'a, C, H, T> 11 | where 12 | C: hyper::client::Connect, 13 | H: screeps_api::HyperClient, 14 | T: TokenStorage, 15 | { 16 | type SettingsDeref: Deref + 'a; 17 | 18 | fn settings(&'a self) -> Self::SettingsDeref; 19 | fn api(&'a self) -> &'a screeps_api::Api; 20 | } 21 | 22 | pub fn execute_or_login_and_execute< 23 | Executor, 24 | Tokens, 25 | HyperConnect, 26 | HyperClient, 27 | ReturnData, 28 | ReturnError, 29 | Function, 30 | FunctionReturn, 31 | FailureFunction, 32 | FailureReturn, 33 | >( 34 | executor: Executor, 35 | mut func: Function, 36 | mut failure_func: FailureFunction, 37 | ) -> Box> 38 | where 39 | HyperConnect: hyper::client::Connect + 'static, 40 | HyperClient: screeps_api::HyperClient + 'static, 41 | Tokens: TokenStorage + 'static, 42 | Executor: for<'a> HasClient<'a, HyperConnect, HyperClient, Tokens> + 'static, 43 | ReturnData: 'static, 44 | ReturnError: 'static, 45 | Function: FnMut(Executor) -> Result + 'static, 46 | FunctionReturn: Future + 'static, 47 | FailureFunction: FnMut(Executor, screeps_api::Error) -> FailureReturn + 'static, 48 | FailureReturn: Future + 'static, 49 | { 50 | match func(executor) { 51 | Ok(future) => Box::new(future) as Box>, 52 | Err((executor, NoToken)) => { 53 | let login_future = { 54 | let settings = executor.settings(); 55 | executor 56 | .api() 57 | .login(&*settings.username, &*settings.password) 58 | }; 59 | Box::new(login_future.then(move |login_result| { 60 | match login_result { 61 | Ok(login_ok) => { 62 | login_ok.return_to(&executor.api().tokens); 63 | debug!("execute_or_login_and_execute login finished, attempting to execute again."); 64 | // TODO: something here to ensure that this doesn't end up as an infinite loop 65 | Box::new(execute_or_login_and_execute(executor, func, failure_func)) 66 | as Box> 67 | } 68 | Err(e) => Box::new(failure_func(executor, e)) as Box>, 69 | } 70 | })) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | comment_width = 120 3 | report_fixme = "unnumbered" 4 | use_try_shorthand = true 5 | write_mode = "overwrite" 6 | condense_wildcard_suffixes = true 7 | format_strings = false 8 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # taken from 4 | # https://github.com/sfackler/rust-openssl/blob/a70e27ae646834619c7abf4a3aef95bfd87666b3/openssl/test/build.sh 5 | 6 | set -ex 7 | 8 | MAX_REDIRECTS=5 9 | 10 | if [ -n "${BUILD_OPENSSL_VERSION}" ]; then 11 | NAME=openssl 12 | URL1="https://openssl.org/source/openssl-${BUILD_OPENSSL_VERSION}.tar.gz" 13 | URL2="http://mirrors.ibiblio.org/openssl/source/openssl-${BUILD_OPENSSL_VERSION}.tar.gz" 14 | OUT="/tmp/openssl-${BUILD_OPENSSL_VERSION}.tar.gz" 15 | else 16 | exit 0 17 | fi 18 | 19 | me=$0 20 | myname=`basename ${me}` 21 | 22 | cmp --silent ${me} ${HOME}/${NAME}/${myname} && exit 0 || echo "cache is busted" 23 | 24 | rm -rf "${HOME}/${NAME}" 25 | 26 | if [ "${TRAVIS_OS_NAME}" == "osx" ]; then 27 | exit 0 28 | fi 29 | 30 | if [ "$TARGET" == "i686-unknown-linux-gnu" ]; then 31 | OS_COMPILER=linux-elf 32 | OS_FLAGS=-m32 33 | elif [ "$TARGET" == "arm-unknown-linux-gnueabihf" ]; then 34 | OS_COMPILER=linux-armv4 35 | export AR=arm-linux-gnueabihf-ar 36 | export CC=arm-linux-gnueabihf-gcc 37 | else 38 | OS_COMPILER=linux-x86_64 39 | fi 40 | 41 | mkdir -p /tmp/build 42 | cp ${me} /tmp/build/${myname} 43 | cd /tmp/build 44 | 45 | curl -o ${OUT} -L --max-redirs ${MAX_REDIRECTS} ${URL1} \ 46 | || curl -o ${OUT} -L --max-redirs ${MAX_REDIRECTS} ${URL2} 47 | 48 | tar --strip-components=1 -xzf ${OUT} 49 | 50 | ./Configure --prefix=${HOME}/openssl ${OS_COMPILER} -fPIC ${OS_FLAGS} 51 | 52 | make -j$(nproc) 53 | make install 54 | cp ${myname} ${HOME}/${NAME}/${myname} 55 | -------------------------------------------------------------------------------- /scripts/predeploy.ps1: -------------------------------------------------------------------------------- 1 | $SRC_DIR = $PWD.Path 2 | $STAGE = [System.Guid]::NewGuid().ToString() 3 | 4 | Set-Location $ENV:Temp 5 | New-Item -Type Directory -Name $STAGE 6 | Set-Location $STAGE 7 | 8 | $ZIP = "$SRC_DIR\$($Env:APPVEYOR_REPO_TAG_NAME)-$($Env:TARGET_DESC).zip" 9 | 10 | Copy-Item "$SRC_DIR\target\$($Env:TARGET)\release\screeps-rs-client.exe" '.\' 11 | 12 | 7z a "$ZIP" * 13 | 14 | Push-AppveyorArtifact "$ZIP" 15 | 16 | Remove-Item *.* -Force 17 | Set-Location .. 18 | Remove-Item $STAGE 19 | Set-Location $SRC_DIR 20 | -------------------------------------------------------------------------------- /ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screeps-rs-ui" 3 | version = "0.1.5" 4 | authors = ["David Ross "] 5 | description = "User interface for a work in progress native Screeps client." 6 | 7 | repository = "https://github.com/daboross/screeps-rs" 8 | 9 | readme = "../README.md" 10 | 11 | keywords = [] 12 | categories = ["games"] 13 | license = "MIT" 14 | 15 | [lib] 16 | path = "src/rust/lib.rs" 17 | 18 | [dependencies] 19 | # Graphics 20 | glium = "0.20" 21 | glutin = "0.12" 22 | rusttype = "0.4" 23 | time = "0.1" 24 | conrod = { version = "0.58", features = ["winit", "glium"] } 25 | conrod_derive = "0.1.0" 26 | # Types: 27 | screeps-api = { version = "0.4", default-features = false } 28 | # Networking 29 | screeps-rs-network = { path = "../network" } 30 | # Logging 31 | chrono = "0.4" 32 | log = "0.4" 33 | fern = "0.5" 34 | # Command-line 35 | clap = "2.22" 36 | 37 | [[bin]] 38 | name = "screeps-rs-client" 39 | doc = false 40 | -------------------------------------------------------------------------------- /ui/src/bin/screeps-rs-client.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | extern crate screeps_rs_ui; 4 | 5 | use clap::{App, Arg}; 6 | 7 | fn main() { 8 | let matches = App::new("screeps-rs") 9 | .version(crate_version!()) 10 | .author(&*crate_authors!().replace(":", "\n")) 11 | .about("Native client for the Screeps JavaScript MMO") 12 | .arg( 13 | Arg::with_name("verbose") 14 | .short("v") 15 | .long("verbose") 16 | .multiple(true) 17 | .help("enables verbose logging"), 18 | ) 19 | .arg( 20 | clap::Arg::with_name("debug-modules") 21 | .short("d") 22 | .long("debug") 23 | .value_name("MODULE_PATH") 24 | .help("Enable verbose logging for a specific module") 25 | .long_help( 26 | "Enables verbose debug logging for an individual rust module or path.\n\ 27 | For example, `--debug screeps_rs_ui::ui` will enable verbose logging for UI related code.\n\ 28 | \n\ 29 | Common modules you can use:\n\ 30 | - screeps_rs_network app network calling and result caching\n\ 31 | - screeps_rs_ui app glue and UI\n\ 32 | - screeps_rs_ui::window_management event glue\n\ 33 | - screeps_rs_ui::rendering game state rendering\n\ 34 | - screeps_api network result parsing\n\ 35 | - screeps_api::sockets websocket network result parsing\n\ 36 | - hyper HTTP client\n\ 37 | - ws websocket client", 38 | ) 39 | .takes_value(true) 40 | .multiple(true), 41 | ) 42 | .get_matches(); 43 | 44 | screeps_rs_ui::main( 45 | matches.is_present("verbose"), 46 | matches 47 | .values_of("debug-modules") 48 | .into_iter() 49 | .flat_map(|iter| iter), 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /ui/src/rust/app.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | use std::sync::Arc; 3 | 4 | use screeps_rs_network::{self, MemCache}; 5 | 6 | use {conrod, glium, glium_backend, glutin, layout, rendering}; 7 | 8 | use network_integration::{GlutinNotify, NetworkCache, NetworkHandler}; 9 | 10 | pub struct App { 11 | pub ui: conrod::Ui, 12 | pub display: glium::Display, 13 | pub image_cache: rendering::RenderCache, 14 | pub ids: layout::Ids, 15 | pub renderer: glium_backend::Renderer, 16 | pub net_cache: MemCache, 17 | pub network_handler: NetworkHandler, 18 | pub notify: GlutinNotify, 19 | /// Phantom data in order to allow adding any additional fields in the future. 20 | #[doc(hidden)] 21 | pub _phantom: PhantomData<()>, 22 | } 23 | 24 | pub struct AppCell<'a, 'b: 'a, 'c> { 25 | pub ui: &'a mut conrod::UiCell<'b>, 26 | pub display: &'a glium::Display, 27 | pub image_cache: &'a mut rendering::RenderCache, 28 | pub ids: &'a mut layout::Ids, 29 | pub renderer: &'a mut glium_backend::Renderer, 30 | pub net_cache: NetworkCache<'a>, 31 | pub additional_rendering: &'c mut Option, 32 | pub notify: &'a GlutinNotify, 33 | /// Phantom data in order to allow adding any additional fields in the future. 34 | #[doc(hidden)] 35 | pub _phantom: PhantomData<()>, 36 | } 37 | 38 | impl App { 39 | pub fn new(window: glium::Display, events: &glutin::EventsLoop) -> Self { 40 | let (width, height) = window 41 | .gl_window() 42 | .window() 43 | .get_inner_size() 44 | .expect("expected getting window size to succeed."); 45 | 46 | // Create UI. 47 | let mut ui = conrod::UiBuilder::new([width as f64, height as f64]).build(); 48 | let renderer = 49 | glium_backend::Renderer::new(&window).expect("expected loading conrod glium renderer to succeed."); 50 | let image_cache = rendering::RenderCache::new(); 51 | let ids = layout::Ids::new(&mut ui.widget_id_generator()); 52 | 53 | let notify = GlutinNotify::from(Arc::new(events.create_proxy())); 54 | 55 | App { 56 | ui: ui, 57 | display: window, 58 | image_cache: image_cache, 59 | ids: ids, 60 | renderer: renderer, 61 | net_cache: MemCache::new(), 62 | network_handler: NetworkHandler::new( 63 | screeps_rs_network::ConnectionSettings::new(String::new(), String::new(), None), 64 | notify.clone(), 65 | ), 66 | notify: notify, 67 | _phantom: PhantomData, 68 | } 69 | } 70 | } 71 | 72 | impl<'a, 'b: 'a, 'c> AppCell<'a, 'b, 'c> { 73 | pub fn cell( 74 | cell: &'a mut conrod::UiCell<'b>, 75 | display: &'a glium::Display, 76 | image_cache: &'a mut rendering::RenderCache, 77 | ids: &'a mut layout::Ids, 78 | renderer: &'a mut glium_backend::Renderer, 79 | net_cache: &'a mut MemCache, 80 | network_handler: &'a mut NetworkHandler, 81 | additional_rendering: &'c mut Option, 82 | notify: &'a GlutinNotify, 83 | ) -> Self { 84 | let net_cache = net_cache.align( 85 | network_handler, 86 | |x| { 87 | // TODO: this shouldn't be done here, but rather within the UI event code. 88 | warn!("network error occurred: {}", x); 89 | }, 90 | image_cache.event_handler(), 91 | ); 92 | AppCell { 93 | ui: cell, 94 | display: display, 95 | image_cache: image_cache, 96 | ids: ids, 97 | renderer: renderer, 98 | net_cache: net_cache, 99 | additional_rendering: additional_rendering, 100 | notify: notify, 101 | _phantom: PhantomData, 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /ui/src/rust/glium_backend/mod.rs: -------------------------------------------------------------------------------- 1 | //! An inlined version of conrod::backend::glium for experimentation. 2 | #![cfg_attr(rustfmt, rustfmt_skip)] 3 | 4 | use glium; 5 | 6 | use std; 7 | use conrod::{color, image, render, text, Rect, Scalar}; 8 | 9 | /// A `Command` describing a step in the drawing process. 10 | #[derive(Clone, Debug)] 11 | pub enum Command { 12 | /// Draw to the target. 13 | Draw(Draw), 14 | /// Update the scizzor within the `glium::DrawParameters`. 15 | Scizzor(glium::Rect), 16 | } 17 | 18 | /// A `Command` for drawing to the target. 19 | /// 20 | /// Each variant describes how to draw the contents of the vertex buffer. 21 | #[derive(Clone, Debug)] 22 | pub enum Draw { 23 | /// A range of vertices representing triangles textured with the image in the 24 | /// image_map at the given `widget::Id`. 25 | Image(image::Id, std::ops::Range), 26 | /// A range of vertices representing plain triangles. 27 | Plain(std::ops::Range), 28 | } 29 | 30 | enum PreparedCommand { 31 | Image(image::Id, std::ops::Range), 32 | Plain(std::ops::Range), 33 | Scizzor(glium::Rect), 34 | } 35 | 36 | /// A rusttype `GlyphCache` along with a `glium::texture::Texture2d` for caching text on the `GPU`. 37 | pub struct GlyphCache { 38 | cache: text::GlyphCache<'static>, 39 | texture: glium::texture::Texture2d, 40 | } 41 | 42 | /// A type used for translating `render::Primitives` into `Command`s that indicate how to draw the 43 | /// conrod GUI using `glium`. 44 | pub struct Renderer { 45 | program: glium::Program, 46 | glyph_cache: GlyphCache, 47 | commands: Vec, 48 | vertices: Vec, 49 | } 50 | 51 | /// An iterator yielding `Command`s, produced by the `Renderer::commands` method. 52 | pub struct Commands<'a> { 53 | commands: std::slice::Iter<'a, PreparedCommand>, 54 | } 55 | 56 | /// Possible errors that may occur during a call to `Renderer::new`. 57 | #[derive(Debug)] 58 | pub enum RendererCreationError { 59 | /// Errors that might occur when creating the glyph cache texture. 60 | Texture(glium::texture::TextureCreationError), 61 | /// Errors that might occur when constructing the shader program. 62 | Program(glium::program::ProgramChooserCreationError), 63 | } 64 | 65 | /// Possible errors that may occur during a call to `Renderer::draw`. 66 | #[derive(Debug)] 67 | pub enum DrawError { 68 | /// Errors that might occur upon construction of a `glium::VertexBuffer`. 69 | Buffer(glium::vertex::BufferCreationError), 70 | /// Errors that might occur when drawing to the `glium::Surface`. 71 | Draw(glium::DrawError), 72 | } 73 | 74 | /// The `Vertex` type passed to the vertex shader. 75 | #[derive(Copy, Clone, Debug)] 76 | pub struct Vertex { 77 | /// The mode with which the `Vertex` will be drawn within the fragment shader. 78 | /// 79 | /// `0` for rendering text. 80 | /// `1` for rendering an image. 81 | /// `2` for rendering non-textured 2D geometry. 82 | /// 83 | /// If any other value is given, the fragment shader will not output any color. 84 | pub mode: u32, 85 | /// The position of the vertex within vector space. 86 | /// 87 | /// [-1.0, -1.0] is the leftmost, bottom position of the display. 88 | /// [1.0, 1.0] is the rightmost, top position of the display. 89 | pub position: [f32; 2], 90 | /// The coordinates of the texture used by this `Vertex`. 91 | /// 92 | /// [0.0, 0.0] is the leftmost, bottom position of the texture. 93 | /// [1.0, 1.0] is the rightmost, top position of the texture. 94 | pub tex_coords: [f32; 2], 95 | /// A color associated with the `Vertex`. 96 | /// 97 | /// The way that the color is used depends on the `mode`. 98 | pub color: [f32; 4], 99 | } 100 | 101 | #[allow(unsafe_code)] 102 | mod vertex_impl { 103 | use super::Vertex; 104 | implement_vertex!(Vertex, position, tex_coords, color, mode); 105 | } 106 | 107 | /// Draw text from the text cache texture `tex` in the fragment shader. 108 | pub const MODE_TEXT: u32 = 0; 109 | /// Draw an image from the texture at `tex` in the fragment shader. 110 | pub const MODE_IMAGE: u32 = 1; 111 | /// Ignore `tex` and draw simple, colored 2D geometry. 112 | pub const MODE_GEOMETRY: u32 = 2; 113 | 114 | 115 | /// The vertex shader used within the `glium::Program` for OpenGL. 116 | pub const VERTEX_SHADER_120: &'static str = " 117 | #version 120 118 | 119 | attribute vec2 position; 120 | attribute vec2 tex_coords; 121 | attribute vec4 color; 122 | attribute float mode; 123 | 124 | varying vec2 v_tex_coords; 125 | varying vec4 v_color; 126 | varying float v_mode; 127 | 128 | void main() { 129 | gl_Position = vec4(position, 0.0, 1.0); 130 | v_tex_coords = tex_coords; 131 | v_color = color; 132 | v_mode = mode; 133 | } 134 | "; 135 | 136 | /// The fragment shader used within the `glium::Program` for OpenGL. 137 | pub const FRAGMENT_SHADER_120: &'static str = " 138 | #version 120 139 | uniform sampler2D tex; 140 | 141 | varying vec2 v_tex_coords; 142 | varying vec4 v_color; 143 | varying float v_mode; 144 | 145 | void main() { 146 | // Text 147 | if (v_mode == 0.0) { 148 | gl_FragColor = v_color * vec4(1.0, 1.0, 1.0, texture2D(tex, v_tex_coords).r); 149 | 150 | // Image 151 | } else if (v_mode == 1.0) { 152 | gl_FragColor = texture2D(tex, v_tex_coords); 153 | 154 | // 2D Geometry 155 | } else if (v_mode == 2.0) { 156 | gl_FragColor = v_color; 157 | } 158 | } 159 | "; 160 | 161 | /// The vertex shader used within the `glium::Program` for OpenGL. 162 | pub const VERTEX_SHADER_140: &'static str = " 163 | #version 140 164 | 165 | in vec2 position; 166 | in vec2 tex_coords; 167 | in vec4 color; 168 | in uint mode; 169 | 170 | out vec2 v_tex_coords; 171 | out vec4 v_color; 172 | flat out uint v_mode; 173 | 174 | void main() { 175 | gl_Position = vec4(position, 0.0, 1.0); 176 | v_tex_coords = tex_coords; 177 | v_color = color; 178 | v_mode = mode; 179 | } 180 | "; 181 | 182 | /// The fragment shader used within the `glium::Program` for OpenGL. 183 | pub const FRAGMENT_SHADER_140: &'static str = " 184 | #version 140 185 | uniform sampler2D tex; 186 | 187 | in vec2 v_tex_coords; 188 | in vec4 v_color; 189 | flat in uint v_mode; 190 | 191 | out vec4 f_color; 192 | 193 | void main() { 194 | // Text 195 | if (v_mode == uint(0)) { 196 | f_color = v_color * vec4(1.0, 1.0, 1.0, texture(tex, v_tex_coords).r); 197 | 198 | // Image 199 | } else if (v_mode == uint(1)) { 200 | f_color = texture(tex, v_tex_coords); 201 | 202 | // 2D Geometry 203 | } else if (v_mode == uint(2)) { 204 | f_color = v_color; 205 | } 206 | } 207 | "; 208 | 209 | /// The vertex shader used within the `glium::Program` for OpenGL ES. 210 | pub const VERTEX_SHADER_300_ES: &'static str = " 211 | #version 300 es 212 | precision mediump float; 213 | 214 | in vec2 position; 215 | in vec2 tex_coords; 216 | in vec4 color; 217 | in uint mode; 218 | 219 | out vec2 v_tex_coords; 220 | out vec4 v_color; 221 | flat out uint v_mode; 222 | 223 | void main() { 224 | gl_Position = vec4(position, 0.0, 1.0); 225 | v_tex_coords = tex_coords; 226 | v_color = color; 227 | v_mode = mode; 228 | } 229 | "; 230 | 231 | /// The fragment shader used within the `glium::Program` for OpenGL ES. 232 | pub const FRAGMENT_SHADER_300_ES: &'static str = " 233 | #version 300 es 234 | precision mediump float; 235 | uniform sampler2D tex; 236 | 237 | in vec2 v_tex_coords; 238 | in vec4 v_color; 239 | flat in uint v_mode; 240 | 241 | out vec4 f_color; 242 | 243 | void main() { 244 | // Text 245 | if (v_mode == uint(0)) { 246 | f_color = v_color * vec4(1.0, 1.0, 1.0, texture(tex, v_tex_coords).r); 247 | 248 | // Image 249 | } else if (v_mode == uint(1)) { 250 | f_color = texture(tex, v_tex_coords); 251 | 252 | // 2D Geometry 253 | } else if (v_mode == uint(2)) { 254 | f_color = v_color; 255 | } 256 | } 257 | "; 258 | 259 | /// Glium textures that have two dimensions. 260 | pub trait TextureDimensions { 261 | /// The width and height of the texture. 262 | fn dimensions(&self) -> (u32, u32); 263 | } 264 | 265 | impl TextureDimensions for T 266 | where T: std::ops::Deref, 267 | { 268 | fn dimensions(&self) -> (u32, u32) { 269 | (self.get_width(), self.get_height().unwrap_or(0)) 270 | } 271 | } 272 | 273 | 274 | /// Construct the glium shader program that can be used to render `Vertex`es. 275 | pub fn program(facade: &F) -> Result 276 | where F: glium::backend::Facade, 277 | { 278 | program!(facade, 279 | 120 => { vertex: VERTEX_SHADER_120, fragment: FRAGMENT_SHADER_120 }, 280 | 140 => { vertex: VERTEX_SHADER_140, fragment: FRAGMENT_SHADER_140 }, 281 | 300 es => { vertex: VERTEX_SHADER_300_ES, fragment: FRAGMENT_SHADER_300_ES }) 282 | } 283 | 284 | /// Default glium `DrawParameters` with alpha blending enabled. 285 | pub fn draw_parameters() -> glium::DrawParameters<'static> { 286 | let blend = glium::Blend::alpha_blending(); 287 | glium::DrawParameters { multisampling: true, blend: blend, ..Default::default() } 288 | } 289 | 290 | 291 | /// Converts gamma (brightness) from sRGB to linear color space. 292 | /// 293 | /// sRGB is the default color space for image editors, pictures, internet etc. 294 | /// Linear gamma yields better results when doing math with colors. 295 | pub fn gamma_srgb_to_linear(c: [f32; 4]) -> [f32; 4] { 296 | fn component(f: f32) -> f32 { 297 | // Taken from https://github.com/PistonDevelopers/graphics/src/color.rs#L42 298 | if f <= 0.04045 { 299 | f / 12.92 300 | } else { 301 | ((f + 0.055) / 1.055).powf(2.4) 302 | } 303 | } 304 | [component(c[0]), component(c[1]), component(c[2]), c[3]] 305 | } 306 | 307 | 308 | /// Return the optimal client format for the text texture given the version. 309 | pub fn text_texture_client_format(opengl_version: &glium::Version) -> glium::texture::ClientFormat { 310 | match *opengl_version { 311 | // If the version is greater than or equal to GL 3.0 or GLes 3.0, we can use the `U8` format. 312 | glium::Version(_, major, _) if major >= 3 => glium::texture::ClientFormat::U8, 313 | // Otherwise, we must use the `U8U8U8` format to support older versions. 314 | _ => glium::texture::ClientFormat::U8U8U8, 315 | } 316 | } 317 | 318 | /// Return the optimal uncompressed float format for the text texture given the version. 319 | pub fn text_texture_uncompressed_float_format(opengl_version: &glium::Version) -> glium::texture::UncompressedFloatFormat { 320 | match *opengl_version { 321 | // If the version is greater than or equal to GL 3.0 or GLes 3.0, we can use the `U8` format. 322 | glium::Version(_, major, _) if major >= 3 => glium::texture::UncompressedFloatFormat::U8, 323 | // Otherwise, we must use the `U8U8U8` format to support older versions. 324 | _ => glium::texture::UncompressedFloatFormat::U8U8U8, 325 | } 326 | } 327 | 328 | 329 | impl GlyphCache { 330 | 331 | /// Construct a `GlyphCache` with a size equal to the given `Display`'s current framebuffer 332 | /// dimensions. 333 | pub fn new(facade: &F) -> Result 334 | where F: glium::backend::Facade, 335 | { 336 | const SCALE_TOLERANCE: f32 = 0.1; 337 | const POSITION_TOLERANCE: f32 = 0.1; 338 | 339 | let context = facade.get_context(); 340 | let (w, h) = context.get_framebuffer_dimensions(); 341 | 342 | // Determine the optimal texture format to use given the opengl version. 343 | let opengl_version = context.get_opengl_version(); 344 | let client_format = text_texture_client_format(opengl_version); 345 | let uncompressed_float_format = text_texture_uncompressed_float_format(opengl_version); 346 | 347 | // Construct the `GlyphCache`. 348 | let num_components = client_format.get_num_components() as u32; 349 | 350 | let buffer_w = num_components * w; 351 | 352 | // First, the rusttype `Cache` which performs the logic for rendering and laying out glyphs 353 | // in the cache. 354 | let cache = text::GlyphCache::new(w, h, SCALE_TOLERANCE, POSITION_TOLERANCE); 355 | 356 | // Now the texture to which glyphs will be rendered. 357 | let grey_image = glium::texture::RawImage2d { 358 | data: std::borrow::Cow::Owned(vec![128u8; buffer_w as usize * h as usize]), 359 | width: w, 360 | height: h, 361 | format: client_format, 362 | }; 363 | let format = uncompressed_float_format; 364 | let no_mipmap = glium::texture::MipmapsOption::NoMipmap; 365 | let texture = try!(glium::texture::Texture2d::with_format(facade, grey_image, format, no_mipmap)); 366 | 367 | Ok(GlyphCache { 368 | cache: cache, 369 | texture: texture, 370 | }) 371 | } 372 | 373 | /// The texture used to cache the glyphs on the GPU. 374 | pub fn texture(&self) -> &glium::texture::Texture2d { 375 | &self.texture 376 | } 377 | 378 | } 379 | 380 | 381 | impl Renderer { 382 | 383 | /// Construct a new empty `Renderer`. 384 | pub fn new(facade: &F) -> Result 385 | where F: glium::backend::Facade, 386 | { 387 | let program = try!(program(facade)); 388 | let glyph_cache = try!(GlyphCache::new(facade)); 389 | Ok(Renderer { 390 | program: program, 391 | glyph_cache: glyph_cache, 392 | commands: Vec::new(), 393 | vertices: Vec::new(), 394 | }) 395 | } 396 | 397 | /// Produce an `Iterator` yielding `Command`s. 398 | pub fn commands(&self) -> Commands { 399 | let Renderer { ref commands, .. } = *self; 400 | Commands { 401 | commands: commands.iter(), 402 | } 403 | } 404 | 405 | /// Fill the inner vertex and command buffers by translating the given `primitives`. 406 | pub fn fill(&mut self, 407 | display: &glium::Display, 408 | mut primitives: P, 409 | image_map: &image::Map) 410 | where P: render::PrimitiveWalker, 411 | T: TextureDimensions, 412 | { 413 | let Renderer { ref mut commands, ref mut vertices, ref mut glyph_cache, .. } = *self; 414 | 415 | commands.clear(); 416 | vertices.clear(); 417 | 418 | // This is necessary for supporting rusttype's GPU cache with OpenGL versions older than GL 419 | // 3.0 and GL ES 3.0. It is used to convert from the `U8` data format given by `rusttype` 420 | // to the `U8U8U8` format that is necessary for older versions of OpenGL. 421 | // 422 | // The buffer is only used if an older version was detected, otherwise the text GPU cache 423 | // uses the rusttype `data` buffer directly. 424 | let mut text_data_u8u8u8 = Vec::new(); 425 | 426 | // Determine the texture format that we're using. 427 | let opengl_version = display.get_opengl_version(); 428 | let client_format = text_texture_client_format(opengl_version); 429 | 430 | enum State { 431 | Image { image_id: image::Id, start: usize }, 432 | Plain { start: usize }, 433 | } 434 | 435 | let mut current_state = State::Plain { start: 0 }; 436 | 437 | // Switches to the `Plain` state and completes the previous `Command` if not already in the 438 | // `Plain` state. 439 | macro_rules! switch_to_plain_state { 440 | () => { 441 | match current_state { 442 | State::Plain { .. } => (), 443 | State::Image { image_id, start } => { 444 | commands.push(PreparedCommand::Image(image_id, start..vertices.len())); 445 | current_state = State::Plain { start: vertices.len() }; 446 | }, 447 | } 448 | }; 449 | } 450 | 451 | // Framebuffer dimensions and the "dots per inch" factor. 452 | let (screen_w, screen_h) = display.get_framebuffer_dimensions(); 453 | let (win_w, win_h) = (screen_w as Scalar, screen_h as Scalar); 454 | let half_win_w = win_w / 2.0; 455 | let half_win_h = win_h / 2.0; 456 | let dpi_factor = display.gl_window().hidpi_factor() as Scalar; 457 | 458 | // Functions for converting for conrod scalar coords to GL vertex coords (-1.0 to 1.0). 459 | let vx = |x: Scalar| (x * dpi_factor / half_win_w) as f32; 460 | let vy = |y: Scalar| (y * dpi_factor / half_win_h) as f32; 461 | 462 | let mut current_scizzor = glium::Rect { 463 | left: 0, 464 | width: screen_w, 465 | bottom: 0, 466 | height: screen_h, 467 | }; 468 | 469 | let rect_to_glium_rect = |rect: Rect| { 470 | let (w, h) = rect.w_h(); 471 | let left = (rect.left() * dpi_factor + half_win_w) as u32; 472 | let bottom = (rect.bottom() * dpi_factor + half_win_h) as u32; 473 | let width = (w * dpi_factor) as u32; 474 | let height = (h * dpi_factor) as u32; 475 | glium::Rect { 476 | left: std::cmp::max(left, 0), 477 | bottom: std::cmp::max(bottom, 0), 478 | width: std::cmp::min(width, screen_w), 479 | height: std::cmp::min(height, screen_h), 480 | } 481 | }; 482 | 483 | // Draw each primitive in order of depth. 484 | while let Some(primitive) = primitives.next_primitive() { 485 | let render::Primitive { kind, scizzor, rect, .. } = primitive; 486 | 487 | // Check for a `Scizzor` command. 488 | let new_scizzor = rect_to_glium_rect(scizzor); 489 | if new_scizzor != current_scizzor { 490 | // Finish the current command. 491 | match current_state { 492 | State::Plain { start } => 493 | commands.push(PreparedCommand::Plain(start..vertices.len())), 494 | State::Image { image_id, start } => 495 | commands.push(PreparedCommand::Image(image_id, start..vertices.len())), 496 | } 497 | 498 | // Update the scizzor and produce a command. 499 | current_scizzor = new_scizzor; 500 | commands.push(PreparedCommand::Scizzor(new_scizzor)); 501 | 502 | // Set the state back to plain drawing. 503 | current_state = State::Plain { start: vertices.len() }; 504 | } 505 | 506 | match kind { 507 | 508 | render::PrimitiveKind::Rectangle { color } => { 509 | switch_to_plain_state!(); 510 | 511 | let color = gamma_srgb_to_linear(color.to_fsa()); 512 | let (l, r, b, t) = rect.l_r_b_t(); 513 | 514 | let v = |x, y| { 515 | // Convert from conrod Scalar range to GL range -1.0 to 1.0. 516 | Vertex { 517 | position: [vx(x), vy(y)], 518 | tex_coords: [0.0, 0.0], 519 | color: color, 520 | mode: MODE_GEOMETRY, 521 | } 522 | }; 523 | 524 | let mut push_v = |x, y| vertices.push(v(x, y)); 525 | 526 | // Bottom left triangle. 527 | push_v(l, t); 528 | push_v(r, b); 529 | push_v(l, b); 530 | 531 | // Top right triangle. 532 | push_v(l, t); 533 | push_v(r, b); 534 | push_v(r, t); 535 | }, 536 | 537 | render::PrimitiveKind::TrianglesSingleColor { color, triangles } => { 538 | if triangles.is_empty() { 539 | continue; 540 | } 541 | 542 | switch_to_plain_state!(); 543 | 544 | let color = gamma_srgb_to_linear(color.into()); 545 | 546 | let v = |p: [Scalar; 2]| { 547 | Vertex { 548 | position: [vx(p[0]), vy(p[1])], 549 | tex_coords: [0.0, 0.0], 550 | color: color, 551 | mode: MODE_GEOMETRY, 552 | } 553 | }; 554 | 555 | for triangle in triangles { 556 | vertices.push(v(triangle[0])); 557 | vertices.push(v(triangle[1])); 558 | vertices.push(v(triangle[2])); 559 | } 560 | }, 561 | 562 | render::PrimitiveKind::TrianglesMultiColor { triangles } => { 563 | if triangles.is_empty() { 564 | continue; 565 | } 566 | 567 | switch_to_plain_state!(); 568 | 569 | let v = |(p, c): ([Scalar; 2], color::Rgba)| { 570 | Vertex { 571 | position: [vx(p[0]), vy(p[1])], 572 | tex_coords: [0.0, 0.0], 573 | color: gamma_srgb_to_linear(c.into()), 574 | mode: MODE_GEOMETRY, 575 | } 576 | }; 577 | 578 | for triangle in triangles { 579 | vertices.push(v(triangle[0])); 580 | vertices.push(v(triangle[1])); 581 | vertices.push(v(triangle[2])); 582 | } 583 | }, 584 | 585 | render::PrimitiveKind::Text { color, text, font_id } => { 586 | switch_to_plain_state!(); 587 | 588 | let positioned_glyphs = text.positioned_glyphs(dpi_factor as f32); 589 | 590 | let GlyphCache { ref mut cache, ref mut texture } = *glyph_cache; 591 | 592 | // Queue the glyphs to be cached. 593 | for glyph in positioned_glyphs.iter() { 594 | cache.queue_glyph(font_id.index(), glyph.clone()); 595 | } 596 | 597 | // Cache the glyphs on the GPU. 598 | cache.cache_queued(|rect, data| { 599 | let w = rect.width(); 600 | let h = rect.height(); 601 | let glium_rect = glium::Rect { 602 | left: rect.min.x, 603 | bottom: rect.min.y, 604 | width: w, 605 | height: h, 606 | }; 607 | 608 | let data = match client_format { 609 | // `rusttype` gives data in the `U8` format so we can use it directly. 610 | glium::texture::ClientFormat::U8 => std::borrow::Cow::Borrowed(data), 611 | // Otherwise we have to convert to the supported format. 612 | glium::texture::ClientFormat::U8U8U8 => { 613 | text_data_u8u8u8.clear(); 614 | for &b in data.iter() { 615 | text_data_u8u8u8.push(b); 616 | text_data_u8u8u8.push(b); 617 | text_data_u8u8u8.push(b); 618 | } 619 | std::borrow::Cow::Borrowed(&text_data_u8u8u8[..]) 620 | }, 621 | // The text cache is only ever created with U8 or U8U8U8 formats. 622 | _ => unreachable!(), 623 | }; 624 | 625 | let image = glium::texture::RawImage2d { 626 | data: data, 627 | width: w, 628 | height: h, 629 | format: client_format, 630 | }; 631 | texture.main_level().write(glium_rect, image); 632 | }).unwrap(); 633 | 634 | let color = gamma_srgb_to_linear(color.to_fsa()); 635 | 636 | let cache_id = font_id.index(); 637 | 638 | let origin = text::rt::point(0.0, 0.0); 639 | let to_gl_rect = |screen_rect: text::rt::Rect| text::rt::Rect { 640 | min: origin 641 | + (text::rt::vector(screen_rect.min.x as f32 / screen_w as f32 - 0.5, 642 | 1.0 - screen_rect.min.y as f32 / screen_h as f32 - 0.5)) * 2.0, 643 | max: origin 644 | + (text::rt::vector(screen_rect.max.x as f32 / screen_w as f32 - 0.5, 645 | 1.0 - screen_rect.max.y as f32 / screen_h as f32 - 0.5)) * 2.0 646 | }; 647 | 648 | for g in positioned_glyphs { 649 | if let Ok(Some((uv_rect, screen_rect))) = cache.rect_for(cache_id, g) { 650 | let gl_rect = to_gl_rect(screen_rect); 651 | let v = |p, t| Vertex { 652 | position: p, 653 | tex_coords: t, 654 | color: color, 655 | mode: MODE_TEXT, 656 | }; 657 | let mut push_v = |p, t| vertices.push(v(p, t)); 658 | push_v([gl_rect.min.x, gl_rect.max.y], [uv_rect.min.x, uv_rect.max.y]); 659 | push_v([gl_rect.min.x, gl_rect.min.y], [uv_rect.min.x, uv_rect.min.y]); 660 | push_v([gl_rect.max.x, gl_rect.min.y], [uv_rect.max.x, uv_rect.min.y]); 661 | push_v([gl_rect.max.x, gl_rect.min.y], [uv_rect.max.x, uv_rect.min.y]); 662 | push_v([gl_rect.max.x, gl_rect.max.y], [uv_rect.max.x, uv_rect.max.y]); 663 | push_v([gl_rect.min.x, gl_rect.max.y], [uv_rect.min.x, uv_rect.max.y]); 664 | } 665 | } 666 | }, 667 | 668 | render::PrimitiveKind::Image { image_id, color, source_rect } => { 669 | 670 | // Switch to the `Image` state for this image if we're not in it already. 671 | let new_image_id = image_id; 672 | match current_state { 673 | 674 | // If we're already in the drawing mode for this image, we're done. 675 | State::Image { image_id, .. } if image_id == new_image_id => (), 676 | 677 | // If we were in the `Plain` drawing state, switch to Image drawing state. 678 | State::Plain { start } => { 679 | commands.push(PreparedCommand::Plain(start..vertices.len())); 680 | current_state = State::Image { 681 | image_id: new_image_id, 682 | start: vertices.len(), 683 | }; 684 | }, 685 | 686 | // If we were drawing a different image, switch state to draw *this* image. 687 | State::Image { image_id, start } => { 688 | commands.push(PreparedCommand::Image(image_id, start..vertices.len())); 689 | current_state = State::Image { 690 | image_id: new_image_id, 691 | start: vertices.len(), 692 | }; 693 | }, 694 | } 695 | 696 | let color = color.unwrap_or(color::WHITE).to_fsa(); 697 | 698 | let (image_w, image_h) = image_map.get(&image_id).unwrap().dimensions(); 699 | let (image_w, image_h) = (image_w as Scalar, image_h as Scalar); 700 | 701 | // Get the sides of the source rectangle as uv coordinates. 702 | // 703 | // Texture coordinates range: 704 | // - left to right: 0.0 to 1.0 705 | // - bottom to top: 0.0 to 1.0 706 | let (uv_l, uv_r, uv_b, uv_t) = match source_rect { 707 | Some(src_rect) => { 708 | let (l, r, b, t) = src_rect.l_r_b_t(); 709 | ((l / image_w) as f32, 710 | (r / image_w) as f32, 711 | (b / image_h) as f32, 712 | (t / image_h) as f32) 713 | }, 714 | None => (0.0, 1.0, 0.0, 1.0), 715 | }; 716 | 717 | let v = |x, y, t| { 718 | // Convert from conrod Scalar range to GL range -1.0 to 1.0. 719 | let x = (x * dpi_factor as Scalar / half_win_w) as f32; 720 | let y = (y * dpi_factor as Scalar / half_win_h) as f32; 721 | Vertex { 722 | position: [x, y], 723 | tex_coords: t, 724 | color: color, 725 | mode: MODE_IMAGE, 726 | } 727 | }; 728 | 729 | let mut push_v = |x, y, t| vertices.push(v(x, y, t)); 730 | 731 | let (l, r, b, t) = rect.l_r_b_t(); 732 | 733 | // Bottom left triangle. 734 | push_v(l, t, [uv_l, uv_t]); 735 | push_v(r, b, [uv_r, uv_b]); 736 | push_v(l, b, [uv_l, uv_b]); 737 | 738 | // Top right triangle. 739 | push_v(l, t, [uv_l, uv_t]); 740 | push_v(r, b, [uv_r, uv_b]); 741 | push_v(r, t, [uv_r, uv_t]); 742 | }, 743 | 744 | // We have no special case widgets to handle. 745 | render::PrimitiveKind::Other(_) => (), 746 | } 747 | 748 | } 749 | 750 | // Enter the final command. 751 | match current_state { 752 | State::Plain { start } => 753 | commands.push(PreparedCommand::Plain(start..vertices.len())), 754 | State::Image { image_id, start } => 755 | commands.push(PreparedCommand::Image(image_id, start..vertices.len())), 756 | } 757 | } 758 | 759 | /// Draws using the inner list of `Command`s to the given `display`. 760 | /// 761 | /// Note: If you require more granular control over rendering, you may want to use the `fill` 762 | /// and `commands` methods separately. This method is simply a convenience wrapper around those 763 | /// methods for the case that the user does not require accessing or modifying conrod's draw 764 | /// parameters, uniforms or generated draw commands. 765 | pub fn draw(&self, facade: &F, surface: &mut S, image_map: &image::Map) 766 | -> Result<(), DrawError> 767 | where F: glium::backend::Facade, 768 | S: glium::Surface, 769 | for<'a> glium::uniforms::Sampler<'a, T>: glium::uniforms::AsUniformValue, 770 | { 771 | let mut draw_params = draw_parameters(); 772 | let no_indices = glium::index::NoIndices(glium::index::PrimitiveType::TrianglesList); 773 | let uniforms = uniform! { 774 | tex: self.glyph_cache.texture() 775 | .sampled() 776 | .magnify_filter(glium::uniforms::MagnifySamplerFilter::Nearest) 777 | .minify_filter(glium::uniforms::MinifySamplerFilter::Linear) 778 | }; 779 | 780 | const NUM_VERTICES_IN_TRIANGLE: usize = 3; 781 | let vbuffer = glium::VertexBuffer::new(facade, &self.vertices)?; 782 | 783 | for command in self.commands() { 784 | match command { 785 | 786 | // Update the `scizzor` before continuing to draw. 787 | Command::Scizzor(scizzor) => draw_params.scissor = Some(scizzor), 788 | 789 | // Draw to the target with the given `draw` command. 790 | Command::Draw(draw) => match draw { 791 | 792 | // Draw text and plain 2D geometry. 793 | // 794 | // Only submit the vertices if there is enough for at least one triangle. 795 | Draw::Plain(slice) => if slice.len() >= NUM_VERTICES_IN_TRIANGLE { 796 | let vertex_buffer = vbuffer.slice(slice).unwrap(); 797 | // let vertex_buffer = try!(glium::VertexBuffer::new(facade, slice)); 798 | surface.draw(vertex_buffer, no_indices, &self.program, &uniforms, &draw_params).unwrap(); 799 | }, 800 | 801 | // Draw an image whose texture data lies within the `image_map` at the 802 | // given `id`. 803 | // 804 | // Only submit the vertices if there is enough for at least one triangle. 805 | Draw::Image(image_id, slice) => if slice.len() >= NUM_VERTICES_IN_TRIANGLE { 806 | let vertex_buffer = vbuffer.slice(slice).unwrap(); 807 | // let vertex_buffer = glium::VertexBuffer::new(facade, slice).unwrap(); 808 | let image = image_map.get(&image_id).unwrap(); 809 | let image_uniforms = uniform! { 810 | tex: glium::uniforms::Sampler::new(image) 811 | .wrap_function(glium::uniforms::SamplerWrapFunction::Clamp) 812 | .magnify_filter(glium::uniforms::MagnifySamplerFilter::Nearest), 813 | }; 814 | surface.draw(vertex_buffer, no_indices, &self.program, &image_uniforms, &draw_params).unwrap(); 815 | }, 816 | 817 | } 818 | } 819 | } 820 | 821 | Ok(()) 822 | } 823 | 824 | } 825 | 826 | impl<'a> Iterator for Commands<'a> { 827 | type Item = Command; 828 | fn next(&mut self) -> Option { 829 | let Commands { ref mut commands, } = *self; 830 | commands.next().map(|command| match *command { 831 | PreparedCommand::Scizzor(scizzor) => Command::Scizzor(scizzor), 832 | PreparedCommand::Plain(ref range) => 833 | Command::Draw(Draw::Plain(range.clone())), 834 | PreparedCommand::Image(id, ref range) => 835 | Command::Draw(Draw::Image(id, range.clone())), 836 | }) 837 | } 838 | } 839 | 840 | impl From for RendererCreationError { 841 | fn from(err: glium::texture::TextureCreationError) -> Self { 842 | RendererCreationError::Texture(err) 843 | } 844 | } 845 | 846 | impl From for RendererCreationError { 847 | fn from(err: glium::program::ProgramChooserCreationError) -> Self { 848 | RendererCreationError::Program(err) 849 | } 850 | } 851 | 852 | impl std::error::Error for RendererCreationError { 853 | fn description(&self) -> &str { 854 | match *self { 855 | RendererCreationError::Texture(ref e) => std::error::Error::description(e), 856 | RendererCreationError::Program(ref e) => std::error::Error::description(e), 857 | } 858 | } 859 | } 860 | 861 | impl std::fmt::Display for RendererCreationError { 862 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 863 | match *self { 864 | RendererCreationError::Texture(ref e) => std::fmt::Display::fmt(e, f), 865 | RendererCreationError::Program(ref e) => std::fmt::Display::fmt(e, f), 866 | } 867 | } 868 | } 869 | 870 | impl From for DrawError { 871 | fn from(err: glium::vertex::BufferCreationError) -> Self { 872 | DrawError::Buffer(err) 873 | } 874 | } 875 | 876 | impl From for DrawError { 877 | fn from(err: glium::DrawError) -> Self { 878 | DrawError::Draw(err) 879 | } 880 | } 881 | 882 | impl std::error::Error for DrawError { 883 | fn description(&self) -> &str { 884 | match *self { 885 | DrawError::Buffer(ref e) => std::error::Error::description(e), 886 | DrawError::Draw(ref e) => std::error::Error::description(e), 887 | } 888 | } 889 | } 890 | 891 | impl std::fmt::Display for DrawError { 892 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 893 | match *self { 894 | DrawError::Buffer(ref e) => std::fmt::Display::fmt(e, f), 895 | DrawError::Draw(ref e) => std::fmt::Display::fmt(e, f), 896 | } 897 | } 898 | } 899 | -------------------------------------------------------------------------------- /ui/src/rust/layout/left_panel.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use conrod::{self, color, Borderable, Colorable, Labelable, Positionable, Sizeable, Widget}; 4 | use conrod::widget::*; 5 | 6 | use super::{Ids, HEADER_HEIGHT}; 7 | use ui_state::{self, Event as UiEvent, MenuState}; 8 | 9 | pub struct LeftPanelIds { 10 | pub panel_toggle: Id, 11 | pub open_panel_canvas: Id, 12 | } 13 | 14 | impl LeftPanelIds { 15 | pub fn new(gen: &mut id::Generator) -> Self { 16 | LeftPanelIds { 17 | panel_toggle: gen.next(), 18 | open_panel_canvas: gen.next(), 19 | } 20 | } 21 | } 22 | 23 | pub fn left_panel_available( 24 | ui: &mut conrod::UiCell, 25 | ids: &Ids, 26 | state: &ui_state::PanelStates, 27 | update: &mut VecDeque, 28 | ) { 29 | let left_toggle_clicks = Button::new() 30 | // style 31 | .color(color::DARK_CHARCOAL) 32 | .border(0.0) 33 | .w_h(100.0, HEADER_HEIGHT) 34 | // label 35 | .label("Screeps") 36 | .small_font(&ui) 37 | .left_justify_label() 38 | .label_color(color::WHITE) 39 | // place 40 | .parent(ids.root.header) 41 | .top_left_of(ids.root.header) 42 | .set(ids.left_panel.panel_toggle, ui) 43 | // now TimesClicked(u16) 44 | .0; 45 | 46 | match state.left { 47 | MenuState::Open => { 48 | left_panel_panel_open(ui, ids, update); 49 | 50 | if left_toggle_clicks % 2 == 1 51 | || left_toggle_clicks == 0 52 | && ui.global_input() 53 | .current 54 | .mouse 55 | .buttons 56 | .pressed() 57 | .next() 58 | .is_some() 59 | && ui.global_input() 60 | .current 61 | .widget_capturing_mouse 62 | .or_else(|| ui.global_input().current.widget_under_mouse) 63 | .map(|capturing| { 64 | capturing != ids.left_panel.panel_toggle 65 | && !ui.widget_graph().does_recursive_edge_exist( 66 | ids.left_panel.open_panel_canvas, 67 | capturing, 68 | |_| true, 69 | ) 70 | && !ui.widget_graph().does_recursive_edge_exist( 71 | ids.left_panel.panel_toggle, 72 | capturing, 73 | |_| true, 74 | ) 75 | }) 76 | .unwrap_or(true) 77 | { 78 | update.push_back(UiEvent::LeftMenuClosed); 79 | } 80 | } 81 | MenuState::Closed => if left_toggle_clicks % 2 == 1 { 82 | update.push_back(UiEvent::LeftMenuOpened); 83 | }, 84 | } 85 | } 86 | 87 | pub fn left_panel_panel_open(ui: &mut conrod::UiCell, ids: &Ids, _update: &mut VecDeque) { 88 | Canvas::new() 89 | // style 90 | .color(color::DARK_CHARCOAL) 91 | .border(0.0) 92 | .w_h(300.0, ui.window_dim()[1] - HEADER_HEIGHT) 93 | // behavior 94 | .scroll_kids_vertically() 95 | // place 96 | .floating(true) 97 | .mid_left_of(ids.root.root) 98 | .down_from(ids.left_panel.panel_toggle, 0.0) 99 | .set(ids.left_panel.open_panel_canvas, ui); 100 | } 101 | -------------------------------------------------------------------------------- /ui/src/rust/layout/login_screen.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use conrod::{self, color, Borderable, Colorable, Labelable, Positionable, Sizeable, Widget}; 4 | use conrod::widget::*; 5 | 6 | use time; 7 | 8 | use screeps_rs_network::{self, ConnectionSettings}; 9 | use widgets::text_box::TextBox; 10 | use ui_state::{Event as UiEvent, LoginScreenState}; 11 | 12 | use app::AppCell; 13 | use layout::{frame, HEADER_HEIGHT}; 14 | const LOGIN_WIDTH: conrod::Scalar = 300.0; 15 | const LOGIN_HEIGHT: conrod::Scalar = 200.0; 16 | 17 | const LOGIN_PADDING: conrod::Scalar = 10.0; 18 | 19 | const LOGIN_LOWER_SECTION_HEIGHT: conrod::Scalar = (LOGIN_HEIGHT - HEADER_HEIGHT) / 3.0; 20 | 21 | #[derive(Copy, Clone)] 22 | struct TextboxIds { 23 | canvas: Id, 24 | textbox: Id, 25 | label: Id, 26 | } 27 | 28 | #[derive(Copy, Clone)] 29 | pub struct LoginIds { 30 | root: Id, 31 | header_canvas: Id, 32 | server: TextboxIds, 33 | username: TextboxIds, 34 | password: TextboxIds, 35 | shard: TextboxIds, 36 | submit_canvas: Id, 37 | exit_button: Id, 38 | submit_button: Id, 39 | } 40 | 41 | impl TextboxIds { 42 | pub fn new(gen: &mut id::Generator) -> Self { 43 | TextboxIds { 44 | canvas: gen.next(), 45 | textbox: gen.next(), 46 | label: gen.next(), 47 | } 48 | } 49 | } 50 | 51 | impl LoginIds { 52 | pub fn new(gen: &mut id::Generator) -> Self { 53 | LoginIds { 54 | root: gen.next(), 55 | header_canvas: gen.next(), 56 | server: TextboxIds::new(gen), 57 | username: TextboxIds::new(gen), 58 | password: TextboxIds::new(gen), 59 | shard: TextboxIds::new(gen), 60 | submit_canvas: gen.next(), 61 | exit_button: gen.next(), 62 | submit_button: gen.next(), 63 | } 64 | } 65 | } 66 | 67 | pub fn create_ui(app: &mut AppCell, state: &LoginScreenState, update: &mut VecDeque) { 68 | if app.net_cache.login_state() == screeps_rs_network::LoginState::LoggedIn { 69 | update.push_front(UiEvent::LoggedInMapView); 70 | } 71 | 72 | let AppCell { 73 | ref mut ui, 74 | ref ids, 75 | .. 76 | } = *app; 77 | 78 | use widgets::text_box::Event as TextBoxEvent; 79 | 80 | let body = Canvas::new().color(color::CHARCOAL).border(0.0); 81 | frame(ui, ids, ids.root.body, body); 82 | 83 | let header_canvas = Canvas::new() 84 | // style 85 | .color(color::DARK_CHARCOAL) 86 | .border(0.0) 87 | .length(HEADER_HEIGHT); 88 | 89 | let bottom_template = Canvas::new() 90 | // style 91 | .color(color::DARK_GREY) 92 | .border_color(color::BLACK); 93 | 94 | // root canvas 95 | Canvas::new() 96 | // style 97 | .color(color::GREY) 98 | .border(2.0) 99 | .border_color(color::DARK_GREY) 100 | .w_h(LOGIN_WIDTH, LOGIN_HEIGHT) 101 | // behavior 102 | .flow_down(&[ 103 | (ids.login.header_canvas, header_canvas), 104 | (ids.login.server.canvas, bottom_template.clone()), 105 | (ids.login.username.canvas, bottom_template.clone()), 106 | (ids.login.password.canvas, bottom_template.clone()), 107 | (ids.login.shard.canvas, bottom_template.clone()), 108 | (ids.login.submit_canvas, bottom_template), 109 | ]) 110 | // place 111 | .floating(true) 112 | .mid_top_of(ids.root.root) 113 | .down_from(ids.root.header, ui.window_dim()[1] / 4.0 - HEADER_HEIGHT) 114 | // set 115 | .set(ids.login.root, ui); 116 | 117 | fn textbox_field( 118 | text: &str, 119 | mut update: F, 120 | ids: TextboxIds, 121 | width: conrod::Scalar, 122 | hide: bool, 123 | ui: &mut conrod::UiCell, 124 | ) -> bool { 125 | let events = TextBox::new(&text) 126 | // style 127 | .w_h(width, LOGIN_LOWER_SECTION_HEIGHT - LOGIN_PADDING * 2.0) 128 | .font_size(ui.theme.font_size_small) 129 | .left_justify() 130 | .pad_text(5.0) 131 | .hide_with_char(if hide { Some('*') } else { None }) 132 | // position 133 | .mid_right_with_margin_on(ids.canvas, 10.0) 134 | .set(ids.textbox, ui); 135 | 136 | let mut enter_pressed = false; 137 | 138 | for event in events.into_iter() { 139 | match event { 140 | TextBoxEvent::Update(s) => { 141 | update(s); 142 | } 143 | TextBoxEvent::Enter => { 144 | enter_pressed = true; 145 | break; 146 | } 147 | } 148 | } 149 | enter_pressed 150 | } 151 | 152 | fn textbox_label(text: &str, ids: TextboxIds, ui: &mut conrod::UiCell) { 153 | Text::new(text) 154 | // style 155 | .font_size(ui.theme.font_size_small) 156 | .center_justify() 157 | .no_line_wrap() 158 | // position 159 | .mid_left_with_margin_on(ids.canvas, LOGIN_PADDING) 160 | .set(ids.label, ui); 161 | } 162 | 163 | textbox_label("server", ids.login.server, ui); 164 | textbox_label("username", ids.login.username, ui); 165 | textbox_label("password", ids.login.password, ui); 166 | textbox_label("shard", ids.login.shard, ui); 167 | 168 | let scalar_max = |f1_opt, f2_opt| match (f1_opt, f2_opt) { 169 | (Some(f1), Some(f2)) => Some(conrod::Scalar::max(f1, f2)), 170 | (Some(v), None) | (None, Some(v)) => Some(v), 171 | (None, None) => None, 172 | }; 173 | let label_width = scalar_max( 174 | scalar_max( 175 | ui.w_of(ids.login.server.label), 176 | ui.w_of(ids.login.username.label), 177 | ), 178 | scalar_max( 179 | ui.w_of(ids.login.password.label), 180 | ui.w_of(ids.login.shard.label), 181 | ), 182 | ).unwrap_or(LOGIN_WIDTH / 2.0 - LOGIN_PADDING * 1.5); 183 | 184 | // Server field 185 | let server_enter_pressed = textbox_field( 186 | &state.server, 187 | |s| update.push_front(UiEvent::LoginServer(s)), 188 | ids.login.server, 189 | LOGIN_WIDTH - LOGIN_PADDING * 3.0 - label_width, 190 | false, 191 | ui, 192 | ); 193 | 194 | // Username field 195 | let username_enter_pressed = textbox_field( 196 | &state.username, 197 | |s| update.push_front(UiEvent::LoginUsername(s)), 198 | ids.login.username, 199 | LOGIN_WIDTH - LOGIN_PADDING * 3.0 - label_width, 200 | false, 201 | ui, 202 | ); 203 | 204 | // Password field 205 | let password_enter_pressed = textbox_field( 206 | &state.password, 207 | |s| update.push_front(UiEvent::LoginPassword(s)), 208 | ids.login.password, 209 | LOGIN_WIDTH - LOGIN_PADDING * 3.0 - label_width, 210 | true, 211 | ui, 212 | ); 213 | 214 | // Shard field 215 | let shard_enter_pressed = textbox_field( 216 | &state.shard, 217 | |s| update.push_front(UiEvent::LoginShard(s)), 218 | ids.login.shard, 219 | LOGIN_WIDTH - LOGIN_PADDING * 3.0 - label_width, 220 | false, 221 | ui, 222 | ); 223 | 224 | let submit_pressed = Button::new() 225 | // style 226 | .color(color::DARK_CHARCOAL) 227 | .border(0.0) 228 | .w_h(LOGIN_WIDTH / 2.0 - 30.0, LOGIN_LOWER_SECTION_HEIGHT - LOGIN_PADDING * 2.0) 229 | // label 230 | .label("submit") 231 | .small_font(ui) 232 | .center_justify_label() 233 | // position 234 | .mid_right_with_margin_on(ids.login.submit_canvas, 10.0) 235 | .set(ids.login.submit_button, ui) 236 | // now TimesClicked 237 | .was_clicked(); 238 | 239 | let exit_pressed = Button::new() 240 | // style 241 | .color(color::DARK_CHARCOAL) 242 | .border(0.0) 243 | .w_h(LOGIN_WIDTH / 2.0 - 30.0, LOGIN_LOWER_SECTION_HEIGHT - LOGIN_PADDING * 2.0) 244 | // label 245 | .label("exit") 246 | .small_font(ui) 247 | .center_justify_label() 248 | // position 249 | .mid_left_with_margin_on(ids.login.submit_canvas, 10.0) 250 | .set(ids.login.exit_button, ui) 251 | // now TimesClicked 252 | .was_clicked(); 253 | 254 | if exit_pressed { 255 | update.push_front(UiEvent::Exit); 256 | } else if (submit_pressed || password_enter_pressed || username_enter_pressed || server_enter_pressed 257 | || shard_enter_pressed) && state.username.len() > 0 && state.password.len() > 0 258 | { 259 | use screeps_rs_network::Url; 260 | let server = if state.server.len() == 0 { 261 | ::screeps_api::DEFAULT_OFFICIAL_API_URL 262 | .parse() 263 | .expect("expected default URL to parse") 264 | } else { 265 | let result = if state.server.starts_with("http") || state.server.starts_with("https") { 266 | state.server.parse() 267 | } else { 268 | format!("http://{}", state.server).parse() 269 | }.map(|url: Url| { 270 | url.join("api/") 271 | .expect("expected hardcoded URL segment to parse") 272 | }); 273 | 274 | match result { 275 | Ok(url) => url, 276 | Err(e) => { 277 | warn!("server URL invalid: {}", e); 278 | return; 279 | } 280 | } 281 | }; 282 | // TODO: UI option for shard. 283 | // let settings = ConnectionSettings::new( 284 | // state.username.clone(), 285 | // state.password.clone(), 286 | // "shard0".to_owned(), 287 | // ); 288 | let settings = ConnectionSettings::with_url( 289 | server, 290 | state.username.clone(), 291 | state.password.clone(), 292 | if state.shard.len() == 0 { 293 | None 294 | } else { 295 | Some(state.shard.clone()) 296 | }, 297 | ); 298 | 299 | debug!("sending login request to existing network."); 300 | 301 | app.net_cache.update_settings(settings); 302 | app.net_cache.login(); 303 | update.push_front(UiEvent::LoginSubmitted(time::now_utc())); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /ui/src/rust/layout/mod.rs: -------------------------------------------------------------------------------- 1 | mod login_screen; 2 | mod room_view; 3 | mod left_panel; 4 | 5 | use std::collections::VecDeque; 6 | 7 | use conrod::{self, color, Borderable, Colorable, Widget}; 8 | use conrod::widget::*; 9 | use conrod::widget::id; 10 | 11 | use app::AppCell; 12 | use rendering::AdditionalRender; 13 | use ui_state::{ScreenState, State}; 14 | 15 | const HEADER_HEIGHT: conrod::Scalar = 30.0; 16 | 17 | pub const BACKGROUND_RGB: [f32; 3] = [0.0625, 0.46875, 0.3125]; 18 | pub const BACKGROUND: conrod::Color = conrod::Color::Rgba(BACKGROUND_RGB[0], BACKGROUND_RGB[1], BACKGROUND_RGB[2], 1.0); 19 | 20 | pub fn create_ui(app: &mut AppCell, state: &mut State) { 21 | let mut update = VecDeque::new(); 22 | 23 | match state.screen_state { 24 | ScreenState::Login(ref login_state) => { 25 | login_screen::create_ui(app, login_state, &mut update); 26 | } 27 | ScreenState::Map(ref map_state) => { 28 | room_view::create_ui(app, map_state, &mut update); 29 | } 30 | ScreenState::Exit => {} 31 | } 32 | 33 | state.transform(update.drain(..)); 34 | } 35 | 36 | fn frame(ui: &mut conrod::UiCell, ids: &Ids, body_id: Id, body: Canvas) { 37 | let header = Canvas::new() 38 | .color(color::DARK_CHARCOAL) 39 | .border(0.0) 40 | .length(HEADER_HEIGHT); 41 | 42 | Canvas::new() 43 | .color(BACKGROUND) 44 | .border(0.0) 45 | .flow_down(&[(ids.root.header, header), (body_id, body)]) 46 | .set(ids.root.root, ui); 47 | } 48 | 49 | pub struct RootIds { 50 | root: Id, 51 | header: Id, 52 | body: Id, 53 | } 54 | impl RootIds { 55 | pub fn new(gen: &mut id::Generator) -> Self { 56 | RootIds { 57 | root: gen.next(), 58 | header: gen.next(), 59 | body: gen.next(), 60 | } 61 | } 62 | } 63 | 64 | pub struct Ids { 65 | root: RootIds, 66 | left_panel: left_panel::LeftPanelIds, 67 | login: login_screen::LoginIds, 68 | room_view: room_view::RoomViewIds, 69 | } 70 | 71 | impl Ids { 72 | pub fn new(gen: &mut id::Generator) -> Self { 73 | Ids { 74 | root: RootIds::new(gen), 75 | left_panel: left_panel::LeftPanelIds::new(gen), 76 | login: login_screen::LoginIds::new(gen), 77 | room_view: room_view::RoomViewIds::new(gen), 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ui/src/rust/layout/room_view.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use conrod::{color, Borderable, Colorable, Positionable, Rect, Sizeable, Widget}; 4 | use conrod::widget::*; 5 | 6 | use screeps_api; 7 | 8 | use screeps_rs_network::SelectedRooms; 9 | use ui_state::{self, Event as UiEvent, MapClickEvent, MapPanEvent, MapScreenState, MapZoomEvent, ScrollState}; 10 | use rendering::MapViewOffset; 11 | 12 | use app::AppCell; 13 | use super::{frame, AdditionalRender}; 14 | use super::left_panel::left_panel_available; 15 | use self::room_view_widget::ScrollableRoomView; 16 | use map_view_utils::zoom_multiplier_from_factor; 17 | 18 | pub struct RoomViewIds { 19 | username_gcl_header: Id, 20 | display: Id, 21 | scroll_widget: Id, 22 | shard_dropdown: Id, 23 | } 24 | 25 | impl RoomViewIds { 26 | pub fn new(gen: &mut id::Generator) -> Self { 27 | RoomViewIds { 28 | username_gcl_header: gen.next(), 29 | display: gen.next(), 30 | scroll_widget: gen.next(), 31 | shard_dropdown: gen.next(), 32 | } 33 | } 34 | } 35 | 36 | pub fn create_ui(app: &mut AppCell, state: &MapScreenState, mut update: &mut VecDeque) { 37 | let AppCell { 38 | ref mut ui, 39 | ref mut net_cache, 40 | ref mut ids, 41 | .. 42 | } = *app; 43 | 44 | let body = Canvas::new().color(color::TRANSPARENT).border(0.0); 45 | 46 | frame(ui, ids, ids.root.body, body); 47 | 48 | left_panel_available(ui, ids, &state.panels, update); 49 | 50 | // scrolling 51 | let scroll_result = ScrollableRoomView::new() 52 | .wh(ui.wh_of(ids.root.body).unwrap()) 53 | .middle_of(ids.root.body) 54 | .set(ids.room_view.scroll_widget, ui); 55 | 56 | // display rect 57 | Rectangle::fill(ui.wh_of(ids.root.body).unwrap()) 58 | .color(color::TRANSPARENT) 59 | .middle_of(ids.root.body) 60 | .graphics_for(ids.room_view.scroll_widget) 61 | .set(ids.room_view.display, ui); 62 | 63 | if state.panels.left == ui_state::MenuState::Open { 64 | let shard_list = net_cache.shard_list(); 65 | match shard_list { 66 | Some(Some(shards)) => { 67 | let mut text = String::new(); 68 | for shard_info in shards { 69 | use std::fmt::Write; 70 | write!(text, "{}\n", shard_info.as_ref()).expect("writing plain string to plain string"); 71 | } 72 | Text::new(&text) 73 | .font_size(ui.theme.font_size_medium) 74 | .right_justify() 75 | .no_line_wrap() 76 | .top_left_of(ids.left_panel.open_panel_canvas) 77 | .set(ids.room_view.shard_dropdown, ui); 78 | } 79 | Some(None) => { 80 | Text::new("") 81 | .font_size(ui.theme.font_size_medium) 82 | .right_justify() 83 | .no_line_wrap() 84 | .top_left_of(ids.left_panel.open_panel_canvas) 85 | .set(ids.room_view.shard_dropdown, ui); 86 | } 87 | None => {} 88 | } 89 | } 90 | 91 | if let Some(info) = net_cache.my_info() { 92 | Text::new(&format!("{} - GCL {}", info.username, screeps_api::gcl_calc(info.gcl_points))) 93 | // style 94 | .font_size(ui.theme.font_size_small) 95 | .right_justify() 96 | .no_line_wrap() 97 | // position 98 | .mid_right_with_margin_on(ids.root.header, 10.0) 99 | .set(ids.room_view.username_gcl_header, ui); 100 | } 101 | 102 | let view_rect = ui.rect_of(ids.room_view.display) 103 | .expect("expected room_display to have a rect"); 104 | 105 | scroll_result.map(|scroll_update| scroll_update.into_events(view_rect, &mut update)); 106 | 107 | let ScrollState { 108 | scroll_x: saved_room_scroll_x, 109 | scroll_y: saved_room_scroll_y, 110 | zoom_factor, 111 | selected_room, 112 | .. 113 | } = state.map_scroll; 114 | 115 | let room_size = view_rect.w().min(view_rect.h()) * zoom_multiplier_from_factor(zoom_factor); 116 | 117 | let room_scroll_x = saved_room_scroll_x - (view_rect.w() / room_size / 2.0); 118 | let room_scroll_y = saved_room_scroll_y - (view_rect.h() / room_size / 2.0); 119 | 120 | let initial_room = screeps_api::RoomName { 121 | x_coord: room_scroll_x.floor() as i32, 122 | y_coord: room_scroll_y.floor() as i32, 123 | }; 124 | 125 | let extra_scroll_x = -(room_scroll_x % 1.0) * room_size; 126 | let extra_scroll_y = -(room_scroll_y % 1.0) * room_size; 127 | let extra_scroll_x = if extra_scroll_x > 0.0 { 128 | extra_scroll_x - room_size 129 | } else { 130 | extra_scroll_x 131 | }; 132 | let extra_scroll_y = if extra_scroll_y > 0.0 { 133 | extra_scroll_y - room_size 134 | } else { 135 | extra_scroll_y 136 | }; 137 | let count_x = ((view_rect.w() - extra_scroll_x) / room_size).ceil() as i32; 138 | let count_y = ((view_rect.h() - extra_scroll_y) / room_size).ceil() as i32; 139 | debug!( 140 | "scroll_state: ({:?}) initial room: {}. extra scroll: ({}, {}). count: ({}, {})", 141 | state.map_scroll, initial_room, extra_scroll_x, extra_scroll_y, count_x, count_y 142 | ); 143 | 144 | // fetch rooms just outside the boundary as well so we can have smoother scrolling 145 | let rooms_to_fetch = SelectedRooms::new((initial_room - (1, 1))..(initial_room + (count_x + 1, count_y + 1))); 146 | 147 | let room_data = net_cache.view_rooms(rooms_to_fetch, selected_room).clone(); 148 | 149 | let rooms_to_view = SelectedRooms::new(initial_room..(initial_room + (count_x, count_y))); 150 | let offset = MapViewOffset::new(extra_scroll_x, extra_scroll_y, room_size); 151 | 152 | *app.additional_rendering = Some(AdditionalRender::map_view( 153 | ids.root.body, 154 | rooms_to_view, 155 | room_data, 156 | offset, 157 | )); 158 | } 159 | 160 | #[derive(Copy, Clone, Debug, Default)] 161 | struct ScrollUpdate { 162 | /// The number of pixels scrolled horizontally. 163 | scrolled_map_x: f64, 164 | /// The number of pixels scrolled vertically. 165 | scrolled_map_y: f64, 166 | /// The scroll change amount (unknown unit). 167 | zoom_change: f64, 168 | /// If zoom_change != 0.0, this is the x position the mouse was at, relative to the center of the widget. 169 | zoom_mouse_rel_x: f64, 170 | /// If zoom_change != 0.0, this is the y position the mouse was at, relative to the center of the widget. 171 | zoom_mouse_rel_y: f64, 172 | /// If the screen was clicked, the relative (x, y) that it was clicked at. 173 | clicked: Option<(f64, f64)>, 174 | } 175 | 176 | impl ScrollUpdate { 177 | fn into_events(&self, view_rect: Rect, update: &mut VecDeque) { 178 | if let Some(pos_tuple) = self.clicked { 179 | update.push_front(UiEvent::MapClick { 180 | view_rect: view_rect, 181 | event: MapClickEvent { clicked: pos_tuple }, 182 | }); 183 | } 184 | if self.zoom_change != 0.0 { 185 | update.push_front(UiEvent::MapZoom { 186 | view_rect: view_rect, 187 | event: MapZoomEvent { 188 | zoom_change: self.zoom_change, 189 | zoom_mouse_rel_x: self.zoom_mouse_rel_x, 190 | zoom_mouse_rel_y: self.zoom_mouse_rel_y, 191 | }, 192 | }); 193 | } 194 | if self.scrolled_map_x != 0.0 || self.scrolled_map_y != 0.0 { 195 | update.push_front(UiEvent::MapPan { 196 | view_rect: view_rect, 197 | event: MapPanEvent { 198 | scrolled_map_x: self.scrolled_map_x, 199 | scrolled_map_y: self.scrolled_map_y, 200 | }, 201 | }); 202 | } 203 | } 204 | } 205 | 206 | mod room_view_widget { 207 | use super::ScrollUpdate; 208 | use conrod::{widget, Widget}; 209 | 210 | #[derive(WidgetCommon)] 211 | pub(super) struct ScrollableRoomView { 212 | #[conrod(common_builder)] 213 | common: widget::CommonBuilder, 214 | style: Style, 215 | } 216 | 217 | #[derive(Copy, Clone, Debug, Default, PartialEq)] 218 | pub(super) struct Style {} 219 | 220 | pub(super) struct State {} 221 | 222 | impl ScrollableRoomView { 223 | pub(super) fn new() -> Self { 224 | ScrollableRoomView { 225 | common: widget::CommonBuilder::default(), 226 | style: Style::default(), 227 | } 228 | } 229 | } 230 | impl Widget for ScrollableRoomView { 231 | type State = State; 232 | type Style = Style; 233 | type Event = Option; 234 | 235 | fn init_state(&self, _: widget::id::Generator) -> State { 236 | State {} 237 | } 238 | 239 | fn style(&self) -> Style { 240 | self.style.clone() 241 | } 242 | 243 | /// Updates this widget. Returns an event of [scroll_x; scroll_y] 244 | fn update(self, args: widget::UpdateArgs) -> Option { 245 | use conrod::event::Widget as Event; 246 | use conrod::input::MouseButton; 247 | 248 | let widget::UpdateArgs { id, ui, state, .. } = args; 249 | 250 | let input = ui.widget_input(id); 251 | 252 | let mut changed = false; 253 | let mut update = ScrollUpdate::default(); 254 | 255 | for event in input.events() { 256 | match event { 257 | Event::Drag(drag) => if drag.button == MouseButton::Left { 258 | debug!("drag update"); 259 | update.scrolled_map_x -= drag.delta_xy[0]; 260 | update.scrolled_map_y -= drag.delta_xy[1]; 261 | changed = true; 262 | }, 263 | Event::Scroll(scroll) => if scroll.modifiers.is_empty() { 264 | debug!("scroll update"); 265 | update.zoom_change -= scroll.y; 266 | changed = true; 267 | }, 268 | Event::Click(click) => if click.button == MouseButton::Left { 269 | debug!("click update"); 270 | update.clicked = Some((click.xy[0], click.xy[1])); 271 | }, 272 | _ => {} 273 | } 274 | } 275 | if update.zoom_change != 0.0 { 276 | let mouse = input 277 | .mouse() 278 | .expect("expected mouse to be captured by widget while scroll event is received"); 279 | let rel_xy = mouse.rel_xy(); 280 | update.zoom_mouse_rel_x = rel_xy[0]; 281 | update.zoom_mouse_rel_y = rel_xy[1]; 282 | } 283 | 284 | if changed { 285 | // let the UI know the state has changed. 286 | state.update(|_| {}); 287 | Some(update) 288 | } else { 289 | None 290 | } 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /ui/src/rust/lib.rs: -------------------------------------------------------------------------------- 1 | // impl Trait 2 | #![feature(conservative_impl_trait)] 3 | // generators (useful for rendering using conrod) 4 | #![feature(generators, generator_trait)] 5 | 6 | // Graphics 7 | #[macro_use] 8 | extern crate conrod; 9 | #[macro_use] 10 | extern crate conrod_derive; 11 | #[macro_use] 12 | extern crate glium; 13 | extern crate glutin; 14 | extern crate rusttype; 15 | 16 | // Network 17 | extern crate screeps_api; 18 | extern crate screeps_rs_network; 19 | 20 | // Caching 21 | extern crate time; 22 | 23 | // Logging 24 | extern crate chrono; 25 | extern crate fern; 26 | #[macro_use] 27 | extern crate log; 28 | 29 | pub mod app; 30 | pub mod layout; 31 | pub mod ui_state; 32 | pub mod rendering; 33 | pub mod network_integration; 34 | pub mod window_management; 35 | pub mod widgets; 36 | mod map_view_utils; 37 | mod glium_backend; 38 | 39 | pub use app::App; 40 | pub use network_integration::NetworkHandler; 41 | 42 | pub fn main(verbose_logging: bool, debug_modules: I) 43 | where 44 | T: AsRef, 45 | I: IntoIterator, 46 | { 47 | window_management::setup::init_logger(verbose_logging, debug_modules); 48 | 49 | let (events_loop, app) = window_management::setup::init_window(); 50 | 51 | window_management::window_loop::main_window_loop(events_loop, app); 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/rust/map_view_utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub const ZOOM_MODIFIER: f64 = 1.0 / 500.0; 2 | pub const MIN_ZOOM: f64 = 0.05; 3 | pub const MAX_ZOOM: f64 = 10.0; 4 | 5 | #[inline(always)] 6 | pub fn zoom_multiplier_from_factor(zoom_factor: f64) -> f64 { 7 | zoom_factor.powf(2.0) 8 | } 9 | 10 | #[inline(always)] 11 | pub fn bound_zoom(zoom_factor: f64) -> f64 { 12 | zoom_factor.powf(2.0).min(MAX_ZOOM).max(MIN_ZOOM).powf(0.5) 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/rust/network_integration.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use {glutin, screeps_rs_network}; 4 | 5 | #[derive(Clone)] 6 | pub struct GlutinNotify(Arc); 7 | 8 | impl screeps_rs_network::Notify for GlutinNotify { 9 | fn wakeup(&self) -> Result<(), screeps_rs_network::Disconnected> { 10 | self.0 11 | .wakeup() 12 | .map_err(|glutin::EventsLoopClosed| screeps_rs_network::Disconnected) 13 | } 14 | } 15 | 16 | impl From> for GlutinNotify { 17 | fn from(arc: Arc) -> Self { 18 | GlutinNotify(arc) 19 | } 20 | } 21 | 22 | impl From for GlutinNotify { 23 | fn from(notify: glutin::EventsLoopProxy) -> Self { 24 | GlutinNotify(Arc::new(notify)) 25 | } 26 | } 27 | 28 | pub type NetworkHandler = screeps_rs_network::TokioHandler; 29 | pub type NetworkCache<'a> = screeps_rs_network::memcache::NetworkedMemCache<'a, NetworkHandler>; 30 | -------------------------------------------------------------------------------- /ui/src/rust/rendering/constants.rs: -------------------------------------------------------------------------------- 1 | use conrod::Color; 2 | 3 | pub const WALL_COLOR: Color = Color::Rgba(0.07, 0.07, 0.07, 1.0); 4 | pub const SWAMP_COLOR: Color = Color::Rgba(0.16, 0.17, 0.09, 1.0); 5 | pub const PLAINS_COLOR: Color = Color::Rgba(0.17, 0.17, 0.17, 1.0); 6 | pub const ROAD_COLOR: Color = Color::Rgba(0.23, 0.23, 0.23, 1.0); 7 | pub const POWER_COLOR: Color = Color::Rgba(0.2, 0.0667, 0.0667, 1.0); 8 | pub const PORTAL_COLOR: Color = Color::Rgba(0.0, 0.7843, 1.0, 1.0); 9 | pub const SOURCE_COLOR: Color = Color::Rgba(1.0, 0.949, 0.274, 1.0); 10 | pub const MINERAL_COLOR: Color = Color::Rgba(0.2157, 0.0745, 0.5137, 1.0); 11 | pub const CONTROLLER_COLOR: Color = Color::Rgba(0.80392, 0.80392, 0.80392, 1.0); 12 | pub const KEEPER_COLOR: Color = Color::Rgba(0.3647, 0.2980, 0.1804, 1.0); 13 | pub const USER_COLOR: Color = Color::Rgba(0.1372, 0.3804, 0.2667, 1.0); 14 | -------------------------------------------------------------------------------- /ui/src/rust/rendering/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! yield_from { 2 | ($g:expr) => ({ 3 | let mut gen = $g; 4 | loop { 5 | let state = gen.resume(); 6 | 7 | match state { 8 | GeneratorState::Yielded(v) => yield v, 9 | GeneratorState::Complete(r) => break r, 10 | } 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/rust/rendering/map_view.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Ref; 2 | use std::collections::HashMap; 3 | use std::ops::{Generator, GeneratorState}; 4 | 5 | use conrod::{self, widget, Rect}; 6 | use conrod::render::{Primitive, PrimitiveKind}; 7 | use conrod::image::Id as ImageId; 8 | use screeps_api::RoomName; 9 | use screeps_api::endpoints::room_terrain::{TerrainGrid, TerrainType}; 10 | use screeps_api::websocket::RoomMapViewUpdate; 11 | use screeps_api::websocket::types::room::objects::KnownRoomObject; 12 | 13 | use screeps_rs_network::{MapCacheData, SelectedRooms}; 14 | 15 | use super::constants::*; 16 | use super::types::{IterAdapter, MapViewOffset}; 17 | use super::render_cache::RenderCache; 18 | 19 | #[derive(Copy, Clone)] 20 | struct RenderData<'a> { 21 | id: widget::Id, 22 | scizzor: Rect, 23 | offset: MapViewOffset, 24 | image_cache: &'a RenderCache, 25 | start_room_screen_pos: (f64, f64), 26 | } 27 | 28 | pub fn render<'a>( 29 | id: widget::Id, 30 | view_rect: Rect, 31 | scizzor: Rect, 32 | image_cache: &'a RenderCache, 33 | (selected, data, offset): (SelectedRooms, Ref<'a, MapCacheData>, MapViewOffset), 34 | ) -> impl Iterator> + 'a { 35 | let render_data = RenderData { 36 | id, 37 | scizzor, 38 | offset, 39 | image_cache: image_cache, 40 | start_room_screen_pos: ( 41 | view_rect.x.start + offset.x_offset, 42 | view_rect.y.start + offset.y_offset, 43 | ), 44 | }; 45 | 46 | let gen = move || { 47 | let start_room_name = selected.start; 48 | let horizontal_room_count = selected.end.x_coord - selected.start.x_coord; 49 | let vertical_room_count = selected.end.y_coord - selected.start.y_coord; 50 | 51 | // terrain 52 | for relative_room_x in 0..horizontal_room_count { 53 | for relative_room_y in 0..vertical_room_count { 54 | let current_name = start_room_name + (relative_room_x, relative_room_y); 55 | 56 | if let Some(image_id) = render_data.image_cache.get_terrain(current_name) { 57 | yield_from!(render_room_image( 58 | render_data, 59 | relative_room_x, 60 | relative_room_y, 61 | image_id, 62 | )); 63 | } 64 | 65 | // // if we can render this room 66 | // if data.terrain.contains_key(¤t_name) { 67 | // // use Ref trick to get an "owned" reference to the specific sub-bit of the data. 68 | // let terrain = Ref::map(Ref::clone(&data), |data| { 69 | // &data.terrain.get(¤t_name).unwrap().1 70 | // }); 71 | // yield_from!(render_terrain_of( 72 | // render_data, 73 | // relative_room_x, 74 | // relative_room_y, 75 | // terrain, 76 | // )); 77 | // } 78 | } 79 | } 80 | 81 | // map view 82 | for relative_room_x in 0..horizontal_room_count { 83 | for relative_room_y in 0..vertical_room_count { 84 | let current_name = start_room_name + (relative_room_x, relative_room_y); 85 | 86 | // if we can render this room 87 | if data.map_views.contains_key(¤t_name) { 88 | let map_data = Ref::map(Ref::clone(&data), |data| { 89 | &data.map_views.get(¤t_name).unwrap().1 90 | }); 91 | yield_from!(render_map_view_of( 92 | render_data, 93 | relative_room_x, 94 | relative_room_y, 95 | map_data, 96 | )); 97 | } 98 | } 99 | } 100 | 101 | // room view 102 | let opt_viewed: Option = data.detail_view.as_ref().map(|&(name, _)| name); 103 | if let Some(viewed_room) = opt_viewed { 104 | let (x_diff, y_diff) = viewed_room - start_room_name; 105 | if x_diff >= 0 && x_diff <= horizontal_room_count && y_diff >= 0 && y_diff <= vertical_room_count { 106 | let room_objects = Ref::map(Ref::clone(&data), |data| { 107 | &data.detail_view.as_ref().unwrap().1 108 | }); 109 | 110 | yield_from!(render_room(render_data, x_diff, y_diff, room_objects)); 111 | } 112 | } 113 | }; 114 | 115 | IterAdapter(gen).fuse() // fuse required for generator safety 116 | } 117 | 118 | fn terrain_color(terrain_type: TerrainType) -> conrod::Color { 119 | match terrain_type { 120 | TerrainType::Plains => PLAINS_COLOR, 121 | TerrainType::Swamp => SWAMP_COLOR, 122 | TerrainType::SwampyWall | TerrainType::Wall => WALL_COLOR, 123 | } 124 | } 125 | 126 | use glium::texture::Texture2dDataSource; 127 | 128 | pub fn make_terrain_texture(terrain: &TerrainGrid) -> impl Texture2dDataSource<'static> { 129 | terrain 130 | .iter() 131 | .map(|row| { 132 | row.iter() 133 | .map(|&terrain_type| { 134 | let rgb = terrain_color(terrain_type).to_rgb(); 135 | (rgb.0, rgb.1, rgb.2, rgb.3) 136 | }) 137 | .collect::>() 138 | }) 139 | .collect::>() 140 | } 141 | 142 | #[allow(dead_code)] 143 | fn render_terrain_of<'a>( 144 | data: RenderData<'a>, 145 | current_relative_room_x: i32, 146 | current_relative_room_y: i32, 147 | terrain: Ref<'a, TerrainGrid>, 148 | ) -> impl Generator, Return = ()> + 'a { 149 | move || { 150 | for current_terrain_x in 0..50 { 151 | for current_terrain_y in 0..50 { 152 | let terrain_type = terrain[current_terrain_y][current_terrain_x]; 153 | 154 | let terrain_square_length = data.offset.room_size / 50.0; 155 | 156 | let x_pos = data.start_room_screen_pos.0 157 | + data.offset.room_size * (current_relative_room_x as f64 + ((current_terrain_x as f64) / 50.0)); 158 | let y_pos = data.start_room_screen_pos.1 159 | + data.offset.room_size 160 | * (current_relative_room_y as f64 + (((50 - current_terrain_y) as f64) / 50.0)); 161 | 162 | yield Primitive { 163 | id: data.id, 164 | kind: PrimitiveKind::Rectangle { 165 | color: terrain_color(terrain_type), 166 | }, 167 | scizzor: data.scizzor, 168 | rect: Rect::from_corners( 169 | [x_pos, y_pos - terrain_square_length], 170 | [x_pos + terrain_square_length, y_pos], 171 | ), 172 | }; 173 | } 174 | } 175 | } 176 | } 177 | 178 | fn render_room_image<'a>( 179 | data: RenderData<'a>, 180 | current_relative_room_x: i32, 181 | current_relative_room_y: i32, 182 | image_id: ImageId, 183 | ) -> impl Generator, Return = ()> + 'a { 184 | move || { 185 | let x_pos = data.start_room_screen_pos.0 + data.offset.room_size * (current_relative_room_x as f64); 186 | let y_pos = data.start_room_screen_pos.1 + data.offset.room_size * (current_relative_room_y as f64); 187 | let end_x = x_pos + data.offset.room_size; 188 | let end_y = y_pos + data.offset.room_size; 189 | 190 | yield Primitive { 191 | id: data.id, 192 | kind: PrimitiveKind::Image { 193 | image_id, 194 | color: None, 195 | source_rect: None, 196 | }, 197 | scizzor: data.scizzor, 198 | rect: Rect::from_corners([x_pos, y_pos], [end_x, end_y]), 199 | } 200 | } 201 | } 202 | 203 | fn render_map_view_of<'a>( 204 | data: RenderData<'a>, 205 | current_relative_room_x: i32, 206 | current_relative_room_y: i32, 207 | map_view: Ref<'a, RoomMapViewUpdate>, 208 | ) -> impl Generator, Return = ()> + 'a { 209 | move || { 210 | let room_screen_size = data.offset.room_size; 211 | let start_room_screen_pos = data.start_room_screen_pos; 212 | let render_id = data.id; 213 | let render_scizzor = data.scizzor; 214 | let terrain_square_length = room_screen_size / 50.0; 215 | 216 | macro_rules! draw_square_at { 217 | ($x:expr, $y:expr, $color:expr) => ({ 218 | let x_pos = start_room_screen_pos.0 219 | + room_screen_size * (current_relative_room_x as f64 + ((($x as f64) - 1.0) / 50.0)) 220 | + terrain_square_length; 221 | let y_pos = start_room_screen_pos.1 222 | + room_screen_size * (current_relative_room_y as f64 + ((49.0 - ($y as f64)) / 50.0)) 223 | + terrain_square_length; 224 | 225 | let visual_length = terrain_square_length; 226 | 227 | Primitive { 228 | id: render_id, 229 | kind: PrimitiveKind::Rectangle { color: $color }, 230 | scizzor: render_scizzor, 231 | rect: Rect::from_corners( 232 | [x_pos, y_pos - visual_length], 233 | [x_pos + visual_length, y_pos], 234 | ), 235 | } 236 | 237 | }) 238 | } 239 | 240 | let num_roads = map_view.roads.len(); 241 | for idx in 0..num_roads { 242 | let (x, y) = map_view.roads[idx]; 243 | yield draw_square_at!(x, y, ROAD_COLOR); 244 | } 245 | 246 | let num_power = map_view.power_or_power_bank.len(); 247 | for idx in 0..num_power { 248 | let (x, y) = map_view.power_or_power_bank[idx]; 249 | yield draw_square_at!(x, y, POWER_COLOR); 250 | } 251 | 252 | let num_walls = map_view.walls.len(); 253 | for idx in 0..num_walls { 254 | let (x, y) = map_view.walls[idx]; 255 | yield draw_square_at!(x, y, WALL_COLOR); 256 | } 257 | 258 | let num_portals = map_view.portals.len(); 259 | for idx in 0..num_portals { 260 | let (x, y) = map_view.portals[idx]; 261 | yield draw_square_at!(x, y, PORTAL_COLOR); 262 | } 263 | 264 | let num_sources = map_view.sources.len(); 265 | for idx in 0..num_sources { 266 | let (x, y) = map_view.sources[idx]; 267 | yield draw_square_at!(x, y, SOURCE_COLOR); 268 | } 269 | 270 | let num_minerals = map_view.minerals.len(); 271 | for idx in 0..num_minerals { 272 | let (x, y) = map_view.minerals[idx]; 273 | yield draw_square_at!(x, y, MINERAL_COLOR); 274 | } 275 | 276 | let num_controllers = map_view.controllers.len(); 277 | for idx in 0..num_controllers { 278 | let (x, y) = map_view.controllers[idx]; 279 | yield draw_square_at!(x, y, CONTROLLER_COLOR); 280 | } 281 | 282 | let num_keepers = map_view.keeper_lairs.len(); 283 | for idx in 0..num_keepers { 284 | let (x, y) = map_view.keeper_lairs[idx]; 285 | yield draw_square_at!(x, y, KEEPER_COLOR); 286 | } 287 | 288 | let num_users = map_view.users_objects.len(); 289 | for idx in 0..num_users { 290 | let num_user_objects = map_view.users_objects[idx].1.len(); 291 | for jdx in 0..num_user_objects { 292 | let (x, y) = map_view.users_objects[idx].1[jdx]; 293 | yield draw_square_at!(x, y, USER_COLOR); 294 | } 295 | } 296 | } 297 | } 298 | 299 | fn render_room<'a>( 300 | data: RenderData<'a>, 301 | current_relative_room_x: i32, 302 | current_relative_room_y: i32, 303 | _room_objects: Ref<'a, HashMap>, 304 | ) -> impl Generator, Return = ()> + 'a { 305 | move || { 306 | let x_pos = data.start_room_screen_pos.0 + data.offset.room_size * (current_relative_room_x as f64); 307 | let y_pos = data.start_room_screen_pos.1 + data.offset.room_size * (current_relative_room_y as f64); 308 | 309 | yield Primitive { 310 | id: data.id, 311 | kind: PrimitiveKind::Rectangle { 312 | color: KEEPER_COLOR, 313 | }, 314 | scizzor: data.scizzor, 315 | rect: Rect::from_corners( 316 | [x_pos, y_pos - data.offset.room_size], 317 | [x_pos + data.offset.room_size, y_pos], 318 | ), 319 | }; 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /ui/src/rust/rendering/mod.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | use std::cell::Ref; 3 | 4 | use glium; 5 | use conrod::{self, widget, Rect}; 6 | use conrod::render::{Primitive, PrimitiveWalker}; 7 | 8 | use screeps_rs_network::{MapCache, MapCacheData, SelectedRooms}; 9 | 10 | #[macro_use] 11 | mod macros; 12 | pub mod constants; 13 | mod map_view; 14 | mod types; 15 | pub mod render_cache; 16 | 17 | pub use self::types::MapViewOffset; 18 | pub use self::render_cache::RenderCache; 19 | 20 | #[derive(Clone, Debug)] 21 | pub enum AdditionalRenderType { 22 | MapView((SelectedRooms, MapCache, MapViewOffset)), 23 | } 24 | 25 | enum BorrowedRenderType<'a> { 26 | MapView((SelectedRooms, Ref<'a, MapCacheData>, MapViewOffset)), 27 | } 28 | 29 | impl<'a> Clone for BorrowedRenderType<'a> { 30 | fn clone(&self) -> Self { 31 | match *self { 32 | BorrowedRenderType::MapView((rooms, ref data, offset)) => { 33 | BorrowedRenderType::MapView((rooms, Ref::clone(data), offset)) 34 | } 35 | } 36 | } 37 | } 38 | 39 | #[derive(Clone, Debug)] 40 | pub struct AdditionalRender { 41 | pub replace: widget::Id, 42 | pub draw_type: AdditionalRenderType, 43 | _phantom: PhantomData<()>, 44 | } 45 | 46 | pub struct ReadyRender { 47 | view_rect: Rect, 48 | scizzor: Rect, 49 | inner: AdditionalRender, 50 | } 51 | 52 | #[derive(Clone)] 53 | struct BorrowedRender<'a> { 54 | replace: widget::Id, 55 | draw_type: BorrowedRenderType<'a>, 56 | view_rect: Rect, 57 | scizzor: Rect, 58 | } 59 | 60 | impl<'a> BorrowedRender<'a> { 61 | #[inline] 62 | pub fn into_primitives(self, image_cache: &'a RenderCache) -> impl Iterator> + 'a { 63 | let parent_rect = self.view_rect; 64 | let parent_scizzor = self.scizzor; 65 | 66 | debug!( 67 | "into_primitives: {{parent_rect: {:?}, parent_scizzor: {:?}}}", 68 | parent_rect, parent_scizzor 69 | ); 70 | 71 | let BorrowedRender { 72 | replace, draw_type, .. 73 | } = self; 74 | 75 | let scizzor = parent_scizzor 76 | .overlap(parent_rect) 77 | .unwrap_or(parent_scizzor); 78 | 79 | match draw_type { 80 | BorrowedRenderType::MapView(parameters) => { 81 | map_view::render(replace, parent_rect, scizzor, image_cache, parameters) 82 | } 83 | } 84 | } 85 | } 86 | 87 | pub trait RenderPipelineFinish { 88 | fn render_with(self, T); 89 | } 90 | 91 | impl AdditionalRender { 92 | #[inline(always)] 93 | pub fn map_view(replace: widget::Id, rooms: SelectedRooms, cache: MapCache, offset: MapViewOffset) -> Self { 94 | AdditionalRender { 95 | replace: replace, 96 | draw_type: AdditionalRenderType::MapView((rooms, cache, offset)), 97 | _phantom: PhantomData, 98 | } 99 | } 100 | 101 | pub fn ready(self, ui: &conrod::Ui) -> Option { 102 | Some(ReadyRender { 103 | view_rect: ui.rect_of(self.replace)?, 104 | scizzor: ui.visible_area(self.replace)?, 105 | inner: self, 106 | }) 107 | } 108 | } 109 | 110 | impl ReadyRender { 111 | pub fn prepare_images(&self, display: &glium::Display, image_cache: &mut RenderCache) { 112 | match self.inner.draw_type { 113 | AdditionalRenderType::MapView((rooms, ref cache, _)) => { 114 | let cache = cache.borrow(); 115 | for room_name in rooms { 116 | if let Some(&(_, Some(ref terrain))) = cache.terrain.get(&room_name) { 117 | image_cache.get_or_generate_terrain(display, room_name, terrain); 118 | } 119 | } 120 | image_cache.invalidation_check(rooms.start - (10, 10), rooms.end + (10, 10)) 121 | } 122 | } 123 | } 124 | 125 | #[inline] 126 | pub fn render_with(&self, walker: T, image_cache: &RenderCache, render_with: F) 127 | where 128 | T: PrimitiveWalker, 129 | F: RenderPipelineFinish, 130 | { 131 | let replace = self.inner.replace; 132 | 133 | let custom_gen = BorrowedRender { 134 | replace: self.inner.replace, 135 | draw_type: match self.inner.draw_type { 136 | AdditionalRenderType::MapView((rooms, ref cache, offset)) => { 137 | BorrowedRenderType::MapView((rooms, cache.borrow(), offset)) 138 | } 139 | }, 140 | view_rect: self.view_rect, 141 | scizzor: self.scizzor, 142 | }.into_primitives(image_cache); 143 | 144 | // WalkerAdapter(gen, PhantomData) 145 | render_with.render_with(MergedPrimitives { 146 | replace: replace, 147 | custom: Some(custom_gen), 148 | currently_replacing: None, 149 | walker: walker, 150 | }) 151 | } 152 | } 153 | 154 | pub struct MergedPrimitives { 155 | replace: widget::Id, 156 | custom: Option, 157 | currently_replacing: Option, 158 | walker: T, 159 | } 160 | 161 | impl PrimitiveWalker for MergedPrimitives 162 | where 163 | T: PrimitiveWalker, 164 | U: Iterator>, 165 | { 166 | #[inline(always)] 167 | fn next_primitive(&mut self) -> Option { 168 | if let Some(ref mut iter) = self.currently_replacing { 169 | if let Some(p) = iter.next() { 170 | return Some(p); 171 | } 172 | } 173 | if self.currently_replacing.is_some() { 174 | self.currently_replacing = None; 175 | } 176 | 177 | match self.walker.next_primitive() { 178 | Some(p) => if p.id == self.replace { 179 | self.currently_replacing = self.custom.take(); 180 | match self.currently_replacing.as_mut().and_then(|c| c.next()) { 181 | Some(first_replace) => Some(first_replace), 182 | None => Some(p), 183 | } 184 | } else { 185 | Some(p) 186 | }, 187 | None => None, 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /ui/src/rust/rendering/render_cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::{Entry, HashMap}; 2 | 3 | use screeps_api::{RoomName, TerrainGrid}; 4 | use screeps_rs_network::NetworkEvent; 5 | use conrod::image::{Id as ImageId, Map as ImageMap}; 6 | use glium; 7 | 8 | use super::map_view; 9 | 10 | // pub type Texture = glium::texture::CompressedSrgbTexture2d; 11 | pub type Texture = glium::texture::SrgbTexture2d; 12 | 13 | pub const INVALIDATE_EVERY_UPDATES: u32 = 200; 14 | 15 | pub struct RenderCache { 16 | /// RenderCache will invalidate 17 | updates_till_invalidation: u32, 18 | room_terrains: HashMap, 19 | pub image_map: ImageMap, 20 | } 21 | 22 | impl RenderCache { 23 | pub fn new() -> Self { 24 | RenderCache { 25 | room_terrains: HashMap::new(), 26 | image_map: ImageMap::new(), 27 | updates_till_invalidation: INVALIDATE_EVERY_UPDATES, 28 | } 29 | } 30 | 31 | pub fn event_handler<'a>(&'a mut self) -> impl FnMut(&NetworkEvent) + 'a { 32 | move |evt| match *evt { 33 | NetworkEvent::MyInfo { .. } 34 | | NetworkEvent::Login { .. } 35 | | NetworkEvent::WebsocketHttpError { .. } 36 | | NetworkEvent::WebsocketError { .. } 37 | | NetworkEvent::WebsocketParseError { .. } 38 | | NetworkEvent::MapView { .. } 39 | | NetworkEvent::RoomView { .. } 40 | | NetworkEvent::ShardList { .. } => (), 41 | NetworkEvent::RoomTerrain { room_name, .. } => self.invalidate_terrain(room_name), 42 | } 43 | } 44 | 45 | /// Invalidates a generated terrain image, so the next time it's fetched it must be 46 | /// updated. 47 | pub fn invalidate_terrain(&mut self, room_name: RoomName) { 48 | debug!("Invalidating cached terrain image for {}", room_name); 49 | if let Some(id) = self.room_terrains.remove(&room_name) { 50 | self.image_map.remove(id); 51 | } 52 | } 53 | 54 | /// Updates the terrain *now* for a room and stores the new updated terrain image. 55 | pub fn update_terrain(&mut self, display: &glium::Display, room_name: RoomName, terrain: &TerrainGrid) { 56 | debug!("Creating new cached terrain image for {}", room_name); 57 | let new_texture = Texture::new(display, map_view::make_terrain_texture(terrain)) 58 | .expect("expected creating srgb texture to suceed"); 59 | 60 | match self.room_terrains.entry(room_name) { 61 | Entry::Occupied(entry) => { 62 | self.image_map.replace(*entry.get(), new_texture); 63 | } 64 | Entry::Vacant(entry) => { 65 | entry.insert(self.image_map.insert(new_texture)); 66 | } 67 | } 68 | } 69 | 70 | /// Gets a generated image for the given room's terrain, or generates it from the given 71 | /// terrain grid if it doesn't exist yet. 72 | pub fn get_or_generate_terrain( 73 | &mut self, 74 | display: &glium::Display, 75 | room_name: RoomName, 76 | terrain: &TerrainGrid, 77 | ) -> ImageId { 78 | let RenderCache { 79 | ref mut room_terrains, 80 | ref mut image_map, 81 | .. 82 | } = *self; 83 | 84 | room_terrains 85 | .entry(room_name) 86 | .or_insert_with(|| { 87 | debug!("Creating new cached terrain image for {}", room_name); 88 | let new_texture = Texture::new(display, map_view::make_terrain_texture(terrain)) 89 | .expect("expected creating srgb texture to suceed"); 90 | 91 | image_map.insert(new_texture) 92 | }) 93 | .clone() 94 | } 95 | 96 | /// Gets already generated terrain image for the given room name. If it doesn't exist yet, 97 | /// or was invalidated, returns `None`. 98 | pub fn get_terrain(&self, room_name: RoomName) -> Option { 99 | self.room_terrains.get(&room_name).cloned() 100 | } 101 | 102 | /// Does a check to see if we should invalidate cached images. 103 | /// 104 | /// Every 200 times this is called, all rendered images which aren't 105 | /// within the two RoomNames will be removed. 106 | pub fn invalidation_check(&mut self, r1: RoomName, r2: RoomName) { 107 | self.updates_till_invalidation -= 1; 108 | if self.updates_till_invalidation == 0 { 109 | self.invalidate_outside_of(r1, r2); 110 | self.updates_till_invalidation = INVALIDATE_EVERY_UPDATES; 111 | } 112 | } 113 | 114 | /// Removes all cached rendered images which aren't within the two RoomNames, inclusively. 115 | pub fn invalidate_outside_of(&mut self, r1: RoomName, r2: RoomName) { 116 | let RenderCache { 117 | ref mut room_terrains, 118 | ref mut image_map, 119 | .. 120 | } = *self; 121 | 122 | let min_x = r1.x_coord.min(r1.x_coord); 123 | let max_x = r1.x_coord.max(r2.x_coord); 124 | let min_y = r1.y_coord.min(r2.y_coord); 125 | let max_y = r1.y_coord.max(r2.y_coord); 126 | 127 | room_terrains.retain(|key, value| { 128 | if key.x_coord >= min_x && key.x_coord <= max_x && key.y_coord >= min_y && key.y_coord <= max_y { 129 | true 130 | } else { 131 | debug!("Trimming cached terrain image for {}", key); 132 | image_map.remove(*value); 133 | false 134 | } 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /ui/src/rust/rendering/types.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Generator, GeneratorState}; 2 | 3 | #[derive(Copy, Clone, Debug, PartialEq)] 4 | pub struct MapViewOffset { 5 | pub(super) x_offset: f64, 6 | pub(super) y_offset: f64, 7 | pub(super) room_size: f64, 8 | } 9 | 10 | impl MapViewOffset { 11 | #[inline(always)] 12 | pub fn new(x: f64, y: f64, size: f64) -> Self { 13 | MapViewOffset { 14 | x_offset: x, 15 | y_offset: y, 16 | room_size: size, 17 | } 18 | } 19 | } 20 | 21 | pub(super) struct IterAdapter(pub(super) G); 22 | 23 | impl Iterator for IterAdapter 24 | where 25 | G: Generator, 26 | { 27 | type Item = G::Yield; 28 | 29 | fn next(&mut self) -> Option { 30 | match self.0.resume() { 31 | GeneratorState::Yielded(item) => Some(item), 32 | GeneratorState::Complete(()) => None, 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/rust/ui_state/mod.rs: -------------------------------------------------------------------------------- 1 | use {conrod, screeps_api, time}; 2 | use NetworkHandler; 3 | use map_view_utils::{bound_zoom, zoom_multiplier_from_factor, ZOOM_MODIFIER}; 4 | 5 | #[derive(Debug, PartialEq)] 6 | pub enum Event { 7 | LeftMenuOpened, 8 | LeftMenuClosed, 9 | SwitchShard(Option), 10 | LoginUsername(String), 11 | LoginPassword(String), 12 | LoginServer(String), 13 | LoginShard(String), 14 | LoginSubmitted(time::Tm), 15 | MapPan { 16 | view_rect: conrod::Rect, 17 | event: MapPanEvent, 18 | }, 19 | MapZoom { 20 | view_rect: conrod::Rect, 21 | event: MapZoomEvent, 22 | }, 23 | MapClick { 24 | view_rect: conrod::Rect, 25 | event: MapClickEvent, 26 | }, 27 | NowLoggedOut, 28 | LoggedInMapView, 29 | Exit, 30 | } 31 | 32 | #[derive(Debug)] 33 | pub struct State { 34 | pub network: Option, 35 | pub screen_state: ScreenState, 36 | } 37 | 38 | #[derive(Debug)] 39 | pub enum ScreenState { 40 | Login(LoginScreenState), 41 | Map(MapScreenState), 42 | Exit, 43 | } 44 | 45 | pub struct LoginScreenState { 46 | pub pending_since: Option, 47 | pub username: String, 48 | pub password: String, 49 | pub server: String, 50 | pub shard: String, 51 | } 52 | 53 | impl Default for LoginScreenState { 54 | fn default() -> Self { 55 | LoginScreenState { 56 | pending_since: None, 57 | username: String::new(), 58 | password: String::new(), 59 | server: "https://screeps.com".to_owned(), 60 | shard: "shard0".to_owned(), 61 | } 62 | } 63 | } 64 | 65 | impl ::std::fmt::Debug for LoginScreenState { 66 | fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { 67 | fmt.debug_struct("LoginScreenState") 68 | .field("pending_since", &self.pending_since) 69 | .field("username", &self.username) 70 | .field("password", &"") 71 | .finish() 72 | } 73 | } 74 | 75 | #[derive(Debug)] 76 | pub struct MapScreenState { 77 | pub shard: Option, 78 | pub map_scroll: ScrollState, 79 | pub panels: PanelStates, 80 | } 81 | 82 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 83 | pub enum MenuState { 84 | Open, 85 | Closed, 86 | } 87 | 88 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)] 89 | pub struct PanelStates { 90 | pub left: MenuState, 91 | } 92 | 93 | impl Default for MenuState { 94 | fn default() -> Self { 95 | MenuState::Closed 96 | } 97 | } 98 | 99 | #[derive(Copy, Clone, Debug, PartialEq, Default)] 100 | pub struct MapPanEvent { 101 | /// The number of pixels scrolled horizontally. 102 | pub scrolled_map_x: f64, 103 | /// The number of pixels scrolled vertically. 104 | pub scrolled_map_y: f64, 105 | } 106 | 107 | #[derive(Copy, Clone, Debug, PartialEq, Default)] 108 | pub struct MapZoomEvent { 109 | /// The scroll change amount (unknown unit). 110 | pub zoom_change: f64, 111 | /// If zoom_change != 0.0, this is the x position the mouse was at, relative to the center of the widget. 112 | pub zoom_mouse_rel_x: f64, 113 | /// If zoom_change != 0.0, this is the y position the mouse was at, relative to the center of the widget. 114 | pub zoom_mouse_rel_y: f64, 115 | } 116 | 117 | #[derive(Copy, Clone, Debug, PartialEq)] 118 | pub struct MapClickEvent { 119 | /// If the screen was clicked, the relative (x, y) that it was clicked at. 120 | pub clicked: (f64, f64), 121 | } 122 | 123 | #[derive(Debug)] 124 | pub struct ScrollState { 125 | /// The horizontal scroll, in fractional rooms. 1 is 1 room. 126 | pub scroll_x: f64, 127 | /// The vertical scroll, in fractional rooms. 1 is 1 room. 128 | pub scroll_y: f64, 129 | /// The zoom factor, 1.0 is a room the same size as the minimum screen dimension. 130 | pub zoom_factor: f64, 131 | /// The room name currently selected. 132 | pub selected_room: Option, 133 | } 134 | 135 | impl MapScreenState { 136 | pub fn new() -> Self { 137 | // TODO: saved position? or use API to get position? 138 | MapScreenState { 139 | panels: PanelStates::default(), 140 | shard: None, 141 | map_scroll: ScrollState::default(), 142 | } 143 | } 144 | } 145 | 146 | impl State { 147 | pub fn new() -> State { 148 | State { 149 | network: None, 150 | screen_state: ScreenState::Login(LoginScreenState::default()), 151 | } 152 | } 153 | 154 | pub fn transform(&mut self, events: T) 155 | where 156 | T: IntoIterator, 157 | { 158 | for event in events { 159 | self.event(event); 160 | } 161 | } 162 | 163 | fn event(&mut self, event: Event) { 164 | match event { 165 | Event::LeftMenuOpened => match self.screen_state { 166 | ScreenState::Map(ref mut state) => { 167 | debug!("left menu opened"); 168 | state.panels.left = MenuState::Open; 169 | } 170 | _ => (), 171 | }, 172 | Event::LeftMenuClosed => match self.screen_state { 173 | ScreenState::Map(ref mut state) => { 174 | debug!("left menu closed"); 175 | state.panels.left = MenuState::Closed; 176 | } 177 | _ => (), 178 | }, 179 | // Event::ShardButton(new_shard) => if let ScreenState::Map(ref mut state) = self.screen_state { 180 | // state.shard = new_shard; 181 | // }, 182 | Event::LoginServer(new_server) => if let ScreenState::Login(ref mut state) = self.screen_state { 183 | debug!("login server changed"); 184 | state.server = new_server; 185 | }, 186 | Event::LoginShard(new_shard) => if let ScreenState::Login(ref mut state) = self.screen_state { 187 | debug!("login shard changed"); 188 | state.shard = new_shard; 189 | }, 190 | Event::LoginUsername(new_username) => if let ScreenState::Login(ref mut state) = self.screen_state { 191 | debug!("login username changed"); 192 | state.username = new_username; 193 | }, 194 | Event::LoginPassword(new_password) => if let ScreenState::Login(ref mut state) = self.screen_state { 195 | debug!("login password changed"); 196 | state.password = new_password; 197 | }, 198 | Event::LoginSubmitted(at) => if let ScreenState::Login(ref mut state) = self.screen_state { 199 | debug!("login submitted"); 200 | state.pending_since = Some(at); 201 | }, 202 | Event::MapPan { view_rect, event } => if let ScreenState::Map(ref mut state) = self.screen_state { 203 | debug!("map view panned"); 204 | state.map_scroll.pan_event(view_rect, event); 205 | }, 206 | Event::MapZoom { view_rect, event } => if let ScreenState::Map(ref mut state) = self.screen_state { 207 | debug!("map view zoomed"); 208 | state.map_scroll.zoom_event(view_rect, event); 209 | }, 210 | Event::MapClick { view_rect, event } => if let ScreenState::Map(ref mut state) = self.screen_state { 211 | debug!("map view MapClickEvent"); 212 | state.map_scroll.click_event(view_rect, event); 213 | }, 214 | Event::NowLoggedOut => { 215 | debug!("logged out"); 216 | self.screen_state = ScreenState::Login(LoginScreenState::default()) 217 | } 218 | Event::LoggedInMapView => { 219 | debug!("logged in"); 220 | self.screen_state = ScreenState::Map(MapScreenState::new()) 221 | } 222 | Event::Exit => { 223 | debug!("exited"); 224 | self.screen_state = ScreenState::Exit 225 | } 226 | Event::SwitchShard(shard) => if let ScreenState::Map(ref mut state) = self.screen_state { 227 | debug!("switched shard"); 228 | state.shard = shard; 229 | }, 230 | } 231 | } 232 | } 233 | 234 | impl Default for ScrollState { 235 | fn default() -> Self { 236 | ScrollState { 237 | scroll_x: 0.0, 238 | scroll_y: 0.0, 239 | zoom_factor: 1.0, 240 | selected_room: None, 241 | } 242 | } 243 | } 244 | 245 | impl ScrollState { 246 | fn room_name_and_xy_from_rel_pos( 247 | &self, 248 | view_rect: conrod::Rect, 249 | mouse_rel_x: f64, 250 | mouse_rel_y: f64, 251 | ) -> (screeps_api::RoomName, (f64, f64)) { 252 | let abs_mouse_x = view_rect.w() / 2.0 + mouse_rel_x; 253 | let abs_mouse_y = view_rect.h() / 2.0 + mouse_rel_y; 254 | 255 | let ScrollState { 256 | scroll_x: saved_room_scroll_x, 257 | scroll_y: saved_room_scroll_y, 258 | zoom_factor, 259 | .. 260 | } = *self; 261 | 262 | let room_size = view_rect.w().min(view_rect.h()) * zoom_multiplier_from_factor(zoom_factor); 263 | 264 | let room_scroll_x = saved_room_scroll_x - (view_rect.w() / room_size / 2.0) + (abs_mouse_x / room_size); 265 | let room_scroll_y = saved_room_scroll_y - (view_rect.h() / room_size / 2.0) + (abs_mouse_y / room_size); 266 | 267 | let initial_room = screeps_api::RoomName { 268 | x_coord: room_scroll_x.floor() as i32, 269 | y_coord: room_scroll_y.floor() as i32, 270 | }; 271 | 272 | return (initial_room, (0.0, 0.0)); 273 | 274 | // let extra_scroll_x = -(room_scroll_x % 1.0) * room_size; 275 | // let extra_scroll_y = -(room_scroll_y % 1.0) * room_size; 276 | // let extra_scroll_x = if extra_scroll_x > 0.0 { 277 | // extra_scroll_x - room_size 278 | // } else { 279 | // extra_scroll_x 280 | // }; 281 | // let extra_scroll_y = if extra_scroll_y > 0.0 { 282 | // extra_scroll_y - room_size 283 | // } else { 284 | // extra_scroll_y 285 | // }; 286 | // let count_x = ((view_rect.w() - extra_scroll_x) / room_size).ceil() as i32; 287 | // let count_y = ((view_rect.h() - extra_scroll_y) / room_size).ceil() as i32; 288 | 289 | // let extra_scroll_x = -(room_scroll_x % 1.0) * room_size; 290 | // let extra_scroll_y = -(room_scroll_y % 1.0) * room_size; 291 | // let extra_scroll_x = if extra_scroll_x > 0.0 { 292 | // extra_scroll_x - room_size 293 | // } else { 294 | // extra_scroll_x 295 | // }; 296 | // let extra_scroll_y = if extra_scroll_y > 0.0 { 297 | // extra_scroll_y - room_size 298 | // } else { 299 | // extra_scroll_y 300 | // }; 301 | 302 | // unimplemented!() 303 | } 304 | 305 | fn pan_event(&mut self, view_rect: conrod::Rect, update: MapPanEvent) { 306 | let room_size_unit = view_rect.w().min(view_rect.h()); 307 | 308 | let room_size = room_size_unit * zoom_multiplier_from_factor(self.zoom_factor); 309 | self.scroll_x += update.scrolled_map_x / room_size; 310 | self.scroll_y += update.scrolled_map_y / room_size; 311 | } 312 | 313 | fn zoom_event(&mut self, view_rect: conrod::Rect, update: MapZoomEvent) { 314 | let room_size_unit = view_rect.w().min(view_rect.h()); 315 | 316 | if update.zoom_change != 0.0 { 317 | let abs_mouse_x = view_rect.w() / 2.0 + update.zoom_mouse_rel_x; 318 | let abs_mouse_y = view_rect.h() / 2.0 + update.zoom_mouse_rel_y; 319 | 320 | let new_zoom_factor = bound_zoom(self.zoom_factor + update.zoom_change * ZOOM_MODIFIER); 321 | 322 | if self.zoom_factor != new_zoom_factor { 323 | let room_pixel_size = room_size_unit * zoom_multiplier_from_factor(self.zoom_factor); 324 | let new_room_pixel_size = room_size_unit * zoom_multiplier_from_factor(new_zoom_factor); 325 | 326 | let current_room_x = abs_mouse_x / room_pixel_size - (view_rect.w() / room_pixel_size / 2.0); 327 | let current_room_y = abs_mouse_y / room_pixel_size - (view_rect.h() / room_pixel_size / 2.0); 328 | 329 | let next_room_x = abs_mouse_x / new_room_pixel_size - (view_rect.w() / new_room_pixel_size / 2.0); 330 | let next_room_y = abs_mouse_y / new_room_pixel_size - (view_rect.h() / new_room_pixel_size / 2.0); 331 | 332 | self.scroll_x += current_room_x - next_room_x; 333 | self.scroll_y += current_room_y - next_room_y; 334 | self.zoom_factor = new_zoom_factor; 335 | } 336 | } 337 | } 338 | 339 | fn click_event(&mut self, view_rect: conrod::Rect, update: MapClickEvent) { 340 | let (clicked_x, clicked_y) = update.clicked; 341 | 342 | let (room_clicked, _) = self.room_name_and_xy_from_rel_pos(view_rect, clicked_x, clicked_y); 343 | 344 | info!("Clicked {}", room_clicked); 345 | 346 | self.selected_room = Some(room_clicked); 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /ui/src/rust/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | //! Conrod widgets 2 | mod text; 3 | pub use self::text::{text_box, text_edit}; 4 | -------------------------------------------------------------------------------- /ui/src/rust/window_management/glutin_glue.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time}; 2 | 3 | use glutin; 4 | 5 | /// This manages the underlying support for glium + glutin. 6 | /// 7 | /// This is roughly modeled off of the [`EventLoop`][ev] structure present in the `conrod` examples, with a number of 8 | /// modifications in order to avoid any additional allocations. 9 | /// 10 | /// Specifically, the 'next' method of the conrod example [`EventLoop`][ev] returns a Vec of events... which are 11 | /// literally collected from an iterator. Instead, this struct implements Iterator itself, and iterates an enum for 12 | /// either `glutin` events or an "update UI" event, which corresponds to returning the collected Vec in the conrod 13 | /// example. 14 | /// 15 | /// [ev]: https://github.com/PistonDevelopers/conrod/blob/master/examples/support/mod.rs#L367 16 | pub struct EventLoop { 17 | events_loop: glutin::EventsLoop, 18 | last_ui_update: time::Instant, 19 | } 20 | 21 | pub struct LoopControl { 22 | ui_needs_update: u8, 23 | exiting: bool, 24 | } 25 | 26 | impl LoopControl { 27 | /// Notifies the event loop that the `Ui` requires another update whether or not there are any 28 | /// pending events. 29 | /// 30 | /// This is primarily used on the occasion that some part of the `Ui` is still animating and 31 | /// requires further updates to do so. 32 | pub fn needs_update(&mut self) { 33 | self.ui_needs_update = 3; 34 | } 35 | 36 | /// Notifies the loop to skip all current pending events, and exit the loop immediately afterwards. 37 | pub fn exit(&mut self) { 38 | self.exiting = true; 39 | } 40 | } 41 | 42 | impl EventLoop { 43 | pub fn new(events_loop: glutin::EventsLoop) -> Self { 44 | EventLoop { 45 | events_loop: events_loop, 46 | last_ui_update: time::Instant::now() - time::Duration::from_millis(16), 47 | } 48 | } 49 | 50 | fn poll_events(&mut self, callback: F) 51 | where 52 | F: FnMut(glutin::Event), 53 | { 54 | self.events_loop.poll_events(callback); 55 | } 56 | 57 | fn wait_events(&mut self, mut callback: F) 58 | where 59 | F: FnMut(glutin::Event), 60 | { 61 | self.events_loop.run_forever(|event| { 62 | callback(event); 63 | 64 | glutin::ControlFlow::Break 65 | }) 66 | } 67 | 68 | /// Gets the next event. If there are no glutin events available, this either returns `Event::UpdateUi` or waits 69 | /// for an event depending on if the UI needs updating 70 | pub fn run_loop(&mut self, mut callback: F) 71 | where 72 | F: FnMut(&mut LoopControl, Event), 73 | { 74 | let mut control = LoopControl { 75 | ui_needs_update: 3, 76 | exiting: false, 77 | }; 78 | 79 | loop { 80 | self.poll_events(|evt| { 81 | if !control.exiting { 82 | callback(&mut control, Event::Glutin(evt)) 83 | } 84 | }); 85 | 86 | if control.exiting { 87 | break; 88 | } 89 | 90 | if control.ui_needs_update > 0 { 91 | let sixteen_ms = time::Duration::from_millis(16); 92 | let time_since = self.last_ui_update.elapsed(); 93 | if time_since < sixteen_ms { 94 | thread::sleep(sixteen_ms - time_since); 95 | // re-poll for window events once more, then when this code block runs sixteen milliseconds 96 | // will definitely have passed. 97 | continue; 98 | } 99 | self.last_ui_update = time::Instant::now(); 100 | control.ui_needs_update -= 1; 101 | callback(&mut control, Event::UpdateUi); 102 | continue; 103 | } 104 | 105 | self.wait_events(|evt| { 106 | if !control.exiting { 107 | callback(&mut control, Event::Glutin(evt)) 108 | } 109 | }); 110 | 111 | if control.exiting { 112 | break; 113 | } 114 | } 115 | } 116 | } 117 | 118 | /// Event returned from EventLoop. 119 | pub enum Event { 120 | /// A glutin event was found. 121 | Glutin(glutin::Event), 122 | /// The UI needs to be updated. 123 | UpdateUi, 124 | } 125 | -------------------------------------------------------------------------------- /ui/src/rust/window_management/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod glutin_glue; 2 | pub mod setup; 3 | pub mod window_loop; 4 | -------------------------------------------------------------------------------- /ui/src/rust/window_management/setup.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub use app::App; 4 | 5 | use {chrono, fern, glium, glutin, log, rusttype}; 6 | 7 | fn akashi_font() -> rusttype::Font<'static> { 8 | let font_data = include_bytes!("../../ttf/Akashi.ttf"); 9 | let collection = rusttype::FontCollection::from_bytes(font_data as &[u8]); 10 | 11 | collection 12 | .into_font() 13 | .expect("expected loading embedded Akashi.ttf font to succeed") 14 | } 15 | 16 | pub fn init_window() -> (glutin::EventsLoop, App) { 17 | // Create window. 18 | let events_loop = glutin::EventsLoop::new(); 19 | let window = glutin::WindowBuilder::new() 20 | .with_dimensions(640, 480) 21 | .with_title("screeps-rs-client"); 22 | let context = glutin::ContextBuilder::new(); 23 | let display = 24 | glium::Display::new(window, context, &events_loop).expect("expected initial window creation to succeed"); 25 | 26 | // Create UI and other components. 27 | let mut app = App::new(display, &events_loop); 28 | 29 | // Add font. 30 | app.ui.fonts.insert(akashi_font()); 31 | 32 | (events_loop, app) 33 | } 34 | 35 | pub fn init_logger(verbose: bool, debug_modules: I) 36 | where 37 | T: AsRef, 38 | I: IntoIterator, 39 | { 40 | let mut dispatch = fern::Dispatch::new() 41 | .level(if verbose { 42 | log::LevelFilter::Trace 43 | } else { 44 | log::LevelFilter::Info 45 | }) 46 | .level_for("rustls", log::LevelFilter::Warn) 47 | .level_for("hyper", log::LevelFilter::Warn); 48 | 49 | for module in debug_modules { 50 | dispatch = dispatch.level_for(module.as_ref().to_owned(), log::LevelFilter::Trace); 51 | } 52 | 53 | dispatch 54 | .format(|out, msg, record| { 55 | let now = chrono::Local::now(); 56 | 57 | out.finish(format_args!( 58 | "[{}][{}] {}: {}", 59 | now.format("%H:%M:%S"), 60 | record.level(), 61 | record.target(), 62 | msg 63 | )); 64 | }) 65 | .chain(io::stdout()) 66 | .apply() 67 | .unwrap_or_else(|_| warn!("Logging initialization failed: a global logger was already set!")); 68 | } 69 | -------------------------------------------------------------------------------- /ui/src/rust/window_management/window_loop.rs: -------------------------------------------------------------------------------- 1 | use glium::Surface; 2 | use app::{App, AppCell}; 3 | use super::glutin_glue::{Event, EventLoop}; 4 | 5 | use {conrod, glium, glium_backend, glutin, layout, rendering, ui_state}; 6 | 7 | pub fn main_window_loop(events: glutin::EventsLoop, mut app: App) { 8 | let mut events = EventLoop::new(events); 9 | 10 | let mut state = ui_state::State::new(); 11 | 12 | debug!("Starting event loop."); 13 | 14 | events.run_loop(|control, event| { 15 | if let ui_state::ScreenState::Exit = state.screen_state { 16 | info!("exiting."); 17 | control.exit(); 18 | return; 19 | } 20 | 21 | match event { 22 | Event::Glutin(event) => { 23 | debug!("Glutin Event: {:?}", event); 24 | 25 | // Use the `winit` backend feature to convert the winit event to a conrod one. 26 | if let Some(event) = conrod::backend::winit::convert_event(event.clone(), &app.display) { 27 | debug!("Conrod Event: {:?}", event); 28 | 29 | app.ui.handle_event(event); 30 | control.needs_update(); 31 | } 32 | 33 | match event { 34 | glutin::Event::WindowEvent { event, .. } => { 35 | match event { 36 | // Break from the loop upon `Escape`. 37 | glutin::WindowEvent::KeyboardInput { 38 | input: 39 | glutin::KeyboardInput { 40 | virtual_keycode: Some(glutin::VirtualKeyCode::Escape), 41 | .. 42 | }, 43 | .. 44 | } 45 | | glutin::WindowEvent::Closed => control.exit(), 46 | glutin::WindowEvent::Refresh | glutin::WindowEvent::Resized(..) => { 47 | app.ui.needs_redraw(); 48 | control.needs_update(); 49 | } 50 | _ => (), 51 | } 52 | } 53 | glutin::Event::Awakened => { 54 | app.ui.needs_redraw(); 55 | control.needs_update(); 56 | } 57 | _ => (), 58 | } 59 | } 60 | Event::UpdateUi => { 61 | debug!("UpdateUI Event."); 62 | 63 | let mut additional_render = None; 64 | 65 | { 66 | let App { 67 | ref mut ui, 68 | ref display, 69 | ref mut image_cache, 70 | ref mut ids, 71 | ref mut renderer, 72 | ref mut net_cache, 73 | ref mut network_handler, 74 | ref notify, 75 | .. 76 | } = app; 77 | 78 | let mut ui_cell = ui.set_widgets(); 79 | 80 | let mut cell = AppCell::cell( 81 | &mut ui_cell, 82 | display, 83 | image_cache, 84 | ids, 85 | renderer, 86 | net_cache, 87 | network_handler, 88 | &mut additional_render, 89 | notify, 90 | ); 91 | 92 | // Create main screen. 93 | layout::create_ui(&mut cell, &mut state); 94 | } 95 | 96 | let ready_additional_render = additional_render.map(|r| { 97 | r.ready(&app.ui) 98 | .expect("expected custom render widget to exist") 99 | }); 100 | // Render the `Ui` and then display it on the screen. 101 | if let Some(mut primitives) = app.ui.draw_if_changed() { 102 | use rendering::RenderPipelineFinish; 103 | use layout::BACKGROUND_RGB; 104 | 105 | if let Some(renderer) = ready_additional_render.as_ref() { 106 | renderer.prepare_images(&app.display, &mut app.image_cache); 107 | } 108 | 109 | struct RenderFinish<'a> { 110 | display: &'a glium::Display, 111 | image_map: &'a conrod::image::Map, 112 | renderer: &'a mut glium_backend::Renderer, 113 | } 114 | impl<'a> RenderPipelineFinish for RenderFinish<'a> { 115 | fn render_with(self, walker: T) 116 | where 117 | T: conrod::render::PrimitiveWalker, 118 | { 119 | let RenderFinish { 120 | display, 121 | image_map, 122 | renderer, 123 | } = self; 124 | 125 | renderer.fill(&*display, walker, &*image_map); 126 | } 127 | } 128 | { 129 | let last_step = RenderFinish { 130 | display: &app.display, 131 | image_map: &app.image_cache.image_map, 132 | renderer: &mut app.renderer, 133 | }; 134 | 135 | match ready_additional_render { 136 | Some(mut r) => r.render_with(primitives, &app.image_cache, last_step), 137 | None => last_step.render_with(primitives), 138 | } 139 | } 140 | 141 | let mut target = app.display.draw(); 142 | target.clear_color(BACKGROUND_RGB[0], BACKGROUND_RGB[1], BACKGROUND_RGB[2], 1.0); 143 | app.renderer 144 | .draw(&app.display, &mut target, &app.image_cache.image_map) 145 | .expect("expected drawing GUI to display to succeed"); 146 | target 147 | .finish() 148 | .expect("expected frame to remain unfinished at this point in the main loop."); 149 | } 150 | } 151 | } 152 | }); 153 | } 154 | -------------------------------------------------------------------------------- /ui/src/ttf/Akashi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daboross/screeps-rs/5c92cbd44f4342e206baa538d8897cb4de942361/ui/src/ttf/Akashi.ttf --------------------------------------------------------------------------------