├── .clang-format ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── Dockerfile ├── LICENSE.txt ├── README.md ├── android └── AndroidManifest.xml ├── assets ├── quickcurver.desktop └── quickcurver.svg ├── doc ├── Doxyfile ├── NETWORK.md └── man │ └── man1 │ └── quickcurver.1 ├── flake.lock ├── flake.nix ├── scripts ├── AppRun ├── appimage.sh ├── build.sh └── format-code.sh └── src ├── backend.cpp ├── backend.hpp ├── bot.cpp ├── bot.hpp ├── cleaninstallanimation.cpp ├── cleaninstallanimation.hpp ├── commandlinereader.cpp ├── commandlinereader.hpp ├── curver.cpp ├── curver.hpp ├── explosion.cpp ├── explosion.hpp ├── game.cpp ├── game.hpp ├── gamewatcher.cpp ├── gamewatcher.hpp ├── gui.cpp ├── gui.hpp ├── headnode.cpp ├── headnode.hpp ├── itemfactory.cpp ├── itemfactory.hpp ├── items ├── agileitem.cpp ├── agileitem.hpp ├── cleaninstallitem.cpp ├── cleaninstallitem.hpp ├── flashitem.cpp ├── flashitem.hpp ├── ghostitem.cpp ├── ghostitem.hpp ├── invisibleitem.cpp ├── invisibleitem.hpp ├── item.cpp ├── item.hpp ├── slowitem.cpp ├── slowitem.hpp ├── speeditem.cpp └── speeditem.hpp ├── main.cpp ├── models ├── chatmodel.cpp ├── chatmodel.hpp ├── itemmodel.cpp ├── itemmodel.hpp ├── playermodel.cpp └── playermodel.hpp ├── network ├── client.cpp ├── client.hpp ├── network.cpp ├── network.hpp ├── server.cpp └── server.hpp ├── qml ├── About.qml ├── Chat.qml ├── Main.qml ├── Players.qml └── Settings.qml ├── segment.cpp ├── segment.hpp ├── settings.cpp ├── settings.hpp ├── util.cpp ├── util.hpp ├── version.cpp ├── version.hpp ├── wall.cpp └── wall.hpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | AccessModifierOffset: -4 4 | AlignAfterOpenBracket: DontAlign 5 | AlignArrayOfStructures: None 6 | AlignConsecutiveAssignments: None 7 | AlignConsecutiveMacros: None 8 | AlignConsecutiveBitFields: None 9 | AlignConsecutiveDeclarations: None 10 | AlignEscapedNewlines: DontAlign 11 | AlignOperands: false 12 | AlignTrailingComments: false 13 | AllowAllArgumentsOnNextLine: true 14 | AllowAllConstructorInitializersOnNextLine: true 15 | AllowAllParametersOfDeclarationOnNextLine: true 16 | AllowShortEnumsOnASingleLine: true 17 | AllowShortBlocksOnASingleLine: Never 18 | AllowShortCaseLabelsOnASingleLine: false 19 | AllowShortFunctionsOnASingleLine: None 20 | AllowShortLambdasOnASingleLine: All 21 | AllowShortIfStatementsOnASingleLine: Never 22 | AllowShortLoopsOnASingleLine: false 23 | AlwaysBreakAfterReturnType: None 24 | AlwaysBreakBeforeMultilineStrings: false 25 | AlwaysBreakTemplateDeclarations: Yes 26 | BinPackArguments: false 27 | BinPackParameters: false 28 | BreakBeforeBinaryOperators: None 29 | # BreakBeforeConceptDeclarations: Allowed 30 | BreakBeforeConceptDeclarations: false 31 | BreakBeforeBraces: Attach 32 | BreakBeforeInheritanceComma: false 33 | BreakInheritanceList: AfterComma 34 | BreakBeforeTernaryOperators: false 35 | BreakConstructorInitializers: BeforeColon 36 | BreakStringLiterals: false 37 | ColumnLimit: 0 38 | CompactNamespaces: false 39 | ConstructorInitializerIndentWidth: 4 40 | ContinuationIndentWidth: 4 41 | Cpp11BracedListStyle: true 42 | DeriveLineEnding: false 43 | DerivePointerAlignment: false 44 | DisableFormat: false 45 | EmptyLineAfterAccessModifier: Never 46 | EmptyLineBeforeAccessModifier: Leave 47 | ExperimentalAutoDetectBinPacking: false 48 | FixNamespaceComments: false 49 | IncludeBlocks: Preserve 50 | IndentAccessModifiers: false 51 | IndentCaseLabels: false 52 | IndentCaseBlocks: true 53 | IndentGotoLabels: false 54 | IndentPPDirectives: None 55 | IndentExternBlock: AfterExternBlock 56 | IndentRequires: false 57 | IndentWidth: 4 58 | IndentWrappedFunctionNames: false 59 | InsertTrailingCommas: Wrapped 60 | KeepEmptyLinesAtTheStartOfBlocks: true 61 | LambdaBodyIndentation: OuterScope 62 | MaxEmptyLinesToKeep: 2 63 | NamespaceIndentation: None 64 | PenaltyBreakAssignment: 2 65 | PenaltyBreakBeforeFirstCallParameter: 19 66 | PenaltyBreakComment: 300 67 | PenaltyBreakFirstLessLess: 120 68 | PenaltyBreakString: 1000 69 | PenaltyBreakTemplateDeclaration: 10 70 | PenaltyExcessCharacter: 1000000 71 | PenaltyReturnTypeOnItsOwnLine: 60 72 | PenaltyIndentedWhitespace: 0 73 | PointerAlignment: Right 74 | PPIndentWidth: -1 75 | ReferenceAlignment: Pointer 76 | ReflowComments: false 77 | ShortNamespaceLines: 1 78 | SortIncludes: CaseSensitive 79 | SortUsingDeclarations: false 80 | SpaceAfterCStyleCast: true 81 | SpaceAfterLogicalNot: false 82 | SpaceAfterTemplateKeyword: false 83 | SpaceAroundPointerQualifiers: Default 84 | SpaceBeforeAssignmentOperators: true 85 | SpaceBeforeCaseColon: false 86 | SpaceBeforeCpp11BracedList: true 87 | SpaceBeforeCtorInitializerColon: true 88 | SpaceBeforeInheritanceColon: true 89 | SpaceBeforeParens: ControlStatements 90 | SpaceBeforeRangeBasedForLoopColon: true 91 | SpaceBeforeSquareBrackets: false 92 | SpaceInEmptyBlock: false 93 | SpaceInEmptyParentheses: false 94 | SpacesBeforeTrailingComments: 1 95 | SpacesInAngles: Never 96 | SpacesInCStyleCastParentheses: false 97 | SpacesInConditionalStatement: false 98 | SpacesInContainerLiterals: false 99 | SpacesInLineCommentPrefix: 100 | Minimum: 1 101 | Maximum: -1 102 | SpacesInParentheses: false 103 | SpacesInSquareBrackets: false 104 | Standard: Latest 105 | TabWidth: 4 106 | UseCRLF: false 107 | UseTab: AlignWithSpaces 108 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 109 | # PackConstructorInitializers: CurrentLine 110 | ... 111 | 112 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | with: 9 | submodules: recursive 10 | - name: Build the Docker image 11 | run: docker build -t quickcurver . 12 | - name: Format code 13 | run: docker run quickcurver sh -c 'scripts/format-code.sh' 14 | - name: Check that headless server works 15 | run: echo "/quit" | docker run -i quickcurver build/quickcurver -platform offscreen 16 | - name: Build documentation 17 | run: docker run quickcurver sh -c 'cd doc && doxygen' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.qbs.user 20 | *.qbs.user.* 21 | *.moc 22 | moc_*.cpp 23 | qrc_*.cpp 24 | ui_*.h 25 | Makefile* 26 | *build-* 27 | 28 | # QtCreator 29 | 30 | *.autosave 31 | 32 | # QtCtreator Qml 33 | *.qmlproject.user 34 | *.qmlproject.user.* 35 | 36 | # QtCtreator CMake 37 | CMakeLists.txt.user 38 | compile_commands.json 39 | 40 | #Ignore build folder 41 | /build/* 42 | #Ignore sublime project file 43 | *.sublime-project 44 | *.sublime-workspace 45 | # KDevelop 46 | *.kdev4 47 | /.kdev4/* 48 | .directory 49 | # vim 50 | *.swp 51 | 52 | # documentation 53 | html/ 54 | latex/ 55 | 56 | *.AppImage 57 | appdir/ 58 | /.cache/ 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | dist: bionic 4 | 5 | branches: 6 | only: 7 | - master 8 | - develop 9 | - travis 10 | 11 | sudo: required 12 | 13 | services: 14 | - docker 15 | 16 | before_install: 17 | - docker pull vimpostor/arch-qt5 18 | 19 | script: 20 | - scripts/travisDocker.sh 21 | 22 | notifications: 23 | email: 24 | on_success: never 25 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | project(quickcurver VERSION 0.1 DESCRIPTION "Modern Qt/C++ implementation of Achtung die Kurve with online multiplayer") 3 | 4 | set(CMAKE_CXX_STANDARD 23) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | set(CMAKE_CXX_EXTENSIONS OFF) 7 | 8 | list(APPEND QT_MODULES Core Qml Quick QuickControls2 Svg) 9 | find_package(Qt6 6.6 COMPONENTS ${QT_MODULES} REQUIRED) 10 | qt_standard_project_setup() 11 | set(QT_PREFIXED_MODULES ${QT_MODULES}) 12 | list(TRANSFORM QT_PREFIXED_MODULES PREPEND "Qt6::") 13 | 14 | include_directories("src" "src/models") 15 | add_compile_definitions(QUICKCURVER_VERSION="${PROJECT_VERSION}") 16 | 17 | file(GLOB_RECURSE SRCS "src/*.cpp") 18 | file(GLOB_RECURSE HDRS "src/*.hpp") 19 | file(GLOB_RECURSE QMLS RELATIVE "${CMAKE_SOURCE_DIR}" "src/qml/*.qml") 20 | 21 | qt_add_executable(${PROJECT_NAME} ${SRCS} ${RESOURCES}) 22 | 23 | qt_add_qml_module(${PROJECT_NAME} URI "Backend" VERSION 1.0 NO_RESOURCE_TARGET_PATH QML_FILES ${QMLS} SOURCES "src/backend.cpp") 24 | 25 | include(FetchContent) 26 | FetchContent_Declare(quartz GIT_REPOSITORY https://github.com/vimpostor/quartz.git GIT_TAG v0.8) 27 | FetchContent_MakeAvailable(quartz) 28 | 29 | target_link_libraries(${PROJECT_NAME} PRIVATE ${QT_PREFIXED_MODULES}) 30 | quartz_link(${PROJECT_NAME}) 31 | 32 | # install 33 | install(TARGETS ${PROJECT_NAME} RUNTIME) 34 | install(DIRECTORY "${CMAKE_SOURCE_DIR}/doc/man/" TYPE MAN) 35 | install(FILES "assets/${PROJECT_NAME}.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") 36 | # icon 37 | install(FILES "assets/${PROJECT_NAME}.svg" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps") 38 | list(APPEND ICON_SIZES 16 32 48 64 128 256 512) 39 | foreach(ICON_SIZE IN LISTS ICON_SIZES) 40 | list(APPEND ICON_COMMANDS COMMAND "convert" "${CMAKE_SOURCE_DIR}/assets/${PROJECT_NAME}.svg" "-resize" "${ICON_SIZE}x${ICON_SIZE}" "${ICON_SIZE}.png") 41 | install(FILES "${CMAKE_BINARY_DIR}/${ICON_SIZE}.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${ICON_SIZE}x${ICON_SIZE}/apps" RENAME "${PROJECT_NAME}.png") 42 | endforeach() 43 | add_custom_target(linux-desktop-integration ${ICON_COMMANDS}) 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM vimpostor/arch-qt6 2 | ADD . /build 3 | WORKDIR /build 4 | RUN pacman -Syu --noconfirm doxygen imagemagick binutils curl 5 | RUN scripts/build.sh 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick Curver 2 | This is a hardware accelerated implementation of the famous game "Achtung die Kurve", written in modern C++20 and Qt 6. 3 | 4 | [![Continuous Integration](https://github.com/vimpostor/quickcurver/actions/workflows/ci.yml/badge.svg)](https://github.com/vimpostor/quickcurver/actions/workflows/ci.yml) 5 | 6 | ![screenshot](https://user-images.githubusercontent.com/21310755/93923083-2ed2e300-fd13-11ea-86f1-d79bc09ce96a.png) 7 | 8 | # Features 9 | * Material Design 10 | * Local Multiplayer 11 | * Online Multiplayer 12 | * Bots 13 | * Items 14 | 15 | 16 | # Installation 17 | ## Compiling from source 18 | 19 | Note: For Arch Linux users there is an [AUR package](https://aur.archlinux.org/packages/quickcurver-git), for Nix users there is a [flake](flake.nix) available. 20 | 21 | ### Dependencies 22 | First make sure, that you have the required dependencies of QuickCurver installed. These are: 23 | 24 | * A C++ compiler with C++23 support 25 | * The latest Qt 26 | * The following Qt Modules (in the parentheses there is an example how the package could be called for your distro (this depends on the distro!)): 27 | - Qt Core (qt6-base) 28 | - Qt GUI (qt6-base) 29 | - Qt Quick (qt6-declarative) 30 | - Qt QML (qt6-declarative) 31 | - Qt SVG (qt6-svg) 32 | - Qt Network (qt6-base) 33 | 34 | ### Build instructions 35 | Run the following commands: 36 | ```bash 37 | git clone --recursive https://github.com/vimpostor/quickcurver.git 38 | # If you forgot to clone with --recursive, just run git submodule update --init 39 | cd quickcurver 40 | cmake -B build 41 | cmake --build build 42 | ``` 43 | 44 | To start QuickCurver you need to run the built executable in the `build` directory, for example on Linux run: `build/quickcurver` 45 | 46 | ## Installing compiled binaries 47 | 48 | ### Windows 49 | Download the precompiled binary from the [latest stable release](https://github.com/vimpostor/quickcurver/releases/latest). 50 | Extract all files and run `QuickCurver.exe` in the `release` directory. 51 | 52 | # Multiplayer 53 | To play multiplayer, the host starts an instance and shares the port that QuickCurver is running on. The client then just has to connect to this port on the host's ip address. 54 | If you are not in the same local network, the host most likely has to use [Port Forwarding](https://en.wikipedia.org/wiki/Port_forwarding) to make his device available to the internet. 55 | If a firewall is the problem, you might also want to take a look at [Hole Punching](https://en.wikipedia.org/wiki/Hole_punching_(networking)). 56 | 57 | If network performance isn't good, the Server can tweak the "Network update rate" value in the settings, which causes data to be sent less frequently which may improve the network performance at the cost of update frequency. (A higher value means worse quality, but better network performance) 58 | 59 | If you want to host Quickcurver cleanly on a separate server and do not need the GUI, you can start it with the CLI parameter `-platform offscreen`. 60 | -------------------------------------------------------------------------------- /android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /assets/quickcurver.desktop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xdg-open 2 | [Desktop Entry] 3 | Type=Application 4 | Name=QuickCurver 5 | Comment=A Qt implementation of Curve Fever 6 | Exec=quickcurver 7 | Terminal=false 8 | Categories=Game; 9 | Icon=quickcurver 10 | -------------------------------------------------------------------------------- /assets/quickcurver.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 51 | 55 | 59 | 63 | 67 | 68 | -------------------------------------------------------------------------------- /doc/NETWORK.md: -------------------------------------------------------------------------------- 1 | # Quick Curver - Multiplayer - Specification 2 | 3 | ## Abstract 4 | 5 | This document defines the Quick Curver multiplayer. 6 | It describes how multiple instances of Quick Curver communicate in order 7 | to provide a fluent online multiplayer. 8 | 9 | ## Introduction 10 | 11 | Quick Curver uses TCP and UDP to communicate with different instances. 12 | TCP is used for most packet types, but UDP is used for the broadcasting 13 | of Curver data due to performance reasons. 14 | 15 | ## Conventions and Definitions 16 | 17 | The words "MUST", "MUST NOT", "SHOULD", "REQUIRED", "RECOMMENDED", 18 | "OPTIONAL" and "MAY" are used in this document. These keywords are to be 19 | understood as defined in [RFC2119]. 20 | 21 | Client: The instance instantiating a Quick Curver connection request. 22 | 23 | Server: The instance listening for incoming connection requests. 24 | 25 | DISCONNECT: The event on closing the TCP connection between server and 26 | client. 27 | 28 | ## Overview 29 | 30 | The actual game is run on the server. All logic is computed on the 31 | server and on the server only. The client only sends keyboard inputs to 32 | the server, which makes it impossible to cheat as long as the server 33 | runs a sane version of Quick Curver. 34 | The client gets back all visual data, which is computed by the server, 35 | which includes data for curvers, item positions etc... 36 | 37 | An online game is opened from an initial local game and only then a 38 | server is started. The server listens for incoming client connections 39 | and adds players to the local running game on demand by its own. 40 | 41 | A client requests a connection simply by sending a TCP connection 42 | request to the server. If the server accepts the connection, the client 43 | must send a Ping packet via UDP to the server. The server replies with a 44 | Pong packet and only then the player has successfully joined the game. 45 | 46 | Any party may at any time close the TCP socket, which closes the session 47 | for the client. 48 | 49 | If the server receives a DISCONNECT from the client, this is interpreted 50 | as the client purposefully leaving the game. The client may at any time 51 | rejoin the game. 52 | 53 | If the client receives a DISCONNECT from the server, this is interpreted 54 | as the server purposefully kicking the client from the game. The client 55 | MAY try to immediately reconnect, however the server MAY as well block 56 | future incoming connections from the client, if it did in fact 57 | purposefully kick the client. 58 | 59 | Once a stable connection has been established, there are different 60 | phases of the game: 61 | 62 | * Waiting for the game to start 63 | * The actual game being played 64 | 65 | Server and client may exchange chat messages at any time. 66 | Chat messages are sent by either instance to a server. The server then 67 | distributes the chat message to every client instance. 68 | 69 | ## Packet Types 70 | 71 | Quick Curver uses a common packet type across all packets, that 72 | encapsulates all packets in one common format. 73 | 74 | The format of all packets is defined by the following outline: Each 75 | packet begins with the following 1-byte header: 76 | 77 | ``` 78 | 0 1 79 | 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 80 | -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- 81 | |T|T|T|U|U|U|R|S| ..... 82 | -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- 83 | ``` 84 | 85 | The first three bits denote the packet type (T). The next 5 bits define 86 | special flags. The following table defines how the flags are interpreted: 87 | 88 | ``` 89 | ------------------ 90 | | Flag | Meaning | 91 | |----------------| 92 | | S | Start | 93 | | R | Reset | 94 | | U | Unused | 95 | ------------------ 96 | ``` 97 | 98 | The interpretation of the type information depends on whether the packet 99 | was sent by a server or a client. The fact whether the sender is a 100 | server or a client is simply deduced by the fact whether the instance 101 | itself is a server or a client. The following table defines how the type 102 | information is interpreted: 103 | 104 | ``` 105 | ------------------------------------------------------- 106 | | Type | Binary | Sent from server | Sent from client | 107 | |-----------------------------------------------------| 108 | | 0 | 000 | Chat Message | Chat Message | 109 | | 1 | 001 | Playermodel Edit | Playermodel Edit | 110 | | 2 | 010 | Curver Data | Curver Rotation | 111 | | 3 | 011 | Item Data | Ping | 112 | | 4 | 100 | Settings | ---------------- | 113 | | 5 | 101 | Pong | ---------------- | 114 | ------------------------------------------------------- 115 | ``` 116 | 117 | The following section defines every single packet by its own. 118 | Each packet already comes with the general 1-byte header at the very 119 | beginning. The following only describes what comes after the first byte. 120 | 121 | ### Chat Message from server 122 | 123 | Username and message as QString. 124 | 125 | ### Chat Message from client 126 | The client simply sends the message as a serialized QString. The server 127 | knows the username of the client already, so there is no need to send 128 | the username. 129 | 130 | ### Playermodel Edit from server 131 | 132 | The server sends every setting bundled to every client. 133 | This means in particular that it first sends the number of players as an 134 | unsigned integer, and after that for each player the following properties (in 135 | that order): 136 | 137 | * Username 138 | * Color 139 | * Round score 140 | * Total score 141 | * Controller 142 | * Alive 143 | 144 | ### Playermodel Edit from client 145 | 146 | The player sends any change to the server and the server determines 147 | what curver the player belongs to and changes the settings accordingly. 148 | After that, the server sends back the new settings to all clients. 149 | The settings that the client sends are (in this order): 150 | 151 | * Username 152 | * Color 153 | 154 | ### Curver Data from server 155 | 156 | First the id of the curver that the segment belongs to, then a new 157 | segment flag (set or unset), then the amount of points, then the segment data. 158 | 159 | ### Curver Rotation from client 160 | 161 | The client sends the new rotation direction of the curver that is 162 | controlled by this client. A 1-byte integer is sufficient for this 163 | purpose, so an uint8_t is simply serialized, where the different values 164 | are interpreted as follows: 165 | 166 | ``` 167 | -------------------- 168 | | Value | Rotation | 169 | |------------------| 170 | | 0 | Left | 171 | | 1 | None | 172 | | 2 | Right | 173 | -------------------- 174 | ``` 175 | 176 | Any other value than the above mentioned is undefined behaviour. 177 | 178 | ### Item Data from server 179 | 180 | The server sends the following data (in this order): 181 | 182 | * spawned (Item spawned or triggered) 183 | * Sequence Number (unsigned int) 184 | * Index of the kind of item 185 | * Position as QPointF 186 | * Allowed users as uint8_t 187 | * index of collector as int (if spawned, this must be -1) 188 | 189 | ### Ping packet from client 190 | 191 | The client sends the current time and the currently estimated ping. 192 | If the client didn't receive a Pong from the server yet, then the estimated ping 193 | SHOULD be `0`. 194 | The estimated ping MUST be a signed 64-bit integer representing milliseconds. 195 | 196 | ### Settings from server 197 | 198 | The server sends the following data: 199 | 200 | * Game dimension as QPoint 201 | 202 | ### Pong packet from server 203 | 204 | The server sends the current time as received from the corresponding Ping 205 | packet. The client MUST calculate the ping by computing the time difference 206 | between now and the time of the Ping relayed over this Pong packet. 207 | The server also sends the index of the player that it responds to and 208 | it additionally sends all estimated pings from all clients. 209 | -------------------------------------------------------------------------------- /doc/man/man1/quickcurver.1: -------------------------------------------------------------------------------- 1 | .TH "quickcurver" 1 "02 May 2022" "redacted@redacted.com" "quickcurver Documentation" 2 | 3 | .SH NAME 4 | quickcurver \- A Curve Fever implementation in Qt 5 | 6 | .SH SYNOPSIS 7 | .B quickcurver 8 | [\-h] 9 | 10 | .SH DESCRIPTION 11 | 12 | .P 13 | This is an implementation of Curve Fever (also known as Achtung die Kurve) in Qt. 14 | 15 | .TP 16 | .B \-h 17 | Show usage information. 18 | 19 | .SH EXIT STATUS 20 | Returns zero on success. 21 | 22 | .SH NOTES 23 | The quickcurver project site, with more information and the source code repository, can be found at https://github.com/vimpostor/quickcurver. This program is currently under development, please report any bugs at the project site or directly to the author. 24 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1725634671, 6 | "narHash": "sha256-v3rIhsJBOMLR8e/RNWxr828tB+WywYIoajrZKFM+0Gg=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "574d1eac1c200690e27b8eb4e24887f8df7ac27c", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "quartz": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | }, 23 | "locked": { 24 | "lastModified": 1725898343, 25 | "narHash": "sha256-sKKfBP7/Nyh3Y6V/C8L6wgCbLipvldxQspsgFbmp+6c=", 26 | "owner": "vimpostor", 27 | "repo": "quartz", 28 | "rev": "19fc6131b6ac09693cdb0562c9e2bc6495140bdc", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "vimpostor", 33 | "repo": "quartz", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "quartz": "quartz" 40 | } 41 | } 42 | }, 43 | "root": "root", 44 | "version": 7 45 | } 46 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Modern Qt/C++ implementation of Achtung die Kurve with online multiplayer"; 3 | inputs = { 4 | quartz.url = "github:vimpostor/quartz"; 5 | }; 6 | 7 | outputs = { self, quartz }: quartz.lib.eachSystem (system: 8 | let 9 | pkgs = quartz.inputs.nixpkgs.legacyPackages.${system}; 10 | in 11 | { 12 | packages = { 13 | default = pkgs.stdenv.mkDerivation { 14 | pname = "quickcurver"; 15 | version = quartz.lib.cmakeProjectVersion ./CMakeLists.txt; 16 | 17 | src = ./.; 18 | 19 | nativeBuildInputs = with pkgs; [ 20 | cmake 21 | pkg-config 22 | qt6.wrapQtAppsHook 23 | imagemagick 24 | ]; 25 | buildInputs = with pkgs; [ 26 | qt6.qtbase 27 | qt6.qtdeclarative 28 | qt6.qtsvg 29 | ]; 30 | cmakeFlags = quartz.lib.cmakeWrapper { inherit pkgs; cmakeFile = ./CMakeLists.txt; }; 31 | postBuild = "make linux-desktop-integration"; 32 | }; 33 | }; 34 | } 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /scripts/AppRun: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SELF="$(readlink -f "$0")" 4 | HERE="${SELF%/*}" 5 | export PATH="${HERE}/usr/bin/:/usr/bin/:$PATH" 6 | export LD_LIBRARY_PATH="${HERE}/usr/lib/:/usr/lib/:$LD_LIBRARY_PATH" 7 | export QT_PLUGIN_PATH="${HERE}/usr/lib/qt6/plugins/:$QT_PLUGIN_PATH" 8 | EXEC=$(grep -e '^Exec=.*' "${HERE}"/*.desktop | head -n 1 | cut -d "=" -f 2 | cut -d " " -f 1) 9 | exec "$EXEC" "$@" 10 | -------------------------------------------------------------------------------- /scripts/appimage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This script assumes that a cmake build ran already with build directory "build" 3 | 4 | APPIMAGETOOL="/tmp/appimagetool" 5 | APPDIR="/tmp/appdir" 6 | 7 | cmake --build build --target linux-desktop-integration 8 | # create an appdir 9 | DESTDIR="$APPDIR" cmake --install build 10 | # copy Qt libraries 11 | mkdir -p "$APPDIR/usr/lib" 12 | readelf -d build/quickcurver | while IFS= read -r LINE; do 13 | if [[ "$LINE" == *libQt6*.so* ]]; then 14 | LIB="$(echo "$LINE" | grep -oE 'libQt6.*.so[^]]*')" 15 | cp -L "/usr/lib/$LIB" "$APPDIR/usr/lib/" 16 | fi 17 | done 18 | # more hardcoded libraries 19 | cp -L "/usr/lib/libQt6XcbQpa.so.6" "$APPDIR/usr/lib/" 20 | mkdir -p "$APPDIR/usr/lib/qt6/plugins" 21 | cp -rT "/usr/lib/qt6/plugins" "$APPDIR/usr/lib/qt6/plugins" 22 | 23 | # Create AppRun 24 | cp scripts/AppRun "$APPDIR/" 25 | 26 | # Create desktop file 27 | ln -sf "$APPDIR/usr/share/applications/quickcurver.desktop" "$APPDIR/" 28 | 29 | # Create icon 30 | ln -sf "$APPDIR/usr/share/icons/hicolor/512x512/apps/quickcurver.png" "$APPDIR/" 31 | 32 | # download appimagetool 33 | curl -qL 'https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage' -o "$APPIMAGETOOL" 34 | chmod +x "$APPIMAGETOOL" 35 | $APPIMAGETOOL "$APPDIR" 36 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cmake -B build -DCMAKE_INSTALL_PREFIX=/usr 6 | cmake --build build 7 | -------------------------------------------------------------------------------- /scripts/format-code.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | git ls-files| grep -E '.*\.[ch]pp$'| xargs clang-format -style=file -i 4 | 5 | # return only with EXIT_SUCCESS if there were no changes 6 | # otherwise show the changes and return with error code 7 | STATUS="$(git status -s)" 8 | if [ -n "$STATUS" ]; then 9 | git --no-pager diff && false 10 | fi 11 | -------------------------------------------------------------------------------- /src/backend.cpp: -------------------------------------------------------------------------------- 1 | #include "backend.hpp" 2 | 3 | bool Backend::is_mobile() { 4 | #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) 5 | return true; 6 | #else 7 | return false; 8 | #endif 9 | } 10 | -------------------------------------------------------------------------------- /src/backend.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Backend : public QObject { 6 | Q_OBJECT 7 | QML_ELEMENT 8 | QML_SINGLETON 9 | 10 | Q_PROPERTY(bool isMobile READ is_mobile CONSTANT) 11 | public: 12 | bool is_mobile(); 13 | }; 14 | -------------------------------------------------------------------------------- /src/bot.cpp: -------------------------------------------------------------------------------- 1 | #include "bot.hpp" 2 | 3 | #define LOOK_AHEAD 1000 4 | #define MAX_ANGLE M_PI / 4 5 | #define ANGLE_STEP M_PI / 24 6 | #define WALL_MARGIN 100 7 | 8 | /** 9 | * @brief Asks the Bot to make a move for a given Curver 10 | * @param c The Curver to make a move for 11 | */ 12 | void Bot::makeMove(Curver &c) { 13 | // TODO: Use checkForIntersection to precompute future positions of other curvers and take them into account 14 | // Otherwise bots "race" with other curvers heads right next to them 15 | // good default value, if we don't want to change anything, we can just return 16 | c.rotation = Curver::Rotation::ROTATE_NONE; 17 | auto &curvers = PlayerModel::get()->getCurvers(); 18 | QPointF p = c.getPos(); 19 | QPointF dir = c.getDirection(); 20 | const QPointF dim = Settings::get()->getDimension(); 21 | const float v = c.velocity; 22 | const bool aboutToCollide = c.checkForIntersection(curvers, p, p + LOOK_AHEAD * v * dir); 23 | // are we going straight for a wall? 24 | if (p.x() < WALL_MARGIN || p.y() < WALL_MARGIN || 25 | p.x() > dim.x() - WALL_MARGIN || p.y() > dim.y() - WALL_MARGIN) { 26 | // choose the shorter rotation away from the wall 27 | if (QPointF::dotProduct(p, dim / 2) > 0) { 28 | c.rotation = Curver::Rotation::ROTATE_LEFT; 29 | } else { 30 | c.rotation = Curver::Rotation::ROTATE_RIGHT; 31 | } 32 | } 33 | // else find a way to dodge 34 | const float angle = c.getAngle(); 35 | float angleOffset = ANGLE_STEP / 2; 36 | float leftAngleDanger = 0, rightAngleDanger = 0; 37 | // find out how far danger is in both directions 38 | while ((leftAngleDanger * rightAngleDanger == 0) && angleOffset < MAX_ANGLE) { 39 | if (leftAngleDanger == 0) { 40 | const auto leftAngle = angle - angleOffset; 41 | if (c.checkForIntersection(curvers, p, p + LOOK_AHEAD * v * QPointF(cos(leftAngle), sin(leftAngle)))) { 42 | leftAngleDanger = angleOffset; 43 | } 44 | } 45 | if (rightAngleDanger == 0) { 46 | const auto rightAngle = angle + angleOffset; 47 | if (c.checkForIntersection(curvers, p, p + LOOK_AHEAD * v * QPointF(cos(rightAngle), sin(rightAngle)))) { 48 | rightAngleDanger = angleOffset; 49 | } 50 | } 51 | angleOffset += ANGLE_STEP; 52 | } 53 | // if no danger is found, set danger distance to something huge 54 | if (leftAngleDanger == 0) { 55 | leftAngleDanger = MAX_ANGLE; 56 | } 57 | if (rightAngleDanger == 0) { 58 | rightAngleDanger = MAX_ANGLE; 59 | } 60 | const float minDangerAngle = std::min(leftAngleDanger, rightAngleDanger); 61 | // only do something if danger REALLY is imminent 62 | if (aboutToCollide || (minDangerAngle < MAX_ANGLE)) { 63 | // choose the direction with danger the farthest away 64 | if (leftAngleDanger > rightAngleDanger) { 65 | c.rotation = Curver::Rotation::ROTATE_LEFT; 66 | } else { 67 | c.rotation = Curver::Rotation::ROTATE_RIGHT; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/bot.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "models/playermodel.hpp" 4 | #include "settings.hpp" 5 | 6 | /** 7 | * @brief A class representing an AI that controls a Curver 8 | */ 9 | class Bot { 10 | public: 11 | Bot() = delete; 12 | static void makeMove(Curver &c); 13 | }; 14 | -------------------------------------------------------------------------------- /src/cleaninstallanimation.cpp: -------------------------------------------------------------------------------- 1 | #include "cleaninstallanimation.hpp" 2 | 3 | #define ANIMATION_DURATION 300 4 | 5 | /** 6 | * @brief Triggers the cleaninstall animation 7 | * @param newSegments The current state of segments in the Curver object 8 | * 9 | * This will take ownership of all segments from the Curver object. 10 | * This method automatically removes all segments from the Curver object. 11 | */ 12 | void CleaninstallAnimation::trigger(std::vector> &newSegments) { 13 | if (newSegments.size() == 0) { 14 | // do not spawn an animation, if there is nothing to animate 15 | return; 16 | } 17 | for (auto it = newSegments.begin(); it != newSegments.end(); ++it) { 18 | segments.push_back(std::move(*it)); 19 | } 20 | newSegments.clear(); 21 | // calculate total amount of points 22 | sizeCache.resize(segments.size()); 23 | pointsDeleted.resize(segments.size()); 24 | for (size_t i = 0; i < segments.size(); ++i) { 25 | sizeCache[i] = segments[i]->getSegmentSize(); 26 | pointsDeleted[i] = 0; 27 | } 28 | totalSize = Util::accumulate(sizeCache, 0); 29 | initialTime = QTime::currentTime(); 30 | } 31 | 32 | /** 33 | * @brief Updates the animation 34 | */ 35 | void CleaninstallAnimation::progress() { 36 | if (initialTime.isNull()) { 37 | return; 38 | } 39 | const float timeSinceStart = initialTime.msecsTo(QTime::currentTime()); 40 | const float factor = timeSinceStart / ANIMATION_DURATION; 41 | const float easedFactor = Util::easeInOutSine(factor); 42 | const size_t pos = totalSize * easedFactor; 43 | if (factor > 1) { 44 | segments.clear(); 45 | sizeCache.clear(); 46 | totalSize = 0; 47 | initialTime = QTime(); 48 | return; 49 | } 50 | size_t pointsToDelete = pos; 51 | size_t segmentIndex; 52 | for (segmentIndex = 0; segmentIndex < sizeCache.size() && pointsToDelete >= sizeCache[segmentIndex]; ++segmentIndex) { 53 | pointsToDelete -= sizeCache[segmentIndex]; 54 | segments[segmentIndex]->clear(); 55 | pointsDeleted[segmentIndex] = sizeCache[segmentIndex]; 56 | } 57 | if (segmentIndex >= segments.size()) { 58 | // whoops, out of bounds, so no explosion for us today 59 | return; 60 | } 61 | 62 | const auto pointsAlreadyDeleted = pointsToDelete; 63 | pointsToDelete -= pointsDeleted[segmentIndex]; 64 | // remove points in an interval making an explosion every time 65 | constexpr size_t explosionInterval = 16; 66 | for (; pointsToDelete >= explosionInterval; pointsToDelete -= explosionInterval) { 67 | if (const auto p = segments[segmentIndex]->getFirstPos()) { 68 | spawnExplosion(p.value()); 69 | } 70 | segments[segmentIndex]->popPoints(explosionInterval); 71 | } 72 | // remove the remaining few points without explosion 73 | segments[segmentIndex]->popPoints(pointsToDelete); 74 | pointsDeleted[segmentIndex] = pointsAlreadyDeleted; 75 | } 76 | -------------------------------------------------------------------------------- /src/cleaninstallanimation.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "segment.hpp" 10 | #include "util.hpp" 11 | 12 | /** 13 | * @brief An animated event representing a Curver cleaninstall 14 | * 15 | * Takes ownership of all segments of the old Curver object 16 | */ 17 | class CleaninstallAnimation : public QObject { 18 | Q_OBJECT 19 | public: 20 | void trigger(std::vector> &newSegments); 21 | void progress(); 22 | signals: 23 | /** 24 | * @brief Request to spawn an explosion 25 | * @param location The position of the explosion 26 | */ 27 | void spawnExplosion(QPointF location); 28 | private: 29 | /** 30 | * @brief The point in time that the animation was triggered with trigger() 31 | */ 32 | QTime initialTime; 33 | /** 34 | * @brief The old segments to be faded out 35 | * 36 | * trigger() will automatically take ownership of the old segments from a Curver object 37 | */ 38 | std::vector> segments; 39 | /** 40 | * @brief A cache representing the size of the segments at the point of the animation begin 41 | */ 42 | std::vector sizeCache; 43 | /** 44 | * @brief A cache representing the amount of points already deleted for a given segment 45 | */ 46 | std::vector pointsDeleted; 47 | /** 48 | * @brief The total size over all segments 49 | */ 50 | size_t totalSize; 51 | }; 52 | -------------------------------------------------------------------------------- /src/commandlinereader.cpp: -------------------------------------------------------------------------------- 1 | #include "commandlinereader.hpp" 2 | 3 | /** 4 | * @brief Constructs a CommandlineReader object 5 | * @param parent The parent object 6 | */ 7 | CommandlineReader::CommandlineReader(QObject *parent) 8 | : QObject(parent) { 9 | } 10 | 11 | /** 12 | * @brief Calls run() asynchronously 13 | */ 14 | void CommandlineReader::runAsync() { 15 | auto future = QtConcurrent::run([&]() { this->run(); }); 16 | } 17 | 18 | /** 19 | * @brief Reads lines from stdin. 20 | * 21 | * This function returns, when the user inputs "/quit" into the terminal. 22 | */ 23 | void CommandlineReader::run() { 24 | QTextStream stdInput(stdin); 25 | QString line; 26 | bool cancel = false; 27 | while (!cancel && stdInput.readLineInto(&line)) { 28 | // parse the command 29 | if (!line.startsWith("/")) { 30 | qInfo() << "Commands start with /"; 31 | } else { 32 | const auto split = line.split(' ', Qt::SkipEmptyParts); 33 | auto parts = std::list(split.begin(), split.end()); 34 | if (parts.empty()) { 35 | qInfo() << "Please enter a nonempty command"; 36 | } else { 37 | const auto command = parts.begin()->mid(1); 38 | parts.pop_front(); 39 | if (command == "addbot") { 40 | int amount; 41 | if (!takeInt(amount, parts, "You can also specify the number of bots to add")) { 42 | amount = 1; 43 | } 44 | for (int i = 0; i < amount; ++i) { 45 | addBot(); 46 | } 47 | } else if (command == "chat") { 48 | QString message; 49 | if (takeString(message, parts, "Chat message")) { 50 | chat(message); 51 | } 52 | } else if (command == "help") { 53 | qInfo() << "/start starts the game"; 54 | } else if (command == "itemspawn") { 55 | int index; 56 | float prob; 57 | if (takeInt(index, parts, "Index") && takeFloat(prob, parts, "Probability")) { 58 | itemSpawn(index, prob); 59 | } 60 | } else if (command == "itemwait") { 61 | int min, max; 62 | if (takeInt(min, parts, "Min") && takeInt(max, parts, "Max")) { 63 | itemWait(min, max); 64 | } 65 | } else if (command == "listen") { 66 | int port; 67 | if (takeInt(port, parts, "Pass the port")) { 68 | listen(port); 69 | } 70 | } else if (command == "logicupdate") { 71 | int rate; 72 | if (takeInt(rate, parts, "Logic update rate")) { 73 | logicUpdate(rate); 74 | } 75 | } else if (command == "networkupdate") { 76 | int rate; 77 | if (takeInt(rate, parts, "Network update rate")) { 78 | networkUpdate(rate); 79 | } 80 | } else if (command == "quit") { 81 | cancel = true; 82 | quit(); 83 | } else if (command == "remove") { 84 | int index; 85 | if (takeInt(index, parts, "index")) { 86 | remove(index); 87 | } 88 | } else if (command == "removebots") { 89 | removeBots(); 90 | } else if (command == "reset") { 91 | reset(); 92 | } else if (command == "resize") { 93 | int width, height; 94 | if (takeInt(width, parts, "width") && takeInt(height, parts, "height")) { 95 | resize(QPoint(width, height)); 96 | } 97 | } else if (command == "start") { 98 | start(); 99 | } else if (command == "score") { 100 | int score; 101 | if (takeInt(score, parts, "Score to reach")) { 102 | targetScore(score); 103 | } 104 | } else { 105 | qInfo() << "Unknown command. See /help for more info."; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * @brief Runs some generic checks on a list 114 | * @param l The list to check 115 | * @return Whether the list passed the test 116 | */ 117 | bool CommandlineReader::checkList(const std::list &l) { 118 | return !l.empty() && l.cbegin()->size() > 0; 119 | } 120 | 121 | /** 122 | * @brief Returns the first parameter from the list as float 123 | * @param result The first parameter interpreted as float 124 | * @param l The parameter list 125 | * @param info An info text about the parameter 126 | * @return Whether the operation was successful 127 | */ 128 | bool CommandlineReader::takeFloat(float &result, std::list &l, QString info) { 129 | bool ok = false; 130 | if (checkList(l)) { 131 | QString p = l.front(); 132 | l.pop_front(); 133 | result = p.toFloat(&ok); 134 | } 135 | if (!ok) { 136 | qInfo() << info; 137 | } 138 | return ok; 139 | } 140 | 141 | /** 142 | * @brief Returns the first parameter from the list as int 143 | * @param result The first parameter interpreted as int 144 | * @param l The parameter list 145 | * @param info An info text about the parameter 146 | * @return Whether the operation was successful 147 | */ 148 | bool CommandlineReader::takeInt(int &result, std::list &l, QString info) { 149 | bool ok = false; 150 | if (checkList(l)) { 151 | QString p = l.front(); 152 | l.pop_front(); 153 | result = p.toInt(&ok); 154 | } 155 | if (!ok) { 156 | qInfo() << info; 157 | } 158 | return ok; 159 | } 160 | 161 | /** 162 | * @brief Returns the first parameter from the list as QString 163 | * @param result The first parameter interpreted as QString 164 | * @param l The parameter list 165 | * @param info An info text about the parameter 166 | * @return Whether the operation was successful 167 | */ 168 | bool CommandlineReader::takeString(QString &result, std::list &l, QString info) { 169 | bool ok = false; 170 | if (checkList(l)) { 171 | result = l.front(); 172 | l.pop_front(); 173 | ok = !result.isEmpty(); 174 | } 175 | if (!ok) { 176 | qInfo() << info; 177 | } 178 | return ok; 179 | } 180 | -------------------------------------------------------------------------------- /src/commandlinereader.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /** 9 | * @brief This class is able to read from the command line in an asynchronous way 10 | */ 11 | class CommandlineReader : public QObject { 12 | Q_OBJECT 13 | public: 14 | explicit CommandlineReader(QObject *parent = nullptr); 15 | void runAsync(); 16 | void run(); 17 | 18 | signals: 19 | /** 20 | * @brief The user wants to add a bot 21 | */ 22 | void addBot(); 23 | /** 24 | * @brief The user wants to broadcast a chat message 25 | * @param message The message to broadcast 26 | */ 27 | void chat(QString message); 28 | /** 29 | * @brief The user wants to set item spawn probabilities 30 | * @param index The index of the item 31 | * @param prob The probability of the item to spawn 32 | */ 33 | void itemSpawn(int index, float prob); 34 | /** 35 | * @brief The user wants to set the delay between item spawns 36 | * @param min The minimum time to wait in milliseconds 37 | * @param max The maximum time to wait in milliseconds 38 | */ 39 | void itemWait(int min, int max); 40 | /** 41 | * @brief The user wants the server to listen on a new port 42 | * @param port The port to listen on 43 | */ 44 | void listen(quint16 port); 45 | /** 46 | * @brief The user wants to change the logic update rate 47 | * @param rate The new rate in updates/second 48 | * 49 | * Note: This must be set, before the game is started. 50 | */ 51 | void logicUpdate(int rate); 52 | /** 53 | * @brief The user wants to change the network update rate 54 | * @param rate The new update rate 55 | */ 56 | void networkUpdate(int rate); 57 | /** 58 | * @brief The user wants to quit the program 59 | */ 60 | void quit(); 61 | /** 62 | * @brief Remove a player from the game 63 | * @param index The player to remove 64 | */ 65 | void remove(int index); 66 | /** 67 | * @brief Remove all bots 68 | */ 69 | void removeBots(); 70 | /** 71 | * @brief The user wants to reset the game 72 | */ 73 | void reset(); 74 | /** 75 | * @brief The user wants to resize the game 76 | * @param dimension The new dimension 77 | */ 78 | void resize(QPoint dimension); 79 | /** 80 | * @brief The user wants to start the game 81 | */ 82 | void start(); 83 | /** 84 | * @brief The user wants to change the target score 85 | * @param targetScore The score that once reached determines the winner 86 | */ 87 | void targetScore(int targetScore); 88 | private: 89 | bool checkList(const std::list &l); 90 | bool takeFloat(float &result, std::list &l, QString info); 91 | bool takeInt(int &result, std::list &l, QString info); 92 | bool takeString(QString &result, std::list &l, QString info); 93 | }; 94 | -------------------------------------------------------------------------------- /src/curver.cpp: -------------------------------------------------------------------------------- 1 | #include "curver.hpp" 2 | 3 | #define SPAWN_WALL_THRESHOLD 50 4 | #define SPAWN_INVINCIBLE_DURATION 1000 5 | #define CLEAN_INVINCIBLE_DURATION 100 6 | #define SEGMENT_USE_TIME_MIN 5000 7 | #define SEGMENT_USE_TIME_MAX 8000 8 | #define SEGMENT_CHANGE_TIME 300 9 | 10 | /** 11 | * @brief Constructs a Curver object that belongs to \a parentNode in the scene graph 12 | * @param parentNode The parent node in the scene graph 13 | */ 14 | Curver::Curver(QSGNode *parentNode) { 15 | this->parentNode = parentNode; 16 | 17 | nextSegmentEvent = QTime::currentTime(); 18 | setColor(Util::randColor()); 19 | // random initial rotation 20 | headNode = std::make_unique(parentNode, &material); 21 | resetRound(); 22 | 23 | connect(&cleaninstallAnimation, &CleaninstallAnimation::spawnExplosion, std::bind(&Curver::spawnExplosion, this, std::placeholders::_1, 0.3)); 24 | } 25 | 26 | Curver::~Curver() { 27 | } 28 | 29 | /** 30 | * @brief Sets the color of the Curver 31 | * @param color The new color 32 | */ 33 | void Curver::setColor(const QColor color) { 34 | this->color = color; 35 | material.setColor(color); 36 | } 37 | 38 | /** 39 | * @brief Returns the color of the Curver 40 | * @return The color 41 | */ 42 | QColor Curver::getColor() const { 43 | return this->color; 44 | } 45 | 46 | /** 47 | * @brief Sets the left key of the Curver 48 | * 49 | * This key is used to rotate counter clockwise 50 | * @param key The left key 51 | */ 52 | void Curver::setLeftKey(const Qt::Key key) { 53 | leftKey = key; 54 | } 55 | 56 | /** 57 | * @brief Returns the left key 58 | * @return The left key 59 | */ 60 | Qt::Key Curver::getLeftKey() const { 61 | return this->leftKey; 62 | } 63 | 64 | /** 65 | * @brief Sets the right key of the Curver 66 | * 67 | * This key is used to rotate clockwise 68 | * @param key The right key 69 | */ 70 | void Curver::setRightKey(const Qt::Key key) { 71 | rightKey = key; 72 | } 73 | 74 | /** 75 | * @brief Returns the right key 76 | * @return The right key (not the wrong one! :) ) 77 | */ 78 | Qt::Key Curver::getRightKey() const { 79 | return this->rightKey; 80 | } 81 | 82 | /** 83 | * @brief Returns the segments of this Curver 84 | * @return The segments 85 | */ 86 | const std::vector> &Curver::getSegments() const { 87 | return this->segments; 88 | } 89 | 90 | /** 91 | * @brief Returns the current position 92 | * @return The current position 93 | */ 94 | QPointF Curver::getPos() const { 95 | return this->lastPos; 96 | } 97 | 98 | /** 99 | * @brief Returns the current direction vector 100 | * @return The direction 101 | */ 102 | QPointF Curver::getDirection() const { 103 | return this->direction; 104 | } 105 | 106 | /** 107 | * @brief Returns the current angle 108 | * @return The angle 109 | */ 110 | float Curver::getAngle() const { 111 | return angle; 112 | } 113 | 114 | /** 115 | * @brief Determines if the Curver is currently changing segments 116 | * @return \c True, iif changing segments at the moment 117 | */ 118 | bool Curver::isChangingSegment() const { 119 | return this->changingSegment; 120 | } 121 | 122 | /** 123 | * @brief Processes the given key 124 | * 125 | * If the key is responsible for controlling the Curver, this triggers a rotation accordingly. 126 | * @param key The key to process 127 | * @param release If the key was pressed or released 128 | */ 129 | void Curver::processKey(Qt::Key key, bool release) { 130 | if (controller != Controller::CONTROLLER_LOCAL || ((key != leftKey) && (key != rightKey))) { 131 | return; 132 | } 133 | if (release) { 134 | rotation = Rotation::ROTATE_NONE; 135 | } else if (key == leftKey) { 136 | rotation = Rotation::ROTATE_LEFT; 137 | } else if (key == rightKey) { 138 | rotation = Rotation::ROTATE_RIGHT; 139 | } 140 | } 141 | 142 | /** 143 | * @brief Notify that the game has started 144 | */ 145 | void Curver::start() { 146 | resetRound(); 147 | } 148 | 149 | /** 150 | * @brief Updates the Curver assuming that \a deltat has gone by 151 | * @param deltat The amount of time since the last update in milliseconds 152 | * @param curvers All curvers 153 | */ 154 | void Curver::progress(int deltat, std::vector> &curvers) { 155 | // update all explosions 156 | std::ranges::for_each(explosions, [](auto &i) { i->progress(); }); 157 | cleaninstallAnimation.progress(); 158 | if (!isAlive()) { 159 | return; 160 | } 161 | 162 | if (nextSegmentEvent <= QTime::currentTime()) { 163 | if (changingSegment) { 164 | // spawn a new segment 165 | segments.push_back(std::make_unique(parentNode, &material, thickness)); 166 | prepareSegmentEvent(false, SEGMENT_USE_TIME_MIN, SEGMENT_USE_TIME_MAX); 167 | } else { 168 | // plan a new segment spawn 169 | prepareSegmentEvent(true, SEGMENT_CHANGE_TIME, SEGMENT_CHANGE_TIME); 170 | } 171 | } 172 | secondLastPos = lastPos; 173 | if (rotation == Rotation::ROTATE_LEFT) { 174 | rotate(-deltat * rotateVelocity); 175 | } else if (rotation == Rotation::ROTATE_RIGHT) { 176 | rotate(deltat * rotateVelocity); 177 | } 178 | lastPos += deltat * static_cast(velocity) * direction; 179 | if (headVisible) { 180 | headNode->setPosition(lastPos); 181 | } else { 182 | // it is not possible to have both head invisible and not changing segment 183 | // check for collision 184 | if (checkForIntersection(curvers, secondLastPos, lastPos)) { 185 | die(); 186 | } 187 | } 188 | if (!changingSegment) { 189 | segments.back()->appendPoint(lastPos, angle); 190 | // check for collision 191 | if (checkForIntersection(curvers, secondLastPos, lastPos)) { 192 | die(); 193 | } 194 | } 195 | 196 | checkForWall(); 197 | } 198 | 199 | /** 200 | * @brief Checks, if any Curver collides with the line from \a a to \a b 201 | * @param curvers All Curvers 202 | * @param a The start point of the line 203 | * @param b The end point of the line 204 | * @return \c True, iif the line collides 205 | */ 206 | bool Curver::checkForIntersection(std::vector> &curvers, QPointF a, QPointF b) const { 207 | for (auto &i : curvers) { 208 | const auto &otherSegments = i->getSegments(); 209 | if (otherSegments.end() != std::ranges::find_if(otherSegments, [&](auto &segment) { return segment->checkForIntersection(a, b); })) { 210 | return true; 211 | } 212 | } 213 | return false; 214 | } 215 | 216 | /** 217 | * @brief Check, if the Curver collides with a wall 218 | * 219 | * Triggers Curver::die(), if it does in fact collide 220 | */ 221 | void Curver::checkForWall() { 222 | QPoint dimension = Settings::get()->getDimension(); 223 | if (alive && !changingSegment && (lastPos.x() < 0 || lastPos.x() > dimension.x() || lastPos.y() < 0 || lastPos.y() > dimension.y())) { 224 | die(); 225 | } 226 | } 227 | 228 | /** 229 | * @brief Triggers a cleaninstall of the Curver 230 | * 231 | * Erases all previous segments and immediately inits a new segment. 232 | */ 233 | void Curver::cleanInstall() { 234 | prepareSegmentEvent(true, CLEAN_INVINCIBLE_DURATION, CLEAN_INVINCIBLE_DURATION); 235 | // spawn cleaninstall animation and remove segments with it 236 | cleaninstallAnimation.trigger(segments); 237 | } 238 | 239 | /** 240 | * @brief Increases the score by one 241 | */ 242 | void Curver::increaseScore() { 243 | ++roundScore; 244 | ++totalScore; 245 | } 246 | 247 | /** 248 | * @brief Resets the round 249 | */ 250 | void Curver::resetRound() { 251 | explosions.clear(); 252 | segments.clear(); 253 | // random start position 254 | QPoint dimension = Settings::get()->getDimension(); 255 | lastPos = QPointF(Util::randInt(SPAWN_WALL_THRESHOLD, dimension.x() - SPAWN_WALL_THRESHOLD), Util::randInt(SPAWN_WALL_THRESHOLD, dimension.y() - SPAWN_WALL_THRESHOLD)); 256 | rotate(Util::rand() * 2 * M_PI); 257 | prepareSegmentEvent(true, SPAWN_INVINCIBLE_DURATION, SPAWN_INVINCIBLE_DURATION); 258 | roundScore = 0; 259 | alive = true; 260 | headVisible = true; 261 | } 262 | 263 | /** 264 | * @brief Changes the alive state 265 | * @param alive Whether this Curver is alive 266 | */ 267 | void Curver::setAlive(const bool alive) { 268 | if (isAlive() && !alive) { 269 | die(); 270 | } else { 271 | this->alive = alive; 272 | } 273 | } 274 | 275 | /** 276 | * @brief Determines if the Curver is alive 277 | * @return \c True, iif alive 278 | */ 279 | bool Curver::isAlive() const { 280 | return alive; 281 | } 282 | 283 | /** 284 | * @brief Appends a point to the Curver 285 | * @param pos The point to append 286 | * @param changingSegment Whether the Curver is changing segments at the moment 287 | */ 288 | void Curver::appendPoint(const QPointF pos, const bool changingSegment) { 289 | // segments.size() test fixes a bug, where a client would crash due to network lag 290 | if (!changingSegment && (oldChangingSegment || segments.size() == 0)) { 291 | segments.push_back(std::make_unique(parentNode, &material, thickness)); 292 | } 293 | if (headVisible) { 294 | headNode->setPosition(pos); 295 | } 296 | if (!changingSegment && pos != lastPos) { 297 | const QPointF diff = pos - lastPos; 298 | const double length = sqrt(QPointF::dotProduct(diff, diff)); 299 | float angle = acos(diff.x() / length); 300 | if (diff.y() < 0) { 301 | angle = -1 * angle; 302 | } 303 | Q_ASSERT(segments.size() > 0); 304 | segments.back()->appendPoint(pos, angle); 305 | } 306 | oldChangingSegment = changingSegment; 307 | lastPos = pos; 308 | } 309 | 310 | /** 311 | * @brief Prepares a segment event. 312 | * 313 | * Immediately forces the Curver to adapt the \a changingSegment value and plans a segment spawn at a random time from \a lower t0 \a upper. 314 | * @param changingSegment Whether the Curver should change segments right now 315 | * @param lower The lower random boundary of the segment event 316 | * @param upper The upper random boundary of the segment event 317 | */ 318 | void Curver::prepareSegmentEvent(bool changingSegment, int lower, int upper) { 319 | nextSegmentEvent = QTime::currentTime().addMSecs(Util::randInt(lower, upper)); 320 | this->changingSegment = changingSegment; 321 | } 322 | 323 | /** 324 | * @brief Spawns an explosion animation at the given position 325 | * @param location The position of the explosion 326 | * @param radius The size of the explosion 327 | */ 328 | void Curver::spawnExplosion(QPointF location, float radius) { 329 | explosions.emplace_back(std::make_unique(location, parentNode, &material, this, radius)); 330 | } 331 | 332 | /** 333 | * @brief Rotates the Curver 334 | * @param radians The amount of radian to rotate 335 | */ 336 | void Curver::rotate(float radians) { 337 | angle += radians; 338 | if (angle < 0) { 339 | angle += 2 * M_PI; 340 | } else if (angle > 2 * M_PI) { 341 | angle -= 2 * M_PI; 342 | } 343 | direction.setX(cos(angle)); 344 | direction.setY(sin(angle)); 345 | } 346 | 347 | /** 348 | * @brief Triggers the death of the Curver 349 | * 350 | * Spawns an Explosion at the current location. 351 | */ 352 | void Curver::die() { 353 | alive = false; 354 | spawnExplosion(lastPos); 355 | died(); 356 | } 357 | 358 | /** 359 | * @brief Compares two Curver objects 360 | * @param l The left Curver 361 | * @param r The right Curver 362 | * @return \c True, iif \a l is less than \a r, determined by whether the total score is less. 363 | */ 364 | bool operator<(const std::unique_ptr &l, const std::unique_ptr &r) { 365 | return l->totalScore < r->totalScore; 366 | } 367 | -------------------------------------------------------------------------------- /src/curver.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "cleaninstallanimation.hpp" 9 | #include "explosion.hpp" 10 | #include "headnode.hpp" 11 | #include "segment.hpp" 12 | #include "settings.hpp" 13 | 14 | /** 15 | * @brief The Curver class represents a player and all the segments belonging to the player 16 | * 17 | * This class stores every data of a player including all segments. 18 | */ 19 | class Curver : public QObject { 20 | Q_OBJECT 21 | public: 22 | /** 23 | * @brief Determines the rotation of a Curver object 24 | */ 25 | enum class Rotation { 26 | ROTATE_LEFT, 27 | ROTATE_NONE, 28 | ROTATE_RIGHT 29 | }; 30 | /** 31 | * @brief Determines the controller of a Curver object 32 | */ 33 | enum class Controller { 34 | CONTROLLER_LOCAL, 35 | CONTROLLER_REMOTE, 36 | CONTROLLER_BOT 37 | }; 38 | 39 | explicit Curver(QSGNode *parentNode); 40 | ~Curver(); 41 | 42 | void setColor(const QColor color); 43 | QColor getColor() const; 44 | void setLeftKey(const Qt::Key key); 45 | Qt::Key getLeftKey() const; 46 | void setRightKey(const Qt::Key key); 47 | Qt::Key getRightKey() const; 48 | const std::vector> &getSegments() const; 49 | QPointF getPos() const; 50 | QPointF getDirection() const; 51 | float getAngle() const; 52 | bool isChangingSegment() const; 53 | 54 | void processKey(Qt::Key key, bool release = false); 55 | void start(); 56 | void progress(int deltat, std::vector> &curvers); 57 | bool checkForIntersection(std::vector> &curvers, QPointF a, QPointF b) const; 58 | void checkForWall(); 59 | void cleanInstall(); 60 | void increaseScore(); 61 | void resetRound(); 62 | void setAlive(const bool alive); 63 | bool isAlive() const; 64 | void appendPoint(const QPointF pos, const bool changingSegment); 65 | void prepareSegmentEvent(bool changingSegment, int lower, int upper); 66 | void spawnExplosion(QPointF location, float radius = 1.0); 67 | 68 | /** 69 | * @brief The username of the Curver 70 | */ 71 | QString userName = "Player"; 72 | /** 73 | * @brief The amount of points gained during the current round 74 | */ 75 | int roundScore = 0; 76 | /** 77 | * @brief The amount of total points gained 78 | */ 79 | int totalScore = 0; 80 | /** 81 | * @brief The current rotation 82 | */ 83 | Rotation rotation = Rotation::ROTATE_NONE; 84 | /** 85 | * @brief The current controller of this Curver 86 | */ 87 | Controller controller = Controller::CONTROLLER_LOCAL; 88 | /** 89 | * @brief The current velocity in pixels per millisecond 90 | */ 91 | float velocity = 0.125; 92 | /** 93 | * @brief The rotational velocity in radian per millisecond 94 | */ 95 | float rotateVelocity = 0.0039062f; 96 | /** 97 | * @brief Determines if the head is visible at the moment 98 | */ 99 | bool headVisible = true; 100 | /** 101 | * The ping to the server of this player. 102 | * This is only available in online games. 103 | */ 104 | qint64 ping = 0; 105 | signals: 106 | /** 107 | * @brief Emitted, when the Curver died due to collision 108 | */ 109 | void died(); 110 | private slots: 111 | private: 112 | void rotate(float radians); 113 | void die(); 114 | 115 | /** 116 | * @brief The parent node in the scene graph 117 | */ 118 | QSGNode *parentNode; 119 | /** 120 | * @brief The color of this Curver 121 | */ 122 | QColor color = Qt::black; 123 | /** 124 | * @brief The material in use with the color determined by Curver::color 125 | */ 126 | QSGFlatColorMaterial material; 127 | /** 128 | * @brief A vector containing all segments of this Curver 129 | */ 130 | std::vector> segments; 131 | /** 132 | * @brief The node representing the head of this Curver 133 | */ 134 | std::unique_ptr headNode; 135 | /** 136 | * @brief The current line thickness 137 | */ 138 | float thickness = 4; 139 | /** 140 | * @brief The vector pointing at the direction that this Curver is heading at 141 | */ 142 | QPointF direction; 143 | /** 144 | * @brief The angle of the current rotation in radian 145 | */ 146 | float angle = 0; 147 | /** 148 | * @brief The current position 149 | */ 150 | QPointF lastPos = QPointF(0, 0); 151 | /** 152 | * @brief The position before Curver::lastPos 153 | */ 154 | QPointF secondLastPos; 155 | /** 156 | * @brief The key triggering the counter clock-wise rotation 157 | */ 158 | Qt::Key leftKey = Qt::Key_Left; 159 | /** 160 | * @brief The key triggering the clock-wise rotation 161 | */ 162 | Qt::Key rightKey = Qt::Key_Right; 163 | /** 164 | * @brief Determines, whether the Curver is changing segments at the moment 165 | */ 166 | bool changingSegment = false; 167 | /** 168 | * @brief The time of the next planned segment event 169 | * 170 | * A segment event can be the spawn of a new segment or leaving the current segment. 171 | */ 172 | QTime nextSegmentEvent; 173 | /** 174 | * @brief Decides whether the Curver is alive at the moment 175 | */ 176 | bool alive = true; 177 | /** 178 | * @brief Decides whether the Curver was previously changing the segment 179 | * 180 | * This property is used only by the Client to decide, when to trigger segment events 181 | * 182 | * Unused by Server. 183 | */ 184 | bool oldChangingSegment = true; 185 | /** 186 | * @brief An object responsible for the cleaninstall animation 187 | * 188 | * Note, that this object will take ownership of all segments when triggered 189 | */ 190 | CleaninstallAnimation cleaninstallAnimation; 191 | /** 192 | * @brief A vector holding all explosions for this Curver 193 | */ 194 | std::vector> explosions; 195 | }; 196 | 197 | bool operator<(const std::unique_ptr &l, const std::unique_ptr &r); 198 | -------------------------------------------------------------------------------- /src/explosion.cpp: -------------------------------------------------------------------------------- 1 | #include "explosion.hpp" 2 | 3 | /** 4 | * @brief Creates an explosion 5 | * @param location The location to spawn at 6 | * @param parentNode The parent node in the scene graph 7 | * @param material The material to use for drawing calls 8 | * @param parent The parent object 9 | * @param radius The size of the explosion 10 | */ 11 | Explosion::Explosion(QPointF location, QSGNode *parentNode, QSGFlatColorMaterial *material, QObject *parent, float radius) { 12 | this->location = location; 13 | this->parentNode = parentNode; 14 | 15 | opacityNode = std::make_unique(); 16 | geoNode = std::make_unique(); 17 | geometry.setLineWidth(PARTICLESIZE); 18 | geometry.setDrawingMode(QSGGeometry::DrawLines); 19 | geoNode->setGeometry(&geometry); 20 | geoNode->setMaterial(material); 21 | 22 | /** 23 | * We need to allocate two vertices for each line. 24 | * The first point for each line is always in the origin. 25 | * The second point starts at the origin, but moves outwards over time. 26 | */ 27 | geometry.allocate(2 * PARTICLECOUNT); 28 | vertices = geometry.vertexDataAsPoint2D(); 29 | for (int i = 0; i < PARTICLECOUNT; ++i) { 30 | vertices[2 * i].set(location.x(), location.y()); 31 | vertices[2 * i + 1].set(location.x(), location.y()); 32 | particleDirections[i] = radius * (Util::randQPointF() - QPointF(0.5, 0.5)); 33 | } 34 | opacityNode->appendChildNode(geoNode.get()); 35 | parentNode->appendChildNode(opacityNode.get()); 36 | } 37 | 38 | Explosion::~Explosion() { 39 | parentNode->removeChildNode(opacityNode.get()); 40 | opacityNode->removeChildNode(geoNode.get()); 41 | } 42 | 43 | /** 44 | * @brief Updates the explosion each frame 45 | */ 46 | void Explosion::progress() { 47 | float timeSinceStart = initialTime.msecsTo(QTime::currentTime()); 48 | opacityNode->setOpacity(std::max(0.f, 1 - timeSinceStart / PARTICLELIFETIME)); 49 | opacityNode->markDirty(QSGNode::DirtyOpacity); 50 | if (timeSinceStart > PARTICLELIFETIME) { 51 | // TODO: Improve lifetime of explosion, right now we only delete them at the end of each round 52 | return; 53 | } else { 54 | for (int i = 0; i < PARTICLECOUNT; ++i) { 55 | vertices[2 * i + 1].set(location.x() + PARTICLERANGE * particleDirections[i].x() * timeSinceStart / PARTICLELIFETIME, 56 | location.y() + PARTICLERANGE * particleDirections[i].y() * timeSinceStart / PARTICLELIFETIME); 57 | } 58 | geoNode->markDirty(QSGNode::DirtyGeometry); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/explosion.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "util.hpp" 9 | 10 | #define PARTICLECOUNT 64 11 | #define PARTICLESIZE 2 12 | #define PARTICLERANGE 256 13 | #define PARTICLELIFETIME 400 14 | 15 | /** 16 | * @brief A class representing a Curver explosion 17 | * 18 | * This class is used, when a Curver died 19 | */ 20 | class Explosion : public QObject { 21 | Q_OBJECT 22 | public: 23 | explicit Explosion(QPointF location, QSGNode *parentNode, QSGFlatColorMaterial *material, QObject *parent = nullptr, float radius = 1.0); 24 | ~Explosion(); 25 | void progress(); 26 | private: 27 | /** 28 | * @brief The location of the explosion's origin 29 | */ 30 | QPointF location; 31 | /** 32 | * @brief The parent node in the scene graph 33 | */ 34 | QSGNode *parentNode; 35 | /** 36 | * @brief The node responsible for fading the explosion out 37 | */ 38 | std::unique_ptr opacityNode; 39 | /** 40 | * @brief The node containg the actual explosion 41 | */ 42 | std::unique_ptr geoNode; 43 | /** 44 | * @brief The geometry of the explosion 45 | */ 46 | QSGGeometry geometry = QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); 47 | /** 48 | * @brief The time of the explosion start 49 | */ 50 | QTime initialTime = QTime::currentTime(); 51 | /** 52 | * @brief A pointer to the actual geometry data 53 | */ 54 | QSGGeometry::Point2D *vertices; 55 | /** 56 | * @brief The directions of each individual particle 57 | */ 58 | QPointF particleDirections[PARTICLECOUNT]; 59 | }; 60 | -------------------------------------------------------------------------------- /src/game.cpp: -------------------------------------------------------------------------------- 1 | #include "game.hpp" 2 | 3 | /** 4 | * @brief Constructs a Game with the given parent. 5 | * 6 | * \a parent is used to draw everything onto the GUI window. 7 | * @param parent The parent to draw on 8 | */ 9 | Game::Game(QQuickItem *parent) 10 | : QQuickItem(parent) { 11 | /* Create a new root node. No, this does not leak memory, the Qt scene graph automatically deletes this node at the end. 12 | * The scene graph knows this root node from updatePaintNode and will destroy it, when the window closes. 13 | * Do NOT under any circumstances delete this node, or otherwise Qt will try to double free it, 14 | * which will result in a big Qt backtrace when the window closes. 15 | * Trust me, you do not want to debug this again. 16 | */ 17 | rootNode = new QSGNode(); 18 | itemFactory = std::make_unique(rootNode); 19 | // notify the item factory about window changes 20 | connect(this, &QQuickItem::windowChanged, itemFactory.get(), &ItemFactory::setWindow); 21 | // tell the playermodel, what the root node is, so that it can tell its curvers 22 | PlayerModel::get()->setRootNode(this->rootNode); 23 | connect(PlayerModel::get(), &PlayerModel::curverDied, this, &Game::curverDied); 24 | connect(PlayerModel::get(), &PlayerModel::playerModelChanged, &server, &Server::broadcastPlayerModel); 25 | connect(ItemModel::get(), &ItemModel::itemSpawned, &server, &Server::broadcastItemData); 26 | wall.setParentNode(rootNode); 27 | connect(&client, &Client::integrateItem, itemFactory.get(), &ItemFactory::integrateItem); 28 | connect(&client, &Client::resetRound, this, &Game::triggerResetRound); 29 | connect(&client, &Client::updateGraphics, this, &QQuickItem::update); 30 | // GUI signals 31 | connect(&Gui::getSingleton(), &Gui::postInfoBar, this, &Game::postInfoBar); 32 | connect(&Gui::getSingleton(), &Gui::startGame, this, &Game::tryStartGame); 33 | 34 | connect(&gameTimer, &QTimer::timeout, this, &Game::progress); 35 | // tell QtQuick, that this component wants to draw stuff 36 | setFlag(ItemHasContents); 37 | } 38 | 39 | Game::~Game() { 40 | // we do manual memory management with every child node, so we have to remove every child node to prevent a double free 41 | rootNode->removeAllChildNodes(); 42 | } 43 | 44 | /** 45 | * @brief Starts the game 46 | */ 47 | void Game::startGame() { 48 | tryStartGame(); 49 | lastProgressTime = QTime::currentTime(); 50 | std::ranges::for_each(getCurvers(), [](const std::unique_ptr &c) { c->start(); }); 51 | itemFactory->resetRound(); 52 | // 60 FPS = 16 ms interval 53 | gameTimer.start(static_cast(1000.f / Settings::get()->getUpdatesPerSecond())); 54 | } 55 | 56 | /** 57 | * @brief Processes a key 58 | * 59 | * Checks if this key was registered to trigger any rotation changes. 60 | * @param key The key to process 61 | * @param release Whether the key was pressed or released 62 | */ 63 | void Game::processKey(Qt::Key key, bool release) { 64 | std::ranges::for_each(getCurvers(), [&](auto &c) { c->processKey(key, release); }); 65 | if (getClient()->getJoinStatus() == Client::JoinStatus::JOINED) { 66 | client.processKey(key, release); 67 | } 68 | } 69 | 70 | /** 71 | * @brief Connects as a client to the given host 72 | * @param ip The IP address of the host 73 | * @param port The port that the host is listening on 74 | */ 75 | void Game::connectToHost(QString ip, int port) { 76 | client.connectToHost(ip, port); 77 | } 78 | 79 | /** 80 | * @brief Sends a chat message 81 | * 82 | * If this instance is the Server of a game, the chat message will be broadcasted to all clients. 83 | * @param msg The message to send 84 | */ 85 | void Game::sendChatMessage(QString msg) { 86 | if (getClient()->getJoinStatus() == Client::JoinStatus::JOINED) { 87 | client.sendChatMessage(msg); 88 | } else { 89 | server.broadcastChatMessage(msg); 90 | } 91 | } 92 | 93 | /** 94 | * @brief Reconfigures the server to listen on a different port 95 | * @param port The port to listen on 96 | */ 97 | void Game::serverReListen(quint16 port) { 98 | server.reListen(port); 99 | } 100 | 101 | /** 102 | * @brief Resets the entire game 103 | */ 104 | void Game::resetGame() { 105 | triggerResetRound(); 106 | std::ranges::for_each(getCurvers(), [](const auto &c) { c->totalScore = 0; }); 107 | winnerAnnounced = false; 108 | PlayerModel::get()->forceRefresh(); 109 | } 110 | 111 | /** 112 | * @brief Returns the Client belonging to this Game 113 | * @return The Client 114 | */ 115 | Client *Game::getClient() { 116 | return &client; 117 | } 118 | 119 | /** 120 | * @brief Called by the scene graph. This is called before the screen is redrawn. 121 | * @return Always return Game::rootNode 122 | */ 123 | QSGNode *Game::updatePaintNode(QSGNode *, QQuickItem::UpdatePaintNodeData *) { 124 | // check if round should be reset 125 | if (triggerReset) { 126 | resetRound(); 127 | } 128 | 129 | int deltat = Util::getTimeDiff(lastProgressTime); 130 | lastProgressTime = QTime::currentTime(); 131 | for (auto &c : getCurvers()) { 132 | if (c->isAlive()) { 133 | if (c->controller == Curver::Controller::CONTROLLER_BOT) { 134 | Bot::makeMove(*c.get()); 135 | } 136 | } 137 | c->progress(deltat, getCurvers()); 138 | } 139 | itemFactory->update(); 140 | rootNode->markDirty(QSGNode::DirtyStateBit::DirtyGeometry); 141 | 142 | return rootNode; 143 | } 144 | 145 | /** 146 | * @brief Updates the game's logic respecting how much time actually passed by 147 | */ 148 | void Game::progress() { 149 | update(); 150 | server.broadcastCurverData(); 151 | } 152 | 153 | /** 154 | * @brief Called, when a curver died 155 | * 156 | * This method taskes care of the score board and checks if a new round is due. 157 | */ 158 | void Game::curverDied() { 159 | std::ranges::for_each(getCurvers(), [](const auto &c) { if (c->isAlive()) c->increaseScore(); }); 160 | auto maxScorer = std::ranges::max_element(getCurvers()); 161 | if ((*maxScorer)->totalScore >= Settings::get()->getTargetScore() && !winnerAnnounced) { 162 | // we have a winner 163 | winnerAnnounced = true; 164 | server.broadcastChatMessage((*maxScorer)->userName + " won!"); 165 | } 166 | // check if only one player is remaining 167 | if (!resetPending && std::ranges::count_if(getCurvers(), [](const auto &c) { return c->isAlive(); }) < 2) { 168 | resetPending = true; 169 | resetRoundTimer.singleShot(Settings::get()->getRoundTimeOut(), this, &Game::triggerResetRound); 170 | } 171 | } 172 | 173 | /** 174 | * @brief Resets the round 175 | */ 176 | void Game::resetRound() { 177 | itemFactory->resetRound(); 178 | std::ranges::for_each(getCurvers(), [](const auto &c) { c->resetRound(); }); 179 | server.resetRound(); 180 | resetPending = false; 181 | triggerReset = false; 182 | } 183 | 184 | /** 185 | * @brief Sets a flag so that the next update event iteration will reset the round 186 | * 187 | * We cannot just immediately reset, because a round reset will touch some nodes, which must only happen inside the render thread. 188 | */ 189 | void Game::triggerResetRound() { 190 | this->triggerReset = true; 191 | } 192 | 193 | /** 194 | * @brief Tries to start the game. 195 | * 196 | * This method does nothing, if the game already started. 197 | */ 198 | void Game::tryStartGame() { 199 | if (!started) { 200 | started = true; 201 | gameStarted(); 202 | } 203 | } 204 | 205 | /** 206 | * @brief A helper method to get a vector of all curvers 207 | * @return A vector containing all curvers 208 | */ 209 | std::vector> &Game::getCurvers() { 210 | return PlayerModel::get()->getCurvers(); 211 | } 212 | -------------------------------------------------------------------------------- /src/game.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "bot.hpp" 13 | #include "curver.hpp" 14 | #include "gui.hpp" 15 | #include "itemfactory.hpp" 16 | #include "models/playermodel.hpp" 17 | #include "network/client.hpp" 18 | #include "network/server.hpp" 19 | #include "wall.hpp" 20 | 21 | /** 22 | * @brief A class representing an entire game 23 | * 24 | * This class handles a game and manages all the things involved with it such as resetting rounds and updating the game each frame. 25 | */ 26 | class Game : public QQuickItem { 27 | Q_OBJECT 28 | 29 | Q_PROPERTY(Client *client READ getClient() CONSTANT) 30 | Q_PROPERTY(bool isStarted MEMBER started NOTIFY gameStarted) 31 | public: 32 | explicit Game(QQuickItem *parent = 0); 33 | ~Game(); 34 | 35 | Q_INVOKABLE void startGame(); 36 | Q_INVOKABLE void processKey(Qt::Key key, bool release); 37 | Q_INVOKABLE void connectToHost(QString ip, int port); 38 | Q_INVOKABLE void sendChatMessage(QString msg); 39 | Q_INVOKABLE void serverReListen(quint16 port); 40 | Q_INVOKABLE void resetGame(); 41 | Q_INVOKABLE Client *getClient(); 42 | 43 | QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *); 44 | public slots: 45 | signals: 46 | /** 47 | * @brief Emitted, when something wants to post the GUI infobar 48 | * @param msg The message to display 49 | */ 50 | void postInfoBar(QString msg); 51 | /** 52 | * @brief Emitted, when the game started 53 | */ 54 | void gameStarted(); 55 | private slots: 56 | void progress(); 57 | void curverDied(); 58 | void resetRound(); 59 | void triggerResetRound(); 60 | void tryStartGame(); 61 | private: 62 | std::vector> &getCurvers(); 63 | 64 | /** 65 | * @brief The timer responsible for the game main loop 66 | */ 67 | QTimer gameTimer; 68 | /** 69 | * @brief The time of the last game logic update 70 | */ 71 | QTime lastProgressTime; 72 | /** 73 | * @brief The timer responsible for resetting the round after all players died 74 | */ 75 | QTimer resetRoundTimer; 76 | /** 77 | * @brief The root node in the scene graph 78 | */ 79 | QSGNode *rootNode; 80 | /** 81 | * @brief The item factory of this game 82 | */ 83 | std::unique_ptr itemFactory; 84 | /** 85 | * @brief A node representing the wall 86 | */ 87 | Wall wall; 88 | /** 89 | * @brief The server instance 90 | */ 91 | Server server; 92 | /** 93 | * @brief The client instance 94 | */ 95 | Client client; 96 | /** 97 | * @brief Whether the game already started 98 | */ 99 | bool started = false; 100 | /** 101 | * @brief Whether the winner was already announced 102 | */ 103 | bool winnerAnnounced = false; 104 | /** 105 | * @brief Whether a round reset is currently pending in the queue 106 | */ 107 | bool resetPending = false; 108 | /** 109 | * @brief Whether a round reset must be triggered in the next update event loop iteration 110 | */ 111 | bool triggerReset = false; 112 | }; 113 | -------------------------------------------------------------------------------- /src/gamewatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "gamewatcher.hpp" 2 | 3 | /** 4 | * @brief Constructs a GameWatcher object and connects all signals of the commandline reader with slots related to the Game. 5 | * @param parent The parent object 6 | */ 7 | GameWatcher::GameWatcher(QObject *parent) 8 | : QObject(parent) { 9 | connect(&cliReader, &CommandlineReader::addBot, PlayerModel::get(), &PlayerModel::appendBot); 10 | connect(&cliReader, &CommandlineReader::chat, &game, &Game::sendChatMessage); 11 | connect(&cliReader, &CommandlineReader::itemSpawn, ItemModel::get(), &ItemModel::setProbability); 12 | connect(&cliReader, &CommandlineReader::itemWait, [](int min, int max) { Settings::get()->setItemSpawnIntervalMin(min); Settings::get()->setItemSpawnIntervalMax(max); }); 13 | connect(&cliReader, &CommandlineReader::listen, &game, &Game::serverReListen); 14 | connect(&cliReader, &CommandlineReader::logicUpdate, Settings::get(), &Settings::setUpdatesPerSecond); 15 | connect(&cliReader, &CommandlineReader::networkUpdate, Settings::get(), &Settings::setNetworkCurverBlock); 16 | connect(&cliReader, &CommandlineReader::quit, this, &GameWatcher::quit, Qt::QueuedConnection); 17 | // TODO: Remove player must call the slot in Server 18 | connect(&cliReader, &CommandlineReader::remove, PlayerModel::get(), &PlayerModel::removePlayer); 19 | connect(&cliReader, &CommandlineReader::removeBots, PlayerModel::get(), &PlayerModel::removeBots); 20 | connect(&cliReader, &CommandlineReader::reset, &game, &Game::resetGame); 21 | connect(&cliReader, &CommandlineReader::resize, Settings::get(), &Settings::setDimension); 22 | connect(&cliReader, &CommandlineReader::start, &game, &Game::startGame); 23 | connect(&cliReader, &CommandlineReader::targetScore, Settings::get(), &Settings::setTargetScore); 24 | 25 | // copy ingame chat to terminal 26 | connect(ChatModel::get(), &ChatModel::newMessage, this, &GameWatcher::printChatMessage); 27 | } 28 | 29 | /** 30 | * @brief Starts the GameWatcher by parsing the commandline input. 31 | */ 32 | void GameWatcher::start() { 33 | cliReader.runAsync(); 34 | } 35 | 36 | /** 37 | * @brief Quits the whole operation and the program. 38 | */ 39 | void GameWatcher::quit() { 40 | QCoreApplication::quit(); 41 | } 42 | 43 | /** 44 | * @brief Prints a chat message 45 | * @param username The author of the chat message 46 | * @param message The message content 47 | */ 48 | void GameWatcher::printChatMessage(QString username, QString message) { 49 | qInfo() << username << message; 50 | } 51 | -------------------------------------------------------------------------------- /src/gamewatcher.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "commandlinereader.hpp" 6 | #include "game.hpp" 7 | 8 | /** 9 | * @brief Watches a CommandlineReader to interact with the game. This is the CLI implementation of the game. 10 | */ 11 | class GameWatcher : public QObject { 12 | Q_OBJECT 13 | public: 14 | explicit GameWatcher(QObject *parent = nullptr); 15 | void start(); 16 | private slots: 17 | void quit(); 18 | void printChatMessage(QString username, QString message); 19 | private: 20 | /** 21 | * @brief The commandline reader interface 22 | */ 23 | CommandlineReader cliReader; 24 | /** 25 | * @brief The Game object 26 | */ 27 | Game game; 28 | }; 29 | -------------------------------------------------------------------------------- /src/gui.cpp: -------------------------------------------------------------------------------- 1 | #include "gui.hpp" 2 | 3 | /** 4 | * @brief Returns the Gui singleton 5 | * @return The Gui singleton 6 | */ 7 | const Gui &Gui::getSingleton() { 8 | static Gui result; 9 | return result; 10 | } 11 | -------------------------------------------------------------------------------- /src/gui.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | /** 6 | * @brief A centralized place for notifying the GUI about something 7 | */ 8 | class Gui : public QObject { 9 | Q_OBJECT 10 | public: 11 | static const Gui &getSingleton(); 12 | signals: 13 | /** 14 | * @brief Emitted, when someone wants to post the infobar 15 | * @param msg The message to display 16 | */ 17 | void postInfoBar(QString msg) const; 18 | /** 19 | * @brief Emitted, when the game started 20 | */ 21 | void startGame() const; 22 | }; 23 | -------------------------------------------------------------------------------- /src/headnode.cpp: -------------------------------------------------------------------------------- 1 | #include "headnode.hpp" 2 | 3 | /** 4 | * @brief Constructs a HeadNode 5 | * @param parentNode The parent node in the scene graph 6 | * @param material The material to use for this node 7 | */ 8 | HeadNode::HeadNode(QSGNode *parentNode, QSGFlatColorMaterial *material) 9 | : QSGGeometryNode() { 10 | this->parentNode = parentNode; 11 | 12 | geometry.setDrawingMode(QSGGeometry::DrawTriangleStrip); 13 | this->setGeometry(&geometry); 14 | this->setMaterial(material); 15 | geometry.allocate(4); 16 | parentNode->appendChildNode(this); 17 | } 18 | 19 | HeadNode::~HeadNode() { 20 | parentNode->removeChildNode(this); 21 | } 22 | 23 | /** 24 | * @brief Updates the location 25 | * @param newPos The new location 26 | */ 27 | void HeadNode::setPosition(const QPointF newPos) { 28 | constexpr float pointSize = 2.f; 29 | this->pos = newPos; 30 | QSGGeometry::Point2D *vertices = geometry.vertexDataAsPoint2D(); 31 | vertices[0].set(pos.x() - pointSize, pos.y() - pointSize); 32 | vertices[1].set(pos.x() - pointSize, pos.y() + pointSize); 33 | vertices[2].set(pos.x() + pointSize, pos.y() - pointSize); 34 | vertices[3].set(pos.x() + pointSize, pos.y() + pointSize); 35 | this->markDirty(QSGNode::DirtyGeometry); 36 | } 37 | -------------------------------------------------------------------------------- /src/headnode.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /** 9 | * @brief A node representing the head of a Curver 10 | */ 11 | class HeadNode : public QSGGeometryNode { 12 | public: 13 | HeadNode(QSGNode *parentNode, QSGFlatColorMaterial *material); 14 | ~HeadNode(); 15 | 16 | void setPosition(const QPointF newPos); 17 | private: 18 | /** 19 | * @brief The parent node in the scene graph 20 | */ 21 | QSGNode *parentNode; 22 | /** 23 | * @brief The geometry used for the node 24 | */ 25 | QSGGeometry geometry = QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); 26 | /** 27 | * @brief The current location 28 | */ 29 | QPointF pos; 30 | }; 31 | -------------------------------------------------------------------------------- /src/itemfactory.cpp: -------------------------------------------------------------------------------- 1 | #include "itemfactory.hpp" 2 | 3 | #define SPAWN_WALL_THRESHOLD 20 4 | 5 | /** 6 | * @brief Constructs a ItemFactory 7 | * @param parentNode The parent node in the scene graph 8 | * @param parent The parent object 9 | */ 10 | ItemFactory::ItemFactory(QSGNode *parentNode, QObject *parent) 11 | : QObject(parent) { 12 | this->parentNode = parentNode; 13 | } 14 | 15 | /** 16 | * @brief Resets the round causing all Item instances to disappear 17 | */ 18 | void ItemFactory::resetRound() { 19 | items.clear(); 20 | std::ranges::for_each(usedItems, [](auto &i) { i->defuse(); }); 21 | usedItems.clear(); 22 | prepareNextItem(); 23 | } 24 | 25 | /** 26 | * @brief Updates the ItemFactory to check whether a new Item should spawn 27 | * 28 | * Also checks if any Curver triggers an Item 29 | */ 30 | void ItemFactory::update() { 31 | if (nextItemSpawn.isValid() && QTime::currentTime() >= nextItemSpawn) { 32 | spawnItem(); 33 | prepareNextItem(); 34 | } 35 | checkCollisions(); 36 | 37 | // update all items 38 | std::ranges::for_each(items, [](auto &i) { i->update(); }); 39 | std::ranges::for_each(usedItems, [](auto &i) { i->update(); }); 40 | } 41 | 42 | /** 43 | * @brief Integrates a predetermined Item event into the ItemFactory 44 | * @param spawned Whether the Item spawned or was triggered 45 | * @param sequenceNumber The unique sequence number of the Item 46 | * @param which The kind of Item 47 | * @param pos The location of the Item 48 | * @param allowedUsers The allowed users of the Item 49 | * @param collectorIndex If \a spawned is \c false, this determines the collecting Curver 50 | */ 51 | void ItemFactory::integrateItem(bool spawned, unsigned int sequenceNumber, int which, QPointF pos, Item::AllowedUsers allowedUsers, int collectorIndex) { 52 | if (spawned) { 53 | // add the new spawned item 54 | items.emplace_back(std::unique_ptr(ItemModel::get()->makePredefinedItem(parentNode, which, pos, allowedUsers, window))); 55 | items.back()->sequenceNumber = sequenceNumber; 56 | } else { 57 | auto it = std::ranges::find_if(items, [&](auto &i) { return i->sequenceNumber == sequenceNumber; }); 58 | if (it != items.end()) { 59 | if (collectorIndex != -1) { 60 | (*it)->trigger(PlayerModel::get()->getCurvers()[collectorIndex]); 61 | } 62 | items.erase(it); 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * @brief Sets the new window 69 | * @param w The new window 70 | */ 71 | void ItemFactory::setWindow(QQuickWindow *w) { 72 | window = w; 73 | } 74 | 75 | /** 76 | * @brief Prepares a new Item spawn 77 | */ 78 | void ItemFactory::prepareNextItem() { 79 | nextItemSpawn = QTime::currentTime().addMSecs(Util::randInt(Settings::get()->getItemSpawnIntervalMin(), Settings::get()->getItemSpawnIntervalMax())); 80 | } 81 | 82 | /** 83 | * @brief Spawns a new Item 84 | */ 85 | void ItemFactory::spawnItem() { 86 | QPoint dimension = Settings::get()->getDimension(); 87 | items.emplace_back(std::unique_ptr(ItemModel::get()->makeRandomItem(parentNode, QPointF(Util::randInt(SPAWN_WALL_THRESHOLD, dimension.x() - SPAWN_WALL_THRESHOLD), Util::randInt(SPAWN_WALL_THRESHOLD, dimension.y() - SPAWN_WALL_THRESHOLD)), window))); 88 | } 89 | 90 | /** 91 | * @brief Checks if any Curver is in range of any Item and triggers it accordingly 92 | */ 93 | void ItemFactory::checkCollisions() { 94 | auto itemIt = items.begin(); 95 | while (itemIt != items.end()) { 96 | auto curverIt = std::ranges::find_if(PlayerModel::get()->getCurvers(), [&itemIt](auto &curver) { return (*itemIt)->isInRange(curver->getPos()); }); 97 | if (curverIt != PlayerModel::get()->getCurvers().end()) { 98 | // trigger item 99 | ItemModel::get()->itemSpawned(false, (*itemIt)->sequenceNumber, 0, QPointF(), Item::AllowedUsers::ALLOW_ALL, curverIt - PlayerModel::get()->getCurvers().begin()); 100 | (*itemIt)->trigger(*curverIt); 101 | usedItems.emplace_back(std::move(*itemIt)); 102 | itemIt = items.erase(itemIt); 103 | } else { 104 | ++itemIt; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/itemfactory.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "items/cleaninstallitem.hpp" 8 | #include "items/speeditem.hpp" 9 | #include "models/itemmodel.hpp" 10 | #include "models/playermodel.hpp" 11 | #include "settings.hpp" 12 | #include "util.hpp" 13 | 14 | /** 15 | * @brief This class plans and manages all Item spawns 16 | */ 17 | class ItemFactory : public QObject { 18 | Q_OBJECT 19 | public: 20 | explicit ItemFactory(QSGNode *parentNode, QObject *parent = nullptr); 21 | 22 | void resetRound(); 23 | void update(); 24 | public slots: 25 | void integrateItem(bool spawned, unsigned int sequenceNumber, int which, QPointF pos, Item::AllowedUsers allowedUsers, int collectorIndex); 26 | void setWindow(QQuickWindow *w); 27 | signals: 28 | private: 29 | void prepareNextItem(); 30 | void spawnItem(); 31 | void checkCollisions(); 32 | 33 | /** 34 | * @brief The parent node in the scene graph 35 | */ 36 | QSGNode *parentNode; 37 | /** 38 | * @brief The time of the next Item spawn 39 | */ 40 | QTime nextItemSpawn; 41 | /** 42 | * @brief All currently available visible Item instances 43 | */ 44 | std::vector> items; 45 | /** 46 | * @brief All used Item instances waiting to be deleted 47 | */ 48 | std::vector> usedItems; 49 | /** 50 | * @brief The window to render items in 51 | */ 52 | QQuickWindow *window = nullptr; 53 | }; 54 | -------------------------------------------------------------------------------- /src/items/agileitem.cpp: -------------------------------------------------------------------------------- 1 | #include "agileitem.hpp" 2 | 3 | /** 4 | * @brief Constructs the AgileItem 5 | * @param parentNode The parent node in the scene graph 6 | * @param iconName The icon name 7 | * @param allowedUsers The allowed users 8 | * @param pos The location 9 | * @param win The window to render in 10 | */ 11 | AgileItem::AgileItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) 12 | : Item(parentNode, iconName, allowedUsers, pos, win) { 13 | activatedTime = 3000; 14 | } 15 | 16 | /** 17 | * @brief Increases the rotational velocity of the given Curver 18 | * @param curver The Curver to make rotate faster 19 | */ 20 | void AgileItem::use(Curver *curver) { 21 | curver->rotateVelocity *= 1.25; 22 | } 23 | 24 | /** 25 | * @brief Decreases the rotational velocity of the given Curver 26 | * @param curver The Curver to make rotate slower 27 | */ 28 | void AgileItem::unUse(Curver *curver) { 29 | curver->rotateVelocity /= 1.25; 30 | } 31 | -------------------------------------------------------------------------------- /src/items/agileitem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "item.hpp" 4 | 5 | /** 6 | * @brief An Item that makes a Curver turn around faster 7 | */ 8 | class AgileItem : public Item { 9 | public: 10 | AgileItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 11 | private: 12 | virtual void use(Curver *curver) override; 13 | virtual void unUse(Curver *curver) override; 14 | }; 15 | -------------------------------------------------------------------------------- /src/items/cleaninstallitem.cpp: -------------------------------------------------------------------------------- 1 | #include "cleaninstallitem.hpp" 2 | 3 | /** 4 | * @brief Constructs the CleanInstallItem 5 | * @param parentNode The parent node in the scene graph 6 | * @param iconName The icon name 7 | * @param allowedUsers The allowed users 8 | * @param pos The location 9 | * @param win The window to render in 10 | */ 11 | CleanInstallItem::CleanInstallItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) 12 | : Item(parentNode, iconName, allowedUsers, pos, win) { 13 | } 14 | 15 | /** 16 | * @brief Cleaninstalls the Curver 17 | * @param curver The Curver to cleaninstall 18 | */ 19 | void CleanInstallItem::use(Curver *curver) { 20 | curver->cleanInstall(); 21 | } 22 | -------------------------------------------------------------------------------- /src/items/cleaninstallitem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "item.hpp" 4 | 5 | /** 6 | * @brief Cleaninstalls the entire round 7 | * 8 | * Erases every line 9 | */ 10 | class CleanInstallItem : public Item { 11 | public: 12 | CleanInstallItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 13 | private: 14 | virtual void use(Curver *curver) override; 15 | }; 16 | -------------------------------------------------------------------------------- /src/items/flashitem.cpp: -------------------------------------------------------------------------------- 1 | #include "flashitem.hpp" 2 | 3 | #define RECOVER_TIME 360 4 | 5 | /** 6 | * @brief Constructs the FlashItem 7 | * @param parentNode The parent node in the scene graph 8 | * @param iconName The icon name 9 | * @param allowedUsers The allowed users 10 | * @param pos The location 11 | * @param win The window to render in 12 | */ 13 | FlashItem::FlashItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) 14 | : Item(parentNode, iconName, allowedUsers, pos, win) { 15 | } 16 | 17 | /** 18 | * @brief Flashes the given Curver forward a bit 19 | * @param curver The Curver to flash 20 | */ 21 | void FlashItem::use(Curver *curver) { 22 | curver->prepareSegmentEvent(true, RECOVER_TIME, RECOVER_TIME); 23 | Curver::Rotation backup = curver->rotation; 24 | curver->rotation = Curver::Rotation::ROTATE_NONE; 25 | curver->progress(RECOVER_TIME, PlayerModel::get()->getCurvers()); 26 | curver->rotation = backup; 27 | } 28 | -------------------------------------------------------------------------------- /src/items/flashitem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "item.hpp" 4 | 5 | /** 6 | * @brief Flashes a Curver forward depending on its velocity 7 | */ 8 | class FlashItem : public Item { 9 | public: 10 | FlashItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 11 | private: 12 | virtual void use(Curver *curver) override; 13 | }; 14 | -------------------------------------------------------------------------------- /src/items/ghostitem.cpp: -------------------------------------------------------------------------------- 1 | #include "ghostitem.hpp" 2 | 3 | #define GHOST_TIME 2000 4 | 5 | /** 6 | * @brief Constructs the GhostItem 7 | * @param parentNode The parent node in the scene graph 8 | * @param iconName The icon name 9 | * @param allowedUsers The allowed users 10 | * @param pos The location 11 | * @param win The window to render in 12 | */ 13 | GhostItem::GhostItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) 14 | : Item(parentNode, iconName, allowedUsers, pos, win) { 15 | activatedTime = GHOST_TIME; 16 | } 17 | 18 | /** 19 | * @brief Renders the Curver invisible 20 | * @param curver The affected Curver 21 | */ 22 | void GhostItem::use(Curver *curver) { 23 | curver->prepareSegmentEvent(true, GHOST_TIME, GHOST_TIME); 24 | curver->headVisible = false; 25 | } 26 | 27 | /** 28 | * @brief Renders the Curver visible 29 | * @param curver The affected Curver 30 | */ 31 | void GhostItem::unUse(Curver *curver) { 32 | curver->headVisible = true; 33 | } 34 | -------------------------------------------------------------------------------- /src/items/ghostitem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "item.hpp" 4 | 5 | /** 6 | * @brief Renders a Curver invisible while still being able to collide 7 | */ 8 | class GhostItem : public Item { 9 | public: 10 | GhostItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 11 | private: 12 | virtual void use(Curver *curver) override; 13 | virtual void unUse(Curver *curver) override; 14 | }; 15 | -------------------------------------------------------------------------------- /src/items/invisibleitem.cpp: -------------------------------------------------------------------------------- 1 | #include "invisibleitem.hpp" 2 | 3 | #define INVISIBLE_TIME 3000 4 | 5 | /** 6 | * @brief Constructs the InvisibleItem 7 | * @param parentNode The parent node in the scene graph 8 | * @param iconName The icon name 9 | * @param allowedUsers The allowed users 10 | * @param pos The location 11 | * @param win The window to render in 12 | */ 13 | InvisibleItem::InvisibleItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) 14 | : Item(parentNode, iconName, allowedUsers, pos, win) { 15 | } 16 | 17 | /** 18 | * @brief Forces the Curver to immediately change segments 19 | * @param curver The affected Curver 20 | */ 21 | void InvisibleItem::use(Curver *curver) { 22 | curver->prepareSegmentEvent(true, INVISIBLE_TIME, INVISIBLE_TIME); 23 | } 24 | -------------------------------------------------------------------------------- /src/items/invisibleitem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "item.hpp" 4 | 5 | /** 6 | * @brief Renders a Curver invisible and makes it immune to collisions 7 | */ 8 | class InvisibleItem : public Item { 9 | public: 10 | InvisibleItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 11 | private: 12 | virtual void use(Curver *curver) override; 13 | }; 14 | -------------------------------------------------------------------------------- /src/items/item.cpp: -------------------------------------------------------------------------------- 1 | #include "item.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #define SIZE 12 7 | #define FADEDURATION 256 8 | 9 | /** 10 | * @brief Constructs a new Item instance 11 | * @param parentNode The parent node in the scene graph 12 | * @param iconName The path to the icon used as a texture 13 | * @param allowedUsers The allowed users for this Item 14 | * @param pos The location of this Item 15 | * @param window The window to render in 16 | */ 17 | Item::Item(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *window) { 18 | this->parentNode = parentNode; 19 | this->iconName = iconName; 20 | this->allowedUsers = allowedUsers; 21 | this->pos = pos; 22 | 23 | color = getColor(); 24 | imgNode = window->createImageNode(); 25 | imgNode->setFiltering(QSGTexture::Linear); 26 | imgNode->setMipmapFiltering(QSGTexture::Linear); 27 | initTexture(window); 28 | imgNode->setTexture(texture.get()); 29 | startFade(true); 30 | fade(); 31 | parentNode->appendChildNode(imgNode); 32 | } 33 | 34 | Item::~Item() { 35 | parentNode->removeChildNode(imgNode); 36 | delete imgNode; 37 | } 38 | 39 | /** 40 | * @brief Performs all updates on this Item 41 | */ 42 | void Item::update() { 43 | // check if needs to fade 44 | if (fadeStart.isValid()) { 45 | fade(); 46 | } 47 | // check if this item should be deactivated 48 | if (active && unUseTime.isValid() && QTime::currentTime() > unUseTime) { 49 | defuse(); 50 | } 51 | } 52 | 53 | /** 54 | * @brief Renders the Item inactive 55 | */ 56 | void Item::defuse() { 57 | if (active) { 58 | deactivate(); 59 | } 60 | } 61 | 62 | /** 63 | * @brief Triggers the Item 64 | * @param collector The collecting Curver 65 | */ 66 | void Item::trigger(std::unique_ptr &collector) { 67 | this->collector = collector.get(); 68 | 69 | applyToAffected(&Item::use); 70 | active = true; 71 | if (this->activatedTime != 0) { 72 | // has to be deactivated 73 | unUseTime = QTime::currentTime().addMSecs(activatedTime); 74 | } 75 | startFade(false); 76 | } 77 | 78 | /** 79 | * @brief Fades the Item in or out according to how much time passed by 80 | */ 81 | void Item::fade() { 82 | float actualDuration = fadeStart.msecsTo(QTime::currentTime()); 83 | float factor = !fadeIn + (fadeIn - !fadeIn) * actualDuration / FADEDURATION; 84 | factor = qMin(1.f, qMax(0.f, factor)); // 0 <= factor <= 1 85 | imgNode->setRect(this->pos.x() - SIZE * factor, this->pos.y() - SIZE * factor, 2 * SIZE * factor, 2 * SIZE * factor); 86 | imgNode->markDirty(QSGNode::DirtyGeometry); 87 | if (actualDuration > FADEDURATION) { 88 | fadeStart = QTime(); 89 | } 90 | } 91 | 92 | /** 93 | * @brief Triggered, when the Item was used and a non permanent effect of the Item must be stopped. 94 | * 95 | * Calls Item::unUse() on all affected Curver instances 96 | */ 97 | void Item::deactivate() { 98 | active = false; 99 | applyToAffected(&Item::unUse); 100 | } 101 | 102 | /** 103 | * @brief The immediate effect when the Item is triggered 104 | */ 105 | void Item::use(Curver *) { 106 | } 107 | 108 | /** 109 | * @brief The antidote for Item::use(). 110 | * 111 | * This is triggered, when the Item effect should be deactived again. 112 | */ 113 | void Item::unUse(Curver *) { 114 | } 115 | 116 | /** 117 | * @brief Returns the color of the Item 118 | * @return The color 119 | */ 120 | QColor Item::getColor() const { 121 | switch (allowedUsers) { 122 | case AllowedUsers::ALLOW_ALL: 123 | return Util::getColor("Blue"); 124 | case AllowedUsers::ALLOW_OTHERS: 125 | return Util::getColor("Red"); 126 | default: 127 | return Util::getColor("Green"); 128 | } 129 | } 130 | 131 | /** 132 | * @brief Initializes the texture of the Item 133 | * @param window The window to create the texture in 134 | */ 135 | void Item::initTexture(QQuickWindow *window) { 136 | if (Settings::get()->getOffscreen()) { 137 | return; 138 | } 139 | constexpr const int res = 48; 140 | QImage img = QImage(res, res, QImage::Format_RGB32); 141 | img.fill(color); // fill with background color 142 | QPainter painter(&img); 143 | QFont font {"Material Symbols Outlined"}; 144 | font.setPixelSize(res); 145 | painter.setFont(font); 146 | painter.drawText(QRect(0, 0, res, res), Qt::AlignCenter, Codepoints::get()->icon(iconName)); 147 | 148 | // create the texture 149 | assert(window); 150 | texture = std::unique_ptr(window->createTextureFromImage(img, QQuickWindow::TextureHasMipmaps)); 151 | assert(texture); 152 | texture->setFiltering(QSGTexture::Linear); 153 | texture->setMipmapFiltering(QSGTexture::Linear); 154 | } 155 | 156 | /** 157 | * @brief Checks if a given point is in trigger range of the Item 158 | * @param p The point to check for 159 | * @return \c True, iif \a p is in range 160 | */ 161 | bool Item::isInRange(QPointF p) const { 162 | QPointF diff = p - pos; 163 | if (qAbs(diff.x()) < SIZE && qAbs(diff.y()) < SIZE) { 164 | return true; 165 | } 166 | return false; 167 | } 168 | 169 | /** 170 | * @brief Starts a visual fade of the Item 171 | * @param in Whether to fade in or out 172 | */ 173 | void Item::startFade(bool in) { 174 | fadeIn = in; 175 | fadeStart = QTime::currentTime(); 176 | } 177 | 178 | /** 179 | * @brief Applies an effect to all affected Curver instances 180 | * 181 | * This can be the Item::use() or Item::unUse() routine. 182 | */ 183 | void Item::applyToAffected(void (Item::*method)(Curver *)) { 184 | switch (allowedUsers) { 185 | case AllowedUsers::ALLOW_ALL: 186 | std::ranges::for_each(PlayerModel::get()->getCurvers(), [&](auto &curver) { (this->*method)(curver.get()); }); 187 | break; 188 | case AllowedUsers::ALLOW_OTHERS: 189 | std::ranges::for_each(PlayerModel::get()->getCurvers(), [&](auto &curver) { if (curver.get() != this->collector) (this->*method)(curver.get()); }); 190 | break; 191 | case AllowedUsers::ALLOW_COLLECTOR: 192 | (this->*method)(collector); 193 | break; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/items/item.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "curver.hpp" 11 | #include "models/playermodel.hpp" 12 | #include "util.hpp" 13 | 14 | /** 15 | * @brief An Item that can be collected by a Curver and has certain effects 16 | */ 17 | class Item : public QObject { 18 | Q_OBJECT 19 | public: 20 | /** 21 | * @brief The affected Curver instances when an Item is triggered 22 | */ 23 | enum class AllowedUsers { 24 | ALLOW_ALL, // blue 25 | ALLOW_OTHERS, // red 26 | ALLOW_COLLECTOR // green 27 | }; 28 | 29 | explicit Item(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *window); 30 | ~Item(); 31 | 32 | void update(); 33 | void defuse(); 34 | void trigger(std::unique_ptr &collector); 35 | bool isInRange(QPointF p) const; 36 | 37 | /** 38 | * @brief The sequence number of this Item 39 | * 40 | * This property uniquely determines an Item for network communication 41 | */ 42 | unsigned int sequenceNumber = 0; 43 | protected slots: 44 | void fade(); 45 | void deactivate(); 46 | protected: 47 | virtual void use(Curver *); 48 | virtual void unUse(Curver *); 49 | QColor getColor() const; 50 | void initTexture(QQuickWindow *window); 51 | void startFade(bool in = true); 52 | void applyToAffected(void (Item::*method)(Curver *curver)); 53 | 54 | /** 55 | * @brief The parent node in the scene graph 56 | */ 57 | QSGNode *parentNode; 58 | /** 59 | * @brief The path to the icon that is used as a texture 60 | */ 61 | QString iconName; 62 | /** 63 | * @brief The affected Curver instances, once this Item instance is triggered 64 | */ 65 | AllowedUsers allowedUsers; 66 | /** 67 | * @brief The location of the Item 68 | */ 69 | QPointF pos; 70 | /** 71 | * @brief The amount of time that this Item stays activated before being deactivated in ms 72 | * 73 | * If the Item does not have to be deactivated, this value must be 0. 74 | */ 75 | int activatedTime = 0; 76 | /** 77 | * @brief The Curver that collected the Item 78 | */ 79 | Curver *collector; 80 | /** 81 | * @brief The texture of this Item 82 | */ 83 | std::unique_ptr texture; 84 | /** 85 | * @brief The node displaying this Item in the scene graph 86 | */ 87 | QSGImageNode *imgNode; 88 | /** 89 | * @brief The color of the Item 90 | */ 91 | QColor color; 92 | /** 93 | * @brief The time when this Item should deactivate after it was triggered 94 | * 95 | * If the item wasn't used yet, this is the null time. 96 | */ 97 | QTime unUseTime; 98 | /** 99 | * @brief The point of time that the last fade began 100 | * 101 | * If there is no fade going on right now, this is the null time. 102 | */ 103 | QTime fadeStart; 104 | /** 105 | * @brief Whether the Item is currently fading in or out. 106 | * 107 | * If the Item is not fading in or out at all, this can be any value. 108 | */ 109 | bool fadeIn; 110 | /** 111 | * @brief Whether the effect of the Item is currently active 112 | */ 113 | bool active = false; 114 | }; 115 | -------------------------------------------------------------------------------- /src/items/slowitem.cpp: -------------------------------------------------------------------------------- 1 | #include "slowitem.hpp" 2 | 3 | /** 4 | * @brief Constructs the SlowItem 5 | * @param parentNode The parent node in the scene graph 6 | * @param iconName The icon name 7 | * @param allowedUsers The allowed users 8 | * @param pos The location 9 | * @param win The window to render in 10 | */ 11 | SlowItem::SlowItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) 12 | : Item(parentNode, iconName, allowedUsers, pos, win) { 13 | activatedTime = 2000; 14 | } 15 | 16 | /** 17 | * @brief Decreases the velocity of the Curver 18 | * @param curver The affected Curver 19 | */ 20 | void SlowItem::use(Curver *curver) { 21 | curver->velocity /= 2; 22 | } 23 | 24 | /** 25 | * @brief Increases the velocity of the Curver 26 | * @param curver The affected Curver 27 | */ 28 | void SlowItem::unUse(Curver *curver) { 29 | curver->velocity *= 2; 30 | } 31 | -------------------------------------------------------------------------------- /src/items/slowitem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "item.hpp" 4 | 5 | /** 6 | * @brief Decreases the velocity of a Curver 7 | */ 8 | class SlowItem : public Item { 9 | public: 10 | SlowItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 11 | private: 12 | virtual void use(Curver *curver) override; 13 | virtual void unUse(Curver *curver) override; 14 | }; 15 | -------------------------------------------------------------------------------- /src/items/speeditem.cpp: -------------------------------------------------------------------------------- 1 | #include "speeditem.hpp" 2 | 3 | /** 4 | * @brief Constructs the SpeedItem 5 | * @param parentNode The parent node in the scene graph 6 | * @param iconName The icon name 7 | * @param allowedUsers The allowed users 8 | * @param pos The location 9 | * @param win The window to render in 10 | */ 11 | SpeedItem::SpeedItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) 12 | : Item(parentNode, iconName, allowedUsers, pos, win) { 13 | activatedTime = 2000; 14 | } 15 | 16 | /** 17 | * @brief Increases the speed of the Curver 18 | * @param curver The affected Curver 19 | */ 20 | void SpeedItem::use(Curver *curver) { 21 | curver->velocity *= 2; 22 | } 23 | 24 | /** 25 | * @brief Decreases the speed of the Curver 26 | * @param curver The affected Curver 27 | */ 28 | void SpeedItem::unUse(Curver *curver) { 29 | curver->velocity /= 2; 30 | } 31 | -------------------------------------------------------------------------------- /src/items/speeditem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "item.hpp" 4 | 5 | /** 6 | * @brief Increases the velocity of a Curver 7 | */ 8 | class SpeedItem : public Item { 9 | public: 10 | SpeedItem(QSGNode *parentNode, QString iconName, AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 11 | private: 12 | virtual void use(Curver *curver) override; 13 | virtual void unUse(Curver *curver) override; 14 | }; 15 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "game.hpp" 7 | #include "gamewatcher.hpp" 8 | #include "models/chatmodel.hpp" 9 | #include "models/itemmodel.hpp" 10 | #include "models/playermodel.hpp" 11 | #include "settings.hpp" 12 | #include "utility" 13 | #include "version.hpp" 14 | 15 | /** 16 | * @mainpage Introduction 17 | * 18 | * This is the documentation for the Quickcurver project, a free and open-source implementation of Achtung die Kurve written with Qt. 19 | * 20 | * A good starting point for reading this documentation is the Game class. 21 | */ 22 | 23 | 24 | int main(int argc, char *argv[]) { 25 | QGuiApplication app(argc, argv); 26 | 27 | qRegisterMetaType("JoinStatus"); 28 | 29 | QCoreApplication::setApplicationName("Quickcurver"); 30 | QCoreApplication::setApplicationVersion(Version::version_string()); 31 | 32 | QCommandLineParser parser; 33 | parser.setApplicationDescription("Quickcurver"); 34 | parser.addHelpOption(); 35 | parser.addVersionOption(); 36 | parser.process(app); 37 | 38 | // headless server 39 | if (Settings::get()->getOffscreen()) { 40 | GameWatcher gameWatcher; 41 | gameWatcher.start(); 42 | return app.exec(); 43 | } 44 | 45 | // register QML types here 46 | qmlRegisterType("Game", 1, 0, "Game"); 47 | qmlRegisterType("Client", 1, 0, "Client"); 48 | 49 | QQmlApplicationEngine engine; 50 | engine.addImportPath(QStringLiteral(":/")); 51 | 52 | engine.loadFromModule("Backend", "Main"); 53 | if (engine.rootObjects().isEmpty()) { 54 | return -1; 55 | } 56 | return app.exec(); 57 | } 58 | -------------------------------------------------------------------------------- /src/models/chatmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "chatmodel.hpp" 2 | 3 | /** 4 | * @brief Returns the number of rows in this model 5 | * @return The number of rows 6 | */ 7 | int ChatModel::rowCount(const QModelIndex &) const { 8 | return static_cast(m_data.size()); 9 | } 10 | 11 | /** 12 | * @brief Returns the data at a given position 13 | * @param index The position 14 | * @param role The data to return 15 | * @return The data at the given \a index 16 | */ 17 | QVariant ChatModel::data(const QModelIndex &index, int role) const { 18 | const auto &m = m_data[index.row()]; 19 | switch (role) { 20 | case UserNameRole: 21 | return m.username; 22 | case MessageRole: 23 | return m.message; 24 | case TimestampRole: 25 | return m.timestamp; 26 | default: 27 | return "Unknown role"; 28 | } 29 | } 30 | 31 | /** 32 | * @brief Returns the role names 33 | * @return Role names 34 | */ 35 | QHash ChatModel::roleNames() const { 36 | return m_roleNames; 37 | } 38 | 39 | /** 40 | * @brief Adds a chat message to the model 41 | * @param username The author 42 | * @param message The chat message 43 | */ 44 | void ChatModel::appendMessage(QString username, QString message) { 45 | beginInsertRows(index(m_data.size() - 1).parent(), m_data.size(), m_data.size()); 46 | m_data.push_back({username, message}); 47 | endInsertRows(); 48 | newMessage(username, message); 49 | } 50 | -------------------------------------------------------------------------------- /src/models/chatmodel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /** 9 | * @brief A model managing all chat messages 10 | */ 11 | class ChatModel : public QAbstractListModel { 12 | Q_OBJECT 13 | QML_ELEMENT 14 | QML_SINGLETON 15 | public: 16 | QML_CPP_SINGLETON(ChatModel) 17 | 18 | virtual int rowCount(const QModelIndex &) const override; 19 | virtual QVariant data(const QModelIndex &index, int role) const override; 20 | virtual QHash roleNames() const override; 21 | 22 | void appendMessage(QString username, QString message); 23 | signals: 24 | /** 25 | * @brief Notifies that a new message has arrived 26 | * @param username The username of the author 27 | * @param message The message content 28 | */ 29 | void newMessage(QString username, QString message); 30 | private: 31 | /** 32 | * @brief A chat message 33 | */ 34 | class ChatMessage { 35 | public: 36 | /** 37 | * @brief The author of the chat message 38 | */ 39 | QString username; 40 | /** 41 | * @brief The actual chat message 42 | */ 43 | QString message; 44 | /** 45 | * @brief The time that this message arrived 46 | */ 47 | QDateTime timestamp = QDateTime::currentDateTime(); 48 | }; 49 | /** 50 | * @brief All chat messages 51 | */ 52 | std::vector m_data; 53 | /** 54 | * @brief The role names for this model 55 | */ 56 | enum RoleNames { 57 | UserNameRole = Qt::UserRole, 58 | MessageRole, 59 | TimestampRole, 60 | }; 61 | /** 62 | * @brief The role name strings for this model 63 | */ 64 | QHash m_roleNames = {{UserNameRole, "username"}, {MessageRole, "message"}, {TimestampRole, "timestamp"}}; 65 | }; 66 | -------------------------------------------------------------------------------- /src/models/itemmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "itemmodel.hpp" 2 | 3 | /** 4 | * @brief Returns the number of rows stored in this model 5 | * @return The row count 6 | */ 7 | int ItemModel::rowCount(const QModelIndex &) const { 8 | return itemConfigs.size(); 9 | } 10 | 11 | /** 12 | * @brief Returns specific data stored in this model 13 | * @param index The index of the data 14 | * @param role The role to access 15 | * @return The data 16 | */ 17 | QVariant ItemModel::data(const QModelIndex &index, int role) const { 18 | const auto &itemConfig = itemConfigs[index.row()]; 19 | switch (role) { 20 | case NameRole: 21 | return itemConfig.name; 22 | case DescriptionRole: 23 | return itemConfig.description; 24 | case ProbabilityRole: 25 | return itemConfig.probability; 26 | case AllowedUsersRole: 27 | return static_cast(itemConfig.allowedUsers); 28 | case IconNameRole: 29 | return itemConfig.iconName; 30 | default: 31 | return "Unkown role"; 32 | } 33 | } 34 | 35 | /** 36 | * @brief Returns the role names of this model 37 | * @return The role names 38 | */ 39 | QHash ItemModel::roleNames() const { 40 | return m_roleNames; 41 | } 42 | 43 | /** 44 | * @brief Sets the probability of an Item 45 | * @param row The index of the Item 46 | * @param probability The new probability 47 | */ 48 | void ItemModel::setProbability(const int row, const float probability) { 49 | if (row < 0 || static_cast(row) >= itemConfigs.size() || probability < 0 || probability > 1) { 50 | return; 51 | } 52 | itemConfigs[static_cast(row)].probability = probability; 53 | } 54 | 55 | /** 56 | * @brief Sets the allowed users for an Item 57 | * @param row The index of the Item 58 | * @param allowedUsers The allowed users for this Item 59 | */ 60 | void ItemModel::setAllowedUsers(const int row, const int allowedUsers) { 61 | itemConfigs[static_cast(row)].allowedUsers = static_cast(allowedUsers); 62 | } 63 | 64 | /** 65 | * @brief Creates a random Item at a given position 66 | * @param parentNode The parent node in the scene graph 67 | * @param pos The location of the Item 68 | * @param win The window to render in 69 | * @return The just created Item 70 | */ 71 | Item *ItemModel::makeRandomItem(QSGNode *parentNode, QPointF pos, QQuickWindow *win) { 72 | float totalProbability = Util::accumulate(itemConfigs, 0.f); 73 | std::vector partialSums = itemConfigs; 74 | Util::partial_sum(itemConfigs, partialSums); 75 | float randValue = Util::rand() * totalProbability; 76 | auto it = std::ranges::find_if(partialSums, [randValue](auto &item) { return randValue < item.probability; }); 77 | auto *result = (this->*it->constructor)(parentNode, it->iconName, it->allowedUsers, pos, win); 78 | result->sequenceNumber = ++sequenceNumber; 79 | itemSpawned(true, result->sequenceNumber, it - partialSums.begin(), pos, it->allowedUsers, -1); 80 | return result; 81 | } 82 | 83 | /** 84 | * @brief Creates a predetermined Item 85 | * @param parentNode The parent node in the scene graph 86 | * @param which The kind of Item 87 | * @param pos The location of the Item 88 | * @param allowedUsers The allowed users for this Item 89 | * @param win The window to render in 90 | * @return The just created Item 91 | */ 92 | Item *ItemModel::makePredefinedItem(QSGNode *parentNode, int which, QPointF pos, Item::AllowedUsers allowedUsers, QQuickWindow *win) { 93 | auto conf = itemConfigs[which]; 94 | return (this->*conf.constructor)(parentNode, conf.iconName, allowedUsers, pos, win); 95 | } 96 | 97 | /** 98 | * @brief Constructs a SpeedItem 99 | * @param parentNode The parent node in the scene graph 100 | * @param iconName The path to the icon 101 | * @param allowedUsers The allowed users 102 | * @param pos The location 103 | * @param win The window to render in 104 | * @return The just created item 105 | */ 106 | Item *ItemModel::makeSpeedItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) { 107 | return new SpeedItem(parentNode, iconName, allowedUsers, pos, win); 108 | } 109 | 110 | /** 111 | * @brief Constructs a CleanInstallItem 112 | * @param parentNode The parent node in the scene graph 113 | * @param iconName The path to the icon 114 | * @param allowedUsers The allowed users 115 | * @param pos The location 116 | * @param win The window to render in 117 | * @return The just created item 118 | */ 119 | Item *ItemModel::makeCleanInstallItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) { 120 | return new CleanInstallItem(parentNode, iconName, allowedUsers, pos, win); 121 | } 122 | 123 | /** 124 | * @brief Constructs an InvisibleItem 125 | * @param parentNode The parent node in the scene graph 126 | * @param iconName The path to the icon 127 | * @param allowedUsers The allowed users 128 | * @param pos The location 129 | * @param win The window to render in 130 | * @return The just created item 131 | */ 132 | Item *ItemModel::makeInvisibleItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) { 133 | return new InvisibleItem(parentNode, iconName, allowedUsers, pos, win); 134 | } 135 | 136 | /** 137 | * @brief Constructs an AgileItem 138 | * @param parentNode The parent node in the scene graph 139 | * @param iconName The path to the icon 140 | * @param allowedUsers The allowed users 141 | * @param pos The location 142 | * @param win The window to render in 143 | * @return The just created item 144 | */ 145 | Item *ItemModel::makeAgileItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) { 146 | return new AgileItem(parentNode, iconName, allowedUsers, pos, win); 147 | } 148 | 149 | /** 150 | * @brief Constructs a FlashItem 151 | * @param parentNode The parent node in the scene graph 152 | * @param iconName The path to the icon 153 | * @param allowedUsers The allowed users 154 | * @param pos The location 155 | * @param win The window to render in 156 | * @return The just created item 157 | */ 158 | Item *ItemModel::makeFlashItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) { 159 | return new FlashItem(parentNode, iconName, allowedUsers, pos, win); 160 | } 161 | 162 | /** 163 | * @brief Constructs a SlowItem 164 | * @param parentNode The parent node in the scene graph 165 | * @param iconName The path to the icon 166 | * @param allowedUsers The allowed users 167 | * @param pos The location 168 | * @param win The window to render in 169 | * @return The just created item 170 | */ 171 | Item *ItemModel::makeSlowItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) { 172 | return new SlowItem(parentNode, iconName, allowedUsers, pos, win); 173 | } 174 | 175 | /** 176 | * @brief Constructs a GhostItem 177 | * @param parentNode The parent node in the scene graph 178 | * @param iconName The path to the icon 179 | * @param allowedUsers The allowed users 180 | * @param pos The location 181 | * @param win The window to render in 182 | * @return The just created item 183 | */ 184 | Item *ItemModel::makeGhostItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win) { 185 | return new GhostItem(parentNode, iconName, allowedUsers, pos, win); 186 | } 187 | 188 | /** 189 | * @brief Adds two probabilities 190 | * @param a A given initial probability 191 | * @param b The ItemModel::ItemConfig containing a probability 192 | * @return The accumulated probability 193 | */ 194 | float operator+(const float &a, const ItemModel::ItemConfig &b) { 195 | return a + b.probability; 196 | } 197 | 198 | /** 199 | * @brief Adds two probabilities 200 | * @param a A configuration with a probability 201 | * @param b Another configuration with a probability 202 | * @return The accumulated probability 203 | */ 204 | ItemModel::ItemConfig operator+(const ItemModel::ItemConfig &a, const ItemModel::ItemConfig &b) { 205 | ItemModel::ItemConfig result = b; 206 | result.probability = a.probability + b; 207 | return result; 208 | } 209 | -------------------------------------------------------------------------------- /src/models/itemmodel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "items/agileitem.hpp" 7 | #include "items/cleaninstallitem.hpp" 8 | #include "items/flashitem.hpp" 9 | #include "items/ghostitem.hpp" 10 | #include "items/invisibleitem.hpp" 11 | #include "items/item.hpp" 12 | #include "items/slowitem.hpp" 13 | #include "items/speeditem.hpp" 14 | 15 | /** 16 | * @brief A model containing all Item configurations 17 | * 18 | * This model does not hold spawned items. See ItemFactory for this. 19 | * This model stores the settings of all possible items. 20 | */ 21 | class ItemModel : public QAbstractListModel { 22 | Q_OBJECT 23 | QML_ELEMENT 24 | QML_SINGLETON 25 | public: 26 | QML_CPP_SINGLETON(ItemModel) 27 | 28 | virtual int rowCount(const QModelIndex &) const override; 29 | virtual QVariant data(const QModelIndex &index, int role) const override; 30 | virtual QHash roleNames() const override; 31 | 32 | Q_INVOKABLE void setProbability(const int row, const float probability); 33 | Q_INVOKABLE void setAllowedUsers(const int row, const int allowedUsers); 34 | 35 | Item *makeRandomItem(QSGNode *parentNode, QPointF pos, QQuickWindow *win); 36 | Item *makePredefinedItem(QSGNode *parentNode, int which, QPointF pos, Item::AllowedUsers allowedUsers, QQuickWindow *win); 37 | /** 38 | * @brief This struct contains all configuration options for a Item 39 | */ 40 | struct ItemConfig { 41 | /** 42 | * @brief The constructing function for the Item in question 43 | */ 44 | Item *(ItemModel::*constructor)(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 45 | /** 46 | * @brief The name of the Item 47 | */ 48 | QString name; 49 | /** 50 | * @brief A short description of the Item 51 | */ 52 | QString description; 53 | /** 54 | * @brief The spawn probability of the Item 55 | * 56 | * Must be between 0 and 1 57 | */ 58 | float probability; 59 | /** 60 | * @brief The allowed users for this Item 61 | */ 62 | Item::AllowedUsers allowedUsers; 63 | /** 64 | * @brief The icon name for this Item 65 | */ 66 | QString iconName; 67 | }; 68 | signals: 69 | /** 70 | * @brief Emitted when a new Item event happened 71 | * @param spawned Whether the Item spawned or was triggered 72 | * @param sequenceNumber The unique sequence number of the Item 73 | * @param which The kind of the Item 74 | * @param pos The location of the Item 75 | * @param allowedUsers The allowed users for the Item 76 | * @param collectorIndex If \a spawned is \c false, this value indicates the collecting Curver 77 | */ 78 | void itemSpawned(bool spawned, unsigned int sequenceNumber, int which, QPointF pos, Item::AllowedUsers allowedUsers, int collectorIndex); 79 | private: 80 | /** 81 | * @brief The role names for this model 82 | */ 83 | enum RoleNames { 84 | NameRole = Qt::UserRole, 85 | DescriptionRole, 86 | ProbabilityRole, 87 | AllowedUsersRole, 88 | IconNameRole 89 | }; 90 | /** 91 | * @brief The role name strings for this model 92 | */ 93 | QHash m_roleNames = {{NameRole, "name"}, {DescriptionRole, "description"}, {ProbabilityRole, "probability"}, {AllowedUsersRole, "allowedUsers"}, {IconNameRole, "iconName"}}; 94 | /** 95 | * @brief A vector containing all item configurations 96 | */ 97 | std::vector itemConfigs = { 98 | {&ItemModel::makeSpeedItem, "Speed", "Increases speed", 0.8, Item::AllowedUsers::ALLOW_COLLECTOR, "fast_forward"}, 99 | {&ItemModel::makeCleanInstallItem, "Cleaninstall", "Clears all segments", 0.5, Item::AllowedUsers::ALLOW_ALL, "clear_all"}, 100 | {&ItemModel::makeInvisibleItem, "Invisible", "Where are you?", 0.3, Item::AllowedUsers::ALLOW_COLLECTOR, "mystery"}, 101 | {&ItemModel::makeAgileItem, "Agile", "Turn around faster", 0.2, Item::AllowedUsers::ALLOW_COLLECTOR, "rotate_90_degrees_ccw"}, 102 | {&ItemModel::makeFlashItem, "Flash", "The fastest man alive", 0.2, Item::AllowedUsers::ALLOW_COLLECTOR, "flash_on"}, 103 | {&ItemModel::makeSlowItem, "Freeze", "Decreases speed", 0.1, Item::AllowedUsers::ALLOW_OTHERS, "fast_rewind"}, 104 | {&ItemModel::makeGhostItem, "Ghost", "Booh!", 0.0, Item::AllowedUsers::ALLOW_OTHERS, "mystery"}, 105 | }; 106 | /** 107 | * @brief The sequence number of the last spawned Item 108 | */ 109 | unsigned int sequenceNumber = 0; 110 | 111 | // item constructors 112 | Item *makeSpeedItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 113 | Item *makeCleanInstallItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 114 | Item *makeInvisibleItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 115 | Item *makeAgileItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 116 | Item *makeFlashItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 117 | Item *makeSlowItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 118 | Item *makeGhostItem(QSGNode *parentNode, QString iconName, Item::AllowedUsers allowedUsers, QPointF pos, QQuickWindow *win); 119 | }; 120 | 121 | float operator+(const float &a, const ItemModel::ItemConfig &b); 122 | ItemModel::ItemConfig operator+(const ItemModel::ItemConfig &a, const ItemModel::ItemConfig &b); 123 | -------------------------------------------------------------------------------- /src/models/playermodel.cpp: -------------------------------------------------------------------------------- 1 | #include "playermodel.hpp" 2 | 3 | /** 4 | * @brief Returns the number of rows in this model 5 | * @return The row count 6 | */ 7 | int PlayerModel::rowCount(const QModelIndex &) const { 8 | return static_cast(m_data.size()); 9 | } 10 | 11 | /** 12 | * @brief Returns data in this model 13 | * @param index The index of the data to return 14 | * @param role The data in question 15 | * @return The data 16 | */ 17 | QVariant PlayerModel::data(const QModelIndex &index, int role) const { 18 | const std::unique_ptr &curver = m_data[static_cast(index.row())]; 19 | switch (role) { 20 | case NameRole: 21 | return curver->userName; 22 | case ColorRole: 23 | return curver->getColor(); 24 | case LeftKeyRole: 25 | return curver->getLeftKey(); 26 | case RightKeyRole: 27 | return curver->getRightKey(); 28 | case RoundScoreRole: 29 | return curver->roundScore; 30 | case TotalScoreRole: 31 | return curver->totalScore; 32 | case ControllerRole: 33 | return static_cast(curver->controller); 34 | case PingRole: 35 | return curver->ping; 36 | default: 37 | return "Unknown role"; 38 | } 39 | } 40 | 41 | /** 42 | * @brief Returns the role names 43 | * @return Role names 44 | */ 45 | QHash PlayerModel::roleNames() const { 46 | return m_roleNames; 47 | } 48 | 49 | /** 50 | * @brief Appends a new player to this model 51 | */ 52 | void PlayerModel::appendPlayer() { 53 | beginResetModel(); 54 | m_data.push_back(std::make_unique(rootNode)); 55 | m_data.back()->userName = "Player " + QString::number(m_data.size()); 56 | connect(m_data.back().get(), &Curver::died, this, &PlayerModel::processDeath); 57 | endResetModel(); 58 | playerModelChanged(); 59 | } 60 | 61 | /** 62 | * @brief Appends a new bot to this model 63 | */ 64 | void PlayerModel::appendBot() { 65 | appendPlayer(); 66 | setController(m_data.size() - 1, static_cast(Curver::Controller::CONTROLLER_BOT)); 67 | } 68 | 69 | /** 70 | * @brief Removes a player from this model 71 | * @param row The index of the player 72 | */ 73 | void PlayerModel::removePlayer(int row) { 74 | beginResetModel(); 75 | m_data.erase(m_data.begin() + row); 76 | endResetModel(); 77 | playerModelChanged(); 78 | } 79 | 80 | /** 81 | * @brief Removes a player from this model 82 | * @param curver The pointer to the player 83 | */ 84 | void PlayerModel::removeCurver(Curver *curver) { 85 | auto it = std::ranges::find_if(m_data, [=](const auto &c) { return c.get() == curver; }); 86 | if (it != m_data.cend()) { 87 | removePlayer(it - m_data.cbegin()); 88 | } 89 | } 90 | 91 | /** 92 | * @brief Sets the color of a player 93 | * @param row The index of the player 94 | * @param color The new color 95 | */ 96 | void PlayerModel::setColor(int row, QColor color) { 97 | m_data[static_cast(row)]->setColor(color); 98 | playerModelChanged(); 99 | } 100 | 101 | /** 102 | * @brief Sets the left key of a player 103 | * @param row The index of the player 104 | * @param key The new key 105 | */ 106 | void PlayerModel::setLeftKey(int row, Qt::Key key) { 107 | const auto player = m_data[static_cast(row)].get(); 108 | player->setLeftKey(key); 109 | Gui::getSingleton().postInfoBar("Set left key of " + player->userName + " to " + QKeySequence(key).toString()); 110 | } 111 | 112 | /** 113 | * @brief Sets the right key of a player 114 | * @param row The index of the player 115 | * @param key The new key 116 | */ 117 | void PlayerModel::setRightKey(int row, Qt::Key key) { 118 | const auto player = m_data[static_cast(row)].get(); 119 | player->setRightKey(key); 120 | Gui::getSingleton().postInfoBar("Set right key of " + player->userName + " to " + QKeySequence(key).toString()); 121 | } 122 | 123 | /** 124 | * @brief Sets the username of a player 125 | * @param row The index of the player 126 | * @param username The new username 127 | */ 128 | void PlayerModel::setUserName(int row, QString username) { 129 | m_data[static_cast(row)]->userName = username; 130 | dataChanged(index(row, 0), index(row, 0), QVector() = {NameRole}); 131 | playerModelChanged(); 132 | } 133 | 134 | /** 135 | * @brief Sets the controller of a player 136 | * @param row The index of the player 137 | * @param ctrl The new controller 138 | */ 139 | void PlayerModel::setController(int row, int ctrl) { 140 | m_data[static_cast(row)]->controller = static_cast(ctrl); 141 | dataChanged(index(row, 0), index(row, 0), QVector() = {ControllerRole}); 142 | playerModelChanged(); 143 | } 144 | 145 | /** 146 | * @brief Sets the root node of the scene graph 147 | * @param rootNode The new root node 148 | */ 149 | void PlayerModel::setRootNode(QSGNode *rootNode) { 150 | this->rootNode = rootNode; 151 | } 152 | 153 | /** 154 | * @brief Returns all players as a Curver vector 155 | * @return All players 156 | */ 157 | std::vector> &PlayerModel::getCurvers() { 158 | return this->m_data; 159 | } 160 | 161 | /** 162 | * @brief Serializes this PlayerModel 163 | * @param out The stream to serialize into 164 | */ 165 | void PlayerModel::serialize(QDataStream &out) const { 166 | out << static_cast(m_data.size()); 167 | for (unsigned long i = 0; i < m_data.size(); ++i) { 168 | const std::unique_ptr &c = m_data[i]; 169 | out << c->userName << c->getColor() << c->roundScore << c->totalScore << static_cast(c->controller) << static_cast(c->isAlive()); 170 | } 171 | } 172 | 173 | /** 174 | * @brief Parses a PlayerModel from a stream 175 | * @param in The stream to parse from 176 | */ 177 | void PlayerModel::parse(QDataStream &in) { 178 | beginResetModel(); 179 | unsigned size; 180 | in >> size; 181 | m_data.resize(size); 182 | for (auto &c : m_data) { 183 | if (!c) { 184 | c = std::make_unique(rootNode); 185 | } 186 | QColor color; 187 | uint8_t ctrl, isAlive; 188 | in >> c->userName >> color >> c->roundScore >> c->totalScore >> ctrl >> isAlive; 189 | c->setColor(color); 190 | c->controller = static_cast(ctrl); 191 | c->setAlive(static_cast(isAlive)); 192 | } 193 | endResetModel(); 194 | } 195 | 196 | /** 197 | * @brief Returns the last player added 198 | * @return The last added Curver 199 | */ 200 | Curver *PlayerModel::getNewPlayer() { 201 | appendPlayer(); 202 | return m_data.back().get(); 203 | } 204 | 205 | /** 206 | * @brief Forces a refresh of the GUI 207 | */ 208 | void PlayerModel::forceRefresh() { 209 | beginResetModel(); 210 | endResetModel(); 211 | playerModelChanged(); 212 | } 213 | 214 | /** 215 | * @brief Forces a refresh of the score roles in the GUI 216 | */ 217 | void PlayerModel::processDeath() { 218 | curverDied(); 219 | dataChanged(index(0, 0), index(static_cast(m_data.size()) - 1, 0), QVector() = {RoundScoreRole, TotalScoreRole}); 220 | playerModelChanged(); 221 | } 222 | 223 | /** 224 | * @brief Removes all bots 225 | */ 226 | void PlayerModel::removeBots() { 227 | for (size_t i = 0; i < m_data.size(); ++i) { 228 | if (m_data[i]->controller == Curver::Controller::CONTROLLER_BOT) { 229 | this->removePlayer(i); 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/models/playermodel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "curver.hpp" 10 | #include "gui.hpp" 11 | 12 | /** 13 | * @brief A model containing all players 14 | */ 15 | class PlayerModel : public QAbstractListModel { 16 | Q_OBJECT 17 | QML_ELEMENT 18 | QML_SINGLETON 19 | public: 20 | QML_CPP_SINGLETON(PlayerModel) 21 | 22 | virtual int rowCount(const QModelIndex &) const override; 23 | virtual QVariant data(const QModelIndex &index, int role) const override; 24 | virtual QHash roleNames() const override; 25 | 26 | Q_INVOKABLE void appendPlayer(); 27 | Q_INVOKABLE void appendBot(); 28 | Q_INVOKABLE void removePlayer(int row); 29 | void removeCurver(Curver *curver); 30 | Q_INVOKABLE void setColor(int row, QColor color); 31 | Q_INVOKABLE void setLeftKey(int row, Qt::Key key); 32 | Q_INVOKABLE void setRightKey(int row, Qt::Key key); 33 | Q_INVOKABLE void setUserName(int row, QString username); 34 | Q_INVOKABLE void setController(int row, int ctrl); 35 | 36 | void setRootNode(QSGNode *rootNode); 37 | std::vector> &getCurvers(); 38 | void serialize(QDataStream &out) const; 39 | void parse(QDataStream &in); 40 | Curver *getNewPlayer(); 41 | void forceRefresh(); 42 | public slots: 43 | void processDeath(); 44 | void removeBots(); 45 | signals: 46 | /** 47 | * @brief Emitted when a Curver died 48 | */ 49 | void curverDied(); 50 | /** 51 | * @brief Emitted when the model changed 52 | */ 53 | void playerModelChanged(); 54 | private: 55 | /** 56 | * @brief The model data 57 | */ 58 | std::vector> m_data; 59 | /** 60 | * @brief The role names 61 | */ 62 | enum RoleNames { 63 | NameRole = Qt::UserRole, 64 | ColorRole, 65 | LeftKeyRole, 66 | RightKeyRole, 67 | RoundScoreRole, 68 | TotalScoreRole, 69 | ControllerRole, 70 | PingRole, 71 | }; 72 | /** 73 | * @brief The role name strings 74 | */ 75 | QHash m_roleNames {{NameRole, "name"}, {ColorRole, "color"}, {LeftKeyRole, "leftKey"}, {RightKeyRole, "rightKey"}, {RoundScoreRole, "roundScore"}, {TotalScoreRole, "totalScore"}, {ControllerRole, "controller"}, {PingRole, "ping"}}; 76 | 77 | /** 78 | * @brief The root node in the scene graph 79 | */ 80 | QSGNode *rootNode; 81 | }; 82 | -------------------------------------------------------------------------------- /src/network/client.cpp: -------------------------------------------------------------------------------- 1 | #include "client.hpp" 2 | 3 | #define PING_INTERVAL 5000 4 | #define JOIN_TIMEOUT 30000 5 | 6 | Client::Client() { 7 | in.setDevice(&tcpSocket); 8 | connect(&tcpSocket, &QTcpSocket::errorOccurred, this, &Client::socketError); 9 | connect(&tcpSocket, &QTcpSocket::connected, this, &Client::socketConnected); 10 | connect(&tcpSocket, &QTcpSocket::disconnected, this, &Client::socketDisconnected); 11 | connect(&tcpSocket, &QTcpSocket::readyRead, this, &Client::socketReadyRead); 12 | connect(&udpSocket, &QUdpSocket::errorOccurred, this, &Client::udpSocketError); 13 | connect(&udpSocket, &QUdpSocket::readyRead, this, &Client::udpSocketReadyRead); 14 | 15 | connect(&pingTimer, &QTimer::timeout, this, &Client::pingServer); 16 | pingTimer.setInterval(PING_INTERVAL); 17 | connect(this, &Client::dnsFinished, this, &Client::handleDns); 18 | connect(&joinTimeoutTimer, &QTimer::timeout, this, &Client::handleJoinTimeout); 19 | joinTimeoutTimer.setSingleShot(true); 20 | joinTimeoutTimer.setInterval(JOIN_TIMEOUT); 21 | 22 | // choose an arbitrary local port for UDP 23 | udpSocket.bind(); 24 | } 25 | 26 | /** 27 | * @brief Returns the current join status 28 | * @return joinStatus 29 | */ 30 | Client::JoinStatus Client::getJoinStatus() const { 31 | return this->joinStatus; 32 | } 33 | 34 | /** 35 | * @brief Connects the Client to the given host 36 | * @param addr The IP address of the host to connect to 37 | * @param port The port that the host is listening on 38 | */ 39 | void Client::connectToHost(QString addr, quint16 port) { 40 | this->serverAddress = {QHostAddress(), port}; 41 | joinTimeoutTimer.start(); 42 | // first look up the hostname 43 | setJoinStatus(JoinStatus::DNS_PENDING); 44 | QHostInfo::lookupHost(addr, this, &Client::dnsFinished); 45 | } 46 | 47 | /** 48 | * @brief Sends a chat message for the Server to broadcast 49 | * @param msg The chat message 50 | */ 51 | void Client::sendChatMessage(QString msg) { 52 | Packet::ClientChatMsg p; 53 | p.message = msg; 54 | p.sendPacket(&tcpSocket); 55 | } 56 | 57 | /** 58 | * @brief Sends the PlayerModel of the Client to the Server 59 | */ 60 | void Client::sendPlayerModel() { 61 | Packet::ClientPlayerModel p; 62 | p.username = Settings::get()->getClientName(); 63 | p.color = Settings::get()->getClientColor(); 64 | p.sendPacket(&tcpSocket); 65 | } 66 | 67 | /** 68 | * @brief Processes the key 69 | * 70 | * If the key matches, this changes the rotation of the Curver belonging to the Client 71 | * @param key The key to process 72 | * @param release Whether the key was pressed or released 73 | */ 74 | void Client::processKey(Qt::Key key, bool release) { 75 | Packet::ClientCurverRotation p; 76 | if (release) { 77 | p.rotation = Curver::Rotation::ROTATE_NONE; 78 | } else { 79 | // TODO: Allow custom keys 80 | if (key == Qt::Key_Left) 81 | p.rotation = Curver::Rotation::ROTATE_LEFT; 82 | else 83 | p.rotation = Curver::Rotation::ROTATE_RIGHT; 84 | } 85 | p.sendPacket(&tcpSocket); 86 | } 87 | 88 | /** 89 | * @brief Sends a custom Ping packet to a Server over UDP 90 | */ 91 | void Client::pingServer() { 92 | Packet::Ping p; 93 | p.delta = ping; 94 | p.sendPacketUdp(&udpSocket, serverAddress); 95 | } 96 | 97 | /** 98 | * @brief Called, when a socket error occurred 99 | */ 100 | void Client::socketError(QAbstractSocket::SocketError) { 101 | setJoinStatus(JoinStatus::FAILED); 102 | Gui::getSingleton().postInfoBar(tcpSocket.errorString()); 103 | } 104 | 105 | /** 106 | * @brief Called, when the socket has connected 107 | */ 108 | void Client::socketConnected() { 109 | // TCP connection successful, now try UDP 110 | setJoinStatus(JoinStatus::UDP_PENDING); 111 | pingServer(); 112 | } 113 | 114 | /** 115 | * @brief Called, when the socket has disconnected 116 | */ 117 | void Client::socketDisconnected() { 118 | setJoinStatus(JoinStatus::FAILED); 119 | Gui::getSingleton().postInfoBar("Disconnected"); 120 | } 121 | 122 | /** 123 | * @brief Called, when there is new data available on the socket 124 | */ 125 | void Client::socketReadyRead() { 126 | bool illformedPacket = false; 127 | while (tcpSocket.bytesAvailable() && !illformedPacket) { 128 | in.startTransaction(); 129 | auto packet = Packet::AbstractPacket::receivePacket(in, InstanceType::Server); 130 | if (in.commitTransaction()) { 131 | handlePacket(packet); 132 | } else { 133 | qInfo() << "Received ill-formed packet"; 134 | illformedPacket = true; 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * @brief Handles an UDP socket error 141 | */ 142 | void Client::udpSocketError(QAbstractSocket::SocketError) { 143 | qInfo() << udpSocket.errorString(); 144 | } 145 | 146 | /** 147 | * @brief Handles incoming UDP datagrams 148 | */ 149 | void Client::udpSocketReadyRead() { 150 | while (udpSocket.hasPendingDatagrams()) { 151 | // get datagram 152 | QByteArray datagram; 153 | datagram.resize(udpSocket.pendingDatagramSize()); 154 | udpSocket.readDatagram(datagram.data(), datagram.size()); 155 | QDataStream udpStream(&datagram, QIODevice::ReadOnly); 156 | udpStream.startTransaction(); 157 | auto packet = Packet::AbstractPacket::receivePacket(udpStream, InstanceType::Server); 158 | if (udpStream.commitTransaction()) { 159 | handlePacket(packet); 160 | } else { 161 | qInfo() << "ill-formed udp packet"; 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * @brief Handles the result of a DNS request 168 | * @param info The returned DNS info 169 | */ 170 | void Client::handleDns(QHostInfo info) { 171 | if (info.error()) { 172 | setJoinStatus(JoinStatus::FAILED); 173 | Gui::getSingleton().postInfoBar(info.errorString()); 174 | return; 175 | } else if (info.addresses().isEmpty()) { 176 | setJoinStatus(JoinStatus::FAILED); 177 | Gui::getSingleton().postInfoBar("Could not resolve hostname"); 178 | return; 179 | } 180 | this->serverAddress.addr = info.addresses().first(); 181 | setJoinStatus(JoinStatus::TCP_PENDING); 182 | tcpSocket.connectToHost(serverAddress.addr, serverAddress.port); 183 | } 184 | 185 | /** 186 | * @brief Handles a join timeout 187 | */ 188 | void Client::handleJoinTimeout() { 189 | if (joinStatus != JoinStatus::JOINED) { 190 | setJoinStatus(JoinStatus::FAILED); 191 | Gui::getSingleton().postInfoBar("The join request timed out"); 192 | } 193 | } 194 | 195 | /** 196 | * @brief Processes an already received packet 197 | * @param p The packet that was received 198 | */ 199 | void Client::handlePacket(std::unique_ptr &p) { 200 | // First handle flags 201 | if (p->start) { 202 | Gui::getSingleton().startGame(); 203 | } 204 | if (p->reset) { 205 | resetRound(); 206 | } 207 | switch (static_cast(p->type)) { 208 | case Packet::ServerTypes::Chat_Message: 209 | { 210 | auto *chatMsg = (Packet::ServerChatMsg *) p.get(); 211 | ChatModel::get()->appendMessage(chatMsg->username, chatMsg->message); 212 | break; 213 | } 214 | case Packet::ServerTypes::PlayerModelEdit: 215 | { 216 | auto *playerModel = (Packet::ServerPlayerModel *) p.get(); 217 | playerModel->extract(); 218 | break; 219 | } 220 | case Packet::ServerTypes::CurverData: 221 | { 222 | auto *curverData = (Packet::ServerCurverData *) p.get(); 223 | curverData->extract(); 224 | updateGraphics(); 225 | break; 226 | } 227 | case Packet::ServerTypes::ItemData: 228 | { 229 | auto *itemData = (Packet::ServerItemData *) p.get(); 230 | integrateItem(itemData->spawned, itemData->sequenceNumber, itemData->which, itemData->pos, itemData->allowedUsers, itemData->collectorIndex); 231 | break; 232 | } 233 | case Packet::ServerTypes::SettingsType: 234 | { 235 | auto *settingsData = (Packet::ServerSettingsData *) p.get(); 236 | settingsData->extract(); 237 | break; 238 | } 239 | case Packet::ServerTypes::Pong: 240 | { 241 | auto *pong = (Packet::Pong *) p.get(); 242 | ping = Util::getTimeDiff(pong->sent); 243 | this->curverIndex = pong->curverIndex; 244 | pong->extract(); 245 | PlayerModel::get()->forceRefresh(); 246 | 247 | if (this->joinStatus == JoinStatus::UDP_PENDING) { 248 | setJoinStatus(JoinStatus::JOINED); 249 | sendPlayerModel(); 250 | } 251 | break; 252 | } 253 | default: 254 | qInfo() << "Unsupported packet type"; 255 | break; 256 | } 257 | } 258 | 259 | /** 260 | * @brief Sets the join status 261 | * @param s The new join status 262 | */ 263 | void Client::setJoinStatus(const JoinStatus s) { 264 | this->joinStatus = s; 265 | if (joinStatus == JoinStatus::JOINED) { 266 | pingTimer.start(); 267 | } else { 268 | pingTimer.stop(); 269 | } 270 | joinStatusChanged(s); 271 | } 272 | -------------------------------------------------------------------------------- /src/network/client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "network.hpp" 8 | 9 | /** 10 | * @brief A Client network instance 11 | * 12 | * Handles the network communication from the client side 13 | */ 14 | class Client : public QObject { 15 | Q_OBJECT 16 | 17 | Q_PROPERTY(JoinStatus joinStatus READ getJoinStatus NOTIFY joinStatusChanged) 18 | public: 19 | /** 20 | * @brief The current status of joining a Server 21 | */ 22 | enum class JoinStatus { 23 | NONE, 24 | FAILED, 25 | DNS_PENDING, 26 | TCP_PENDING, 27 | UDP_PENDING, 28 | JOINED 29 | }; 30 | Q_ENUM(JoinStatus) 31 | 32 | Client(); 33 | 34 | Q_INVOKABLE JoinStatus getJoinStatus() const; 35 | 36 | void connectToHost(QString addr, quint16 port); 37 | void sendChatMessage(QString msg); 38 | void sendPlayerModel(); 39 | void processKey(Qt::Key key, bool release); 40 | void pingServer(); 41 | signals: 42 | /** 43 | * @brief Emitted when a new Item has to be integrated into the Game 44 | * @param spawned Whether the Item spawned or was triggered 45 | * @param sequenceNumber The unique sequence number of the Item 46 | * @param which The kind of Item 47 | * @param pos The location of the Item 48 | * @param allowedUsers The allowed users for the Item 49 | * @param collectorIndex If \a spawned is \c false, then this indicates the collecting Curver 50 | */ 51 | void integrateItem(bool spawned, unsigned int sequenceNumber, int which, QPointF pos, Item::AllowedUsers allowedUsers, int collectorIndex); 52 | /** 53 | * @brief Emitted when the round has to be reset 54 | */ 55 | void resetRound(); 56 | /** 57 | * @brief Emitted when the join status changed 58 | * @param joinStatus The new join status 59 | */ 60 | void joinStatusChanged(const JoinStatus joinStatus); 61 | /** 62 | * @brief Emitted when the DNS replied 63 | * @param info The DNS reply 64 | */ 65 | void dnsFinished(QHostInfo info); 66 | /** 67 | * @brief Emitted when the graphics stack needs to push a new frame 68 | * 69 | * This can happen for example when the \a Client receives new \a Curver data. 70 | * Thus requiring an update in the graphics stack so that the user can see the new data. 71 | */ 72 | void updateGraphics(); 73 | private slots: 74 | // tcp 75 | void socketError(QAbstractSocket::SocketError); 76 | void socketConnected(); 77 | void socketDisconnected(); 78 | void socketReadyRead(); 79 | // udp 80 | void udpSocketError(QAbstractSocket::SocketError); 81 | void udpSocketReadyRead(); 82 | 83 | void handleDns(QHostInfo info); 84 | void handleJoinTimeout(); 85 | private: 86 | void handlePacket(std::unique_ptr &p); 87 | void setJoinStatus(const JoinStatus s); 88 | /** 89 | * @brief The TCP socket to communicate with 90 | */ 91 | QTcpSocket tcpSocket; 92 | /** 93 | * @brief The UDP socket to communicate with 94 | */ 95 | QUdpSocket udpSocket; 96 | /** 97 | * @brief A data stream belonging to Client::socket 98 | */ 99 | QDataStream in; 100 | /** 101 | * @brief The address of the server to connect to 102 | */ 103 | FullNetworkAddress serverAddress; 104 | /** 105 | * @brief The current join status 106 | */ 107 | JoinStatus joinStatus = JoinStatus::NONE; 108 | /** 109 | * @brief The timer responsible for continuously sending a Ping to the Server 110 | */ 111 | QTimer pingTimer; 112 | /** 113 | * @brief The current ping to the server 114 | * This is updated with every Pong packet received. 115 | */ 116 | qint64 ping = 0; 117 | /** 118 | * @brief The timer responsible for cancelling a join request, if it takes too long 119 | */ 120 | QTimer joinTimeoutTimer; 121 | /** 122 | * @brief The index of the client in the server curver array 123 | */ 124 | int curverIndex = -1; 125 | }; 126 | -------------------------------------------------------------------------------- /src/network/network.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "curver.hpp" 7 | #include "gui.hpp" 8 | #include "items/item.hpp" 9 | #include "models/chatmodel.hpp" 10 | #include "models/playermodel.hpp" 11 | #include "util.hpp" 12 | 13 | /** 14 | * @brief A wrapper for host address plus port. 15 | */ 16 | struct FullNetworkAddress { 17 | /** 18 | * @brief The address of the remote host 19 | */ 20 | QHostAddress addr; 21 | /** 22 | * @brief The port of the remote host 23 | */ 24 | quint16 port; 25 | }; 26 | 27 | bool operator==(const FullNetworkAddress &l, const FullNetworkAddress &r); 28 | 29 | /** 30 | * @brief An enumeration representing the instance type. 31 | * 32 | * One of Server and Client. 33 | */ 34 | enum class InstanceType : bool { 35 | Server, 36 | Client 37 | }; 38 | 39 | /** 40 | * @brief The namespace containing all packets 41 | */ 42 | namespace Packet { 43 | 44 | /** 45 | * @brief A data type representing the type of a packet 46 | */ 47 | using PacketType = uint8_t; 48 | #define PACKET_TYPE_BITS 3 49 | 50 | /** 51 | * @brief An enumeration containing all server packet types 52 | */ 53 | enum class ServerTypes : PacketType { 54 | Chat_Message, 55 | PlayerModelEdit, 56 | CurverData, 57 | ItemData, 58 | SettingsType, 59 | Pong, 60 | }; 61 | 62 | /** 63 | * @brief An enumeration containing all client packet types 64 | */ 65 | enum class ClientTypes : PacketType { 66 | Chat_Message, 67 | PlayerModelEdit, 68 | CurverRotation, 69 | Ping, 70 | }; 71 | 72 | /** 73 | * @brief A class representing an abstract packet. 74 | * 75 | * Contains all common packet members and their serialization patterns 76 | */ 77 | class AbstractPacket { 78 | public: 79 | explicit AbstractPacket(PacketType type); 80 | virtual ~AbstractPacket(); 81 | void sendPacket(QTcpSocket *s) const; 82 | void sendPacketUdp(QUdpSocket *s, FullNetworkAddress a) const; 83 | static std::unique_ptr receivePacket(QDataStream &in, InstanceType from); 84 | /** 85 | * @brief The packet type 86 | */ 87 | PacketType type; 88 | /** 89 | * @brief Whether the game started 90 | */ 91 | bool start = false; 92 | /** 93 | * @brief Whether the game resets 94 | */ 95 | bool reset = false; 96 | protected: 97 | /** 98 | * @brief Serializes a packet 99 | * @param out The stream to serialize into 100 | */ 101 | virtual void serialize(QDataStream &out) const = 0; 102 | /** 103 | * @brief Parses a packet from an incoming stream 104 | * @param in The stream to parse from 105 | */ 106 | virtual void parse(QDataStream &in) = 0; 107 | }; 108 | 109 | /** 110 | * @brief A packet that represents a chat message coming from a Server 111 | */ 112 | class ServerChatMsg : public AbstractPacket { 113 | public: 114 | ServerChatMsg(); 115 | /** 116 | * @brief The username of the sender 117 | */ 118 | QString username; 119 | /** 120 | * @brief The chat message 121 | */ 122 | QString message; 123 | protected: 124 | virtual void serialize(QDataStream &out) const override; 125 | virtual void parse(QDataStream &in) override; 126 | }; 127 | 128 | /** 129 | * @brief A packet that represents a chat message coming from a Client 130 | */ 131 | class ClientChatMsg : public AbstractPacket { 132 | public: 133 | ClientChatMsg(); 134 | /** 135 | * @brief The chat message 136 | */ 137 | QString message; 138 | protected: 139 | virtual void serialize(QDataStream &out) const override; 140 | virtual void parse(QDataStream &in) override; 141 | }; 142 | 143 | /** 144 | * @brief A struct containing all Player data that is necessary to send over the network 145 | */ 146 | struct Player { 147 | /** 148 | * @brief The username 149 | */ 150 | QString userName; 151 | /** 152 | * @brief The color 153 | */ 154 | QColor color; 155 | /** 156 | * @brief The score in this round 157 | */ 158 | int roundScore; 159 | /** 160 | * @brief The total score 161 | */ 162 | int totalScore; 163 | /** 164 | * @brief The controller of the underlying Curver 165 | */ 166 | Curver::Controller controller; 167 | /** 168 | * @brief Whether the player is alive 169 | */ 170 | bool isAlive; 171 | }; 172 | 173 | QDataStream &operator<<(QDataStream &out, const Player &p); 174 | QDataStream &operator>>(QDataStream &in, Player &p); 175 | 176 | /** 177 | * @brief A packet that represents a PlayerModel change coming from a Server 178 | */ 179 | class ServerPlayerModel : public AbstractPacket { 180 | public: 181 | ServerPlayerModel(); 182 | void fill(); 183 | void extract(); 184 | /** 185 | * @brief A vector containing every Player data 186 | */ 187 | std::vector data; 188 | protected: 189 | virtual void serialize(QDataStream &out) const override; 190 | virtual void parse(QDataStream &in) override; 191 | }; 192 | 193 | /** 194 | * @brief A packet that represents a PlayerModel change coming from a Client 195 | */ 196 | class ClientPlayerModel : public AbstractPacket { 197 | public: 198 | ClientPlayerModel(); 199 | /** 200 | * @brief The username 201 | */ 202 | QString username; 203 | /** 204 | * @brief The color 205 | */ 206 | QColor color; 207 | protected: 208 | virtual void serialize(QDataStream &out) const override; 209 | virtual void parse(QDataStream &in) override; 210 | }; 211 | 212 | /** 213 | * @brief A packet that represents a Curver data post from the Server 214 | */ 215 | class ServerCurverData : public AbstractPacket { 216 | public: 217 | ServerCurverData(); 218 | void fill(); 219 | void extract(); 220 | /** 221 | * @brief The new positions cumulated from every Curver 222 | */ 223 | std::vector pos; 224 | /** 225 | * @brief Whether an individual Curver is changing segments at the moment 226 | */ 227 | std::vector changingSegment; 228 | protected: 229 | virtual void serialize(QDataStream &out) const override; 230 | virtual void parse(QDataStream &in) override; 231 | }; 232 | 233 | /** 234 | * @brief A packet that represents a Curver rotation change coming from the Client 235 | */ 236 | class ClientCurverRotation : public AbstractPacket { 237 | public: 238 | ClientCurverRotation(); 239 | /** 240 | * @brief The wanted rotation of the Curver 241 | */ 242 | Curver::Rotation rotation; 243 | protected: 244 | virtual void serialize(QDataStream &out) const override; 245 | virtual void parse(QDataStream &in) override; 246 | }; 247 | 248 | /** 249 | * @brief A packet that represents an Item event coming from a Server 250 | */ 251 | class ServerItemData : public AbstractPacket { 252 | public: 253 | ServerItemData(); 254 | /** 255 | * @brief Whether the Item was spawned or triggered 256 | */ 257 | bool spawned = true; 258 | /** 259 | * @brief The sequence number uniquely identifying the Item 260 | */ 261 | unsigned int sequenceNumber = 0; 262 | /** 263 | * @brief The kind of Item that spawned 264 | */ 265 | int which = 0; 266 | /** 267 | * @brief The location of the Item 268 | */ 269 | QPointF pos; 270 | /** 271 | * @brief The allowed users of the Item 272 | */ 273 | Item::AllowedUsers allowedUsers = Item::AllowedUsers::ALLOW_ALL; 274 | /** 275 | * @brief If ServeritemData::spawned is false, this value represents the index of the collecting Curver 276 | */ 277 | int collectorIndex = -1; 278 | protected: 279 | virtual void serialize(QDataStream &out) const override; 280 | virtual void parse(QDataStream &in) override; 281 | }; 282 | 283 | /** 284 | * @brief A packet representing a Ping from a client 285 | */ 286 | class Ping : public AbstractPacket { 287 | public: 288 | Ping(); 289 | /** 290 | * @brief The time that the packet was sent at 291 | */ 292 | QTime sent = QTime::currentTime(); 293 | /** 294 | * @brief The last calculated ping of the client 295 | * 296 | * If the client didn't receive a Pong yet, 297 | * then it doesn't know its ping and fallsback to zero. 298 | * 299 | * The ping delta is updated with every Pong received from the server. 300 | */ 301 | qint64 delta = 0; 302 | protected: 303 | virtual void serialize(QDataStream &out) const override; 304 | virtual void parse(QDataStream &in) override; 305 | }; 306 | 307 | /** 308 | * @brief A packet that represents game settings 309 | */ 310 | class ServerSettingsData : public AbstractPacket { 311 | public: 312 | ServerSettingsData(); 313 | void fill(); 314 | void extract(); 315 | /** 316 | * @brief The dimension of the game field 317 | */ 318 | QPoint dimension; 319 | protected: 320 | virtual void serialize(QDataStream &out) const override; 321 | virtual void parse(QDataStream &in) override; 322 | }; 323 | 324 | /** 325 | * @brief A packet that is an answer to Ping 326 | */ 327 | class Pong : public AbstractPacket { 328 | public: 329 | Pong(); 330 | void fill(); 331 | void extract(); 332 | /** 333 | * @brief The time that the original Ping packet was sent at 334 | */ 335 | QTime sent = QTime::currentTime(); 336 | /** 337 | * @brief The index of the client-controlled curver in the server-side array 338 | */ 339 | int curverIndex = -1; 340 | /** 341 | * The ping of each player 342 | */ 343 | std::vector pings; 344 | protected: 345 | virtual void serialize(QDataStream &out) const override; 346 | virtual void parse(QDataStream &in) override; 347 | }; 348 | 349 | } 350 | -------------------------------------------------------------------------------- /src/network/server.cpp: -------------------------------------------------------------------------------- 1 | #include "server.hpp" 2 | 3 | Server::Server() { 4 | connect(&tcpServer, &QTcpServer::acceptError, this, &Server::acceptError); 5 | connect(&tcpServer, &QTcpServer::newConnection, this, &Server::newConnection); 6 | connect(Settings::get(), &Settings::dimensionChanged, this, &Server::broadcastSettings); 7 | connect(&udpSocket, &QUdpSocket::errorOccurred, this, &Server::udpSocketError); 8 | connect(&udpSocket, &QUdpSocket::readyRead, this, &Server::udpSocketReadyRead); 9 | reListen(0); 10 | } 11 | 12 | Server::~Server() { 13 | clients.clear(); 14 | } 15 | 16 | /** 17 | * @brief Broadcasts new Curver data to every Client 18 | */ 19 | void Server::broadcastCurverData() { 20 | if (++dataBroadcastIteration % Settings::get()->getNetworkCurverBlock() == 0) { 21 | Packet::ServerCurverData p; 22 | p.fill(); 23 | p.start = true; 24 | // if reset is due, send reset and reset the reset flag 25 | p.reset = resetDue; 26 | resetDue = false; 27 | broadcastPacket(p, true); 28 | } 29 | } 30 | 31 | /** 32 | * @brief Broadcasts a chat message to every Client 33 | * @param username The author of the message 34 | * @param message The chat message 35 | */ 36 | void Server::broadcastChatMessage(QString username, QString message) { 37 | Packet::ServerChatMsg p; 38 | p.username = username; 39 | p.message = message; 40 | ChatModel::get()->appendMessage(username, message); 41 | broadcastPacket(p); 42 | } 43 | 44 | /** 45 | * @brief Broadcasts an admin chat message to every Client 46 | * @param msg The chat message to broadcast 47 | */ 48 | void Server::broadcastChatMessage(QString msg) { 49 | broadcastChatMessage(ADMIN_NAME, msg); 50 | } 51 | 52 | /** 53 | * @brief Broadcasts the game settings to every Client 54 | */ 55 | void Server::broadcastSettings() { 56 | Packet::ServerSettingsData p; 57 | p.fill(); 58 | broadcastPacket(p); 59 | } 60 | 61 | /** 62 | * @brief Resets the current round 63 | */ 64 | void Server::resetRound() { 65 | resetDue = true; 66 | } 67 | 68 | /** 69 | * @brief Reconfigures the Server to listen on another port 70 | * @param port The new port to listen on 71 | */ 72 | void Server::reListen(quint16 port) { 73 | tcpServer.close(); 74 | tcpServer.listen(QHostAddress::Any, port); 75 | udpSocket.close(); 76 | udpSocket.bind(tcpServer.serverPort()); 77 | qDebug() << "Running on port" << tcpServer.serverPort(); 78 | } 79 | 80 | /** 81 | * @brief Broadcasts the PlayerModel to every Client 82 | */ 83 | void Server::broadcastPlayerModel() { 84 | Packet::ServerPlayerModel p; 85 | p.fill(); 86 | broadcastPacket(p); 87 | } 88 | 89 | /** 90 | * @brief Broadcasts a new Item event to every Client 91 | * @param spawned Whether the Item spawned or was triggered 92 | * @param sequenceNumber The unique sequence number of the Item 93 | * @param which The kind of Item 94 | * @param pos The location of the Item 95 | * @param allowedUsers The allowed users for the Item 96 | * @param collectorIndex If \a spawned is \c false, this value defines which Curver collected the Item 97 | */ 98 | void Server::broadcastItemData(bool spawned, unsigned int sequenceNumber, int which, QPointF pos, Item::AllowedUsers allowedUsers, int collectorIndex) { 99 | Packet::ServerItemData p; 100 | p.spawned = spawned; 101 | p.sequenceNumber = sequenceNumber; 102 | p.which = which; 103 | p.pos = pos; 104 | p.allowedUsers = allowedUsers; 105 | p.collectorIndex = collectorIndex; 106 | broadcastPacket(p); 107 | } 108 | 109 | /** 110 | * @brief This function is called, when an error occurred during accepting an incoming connection 111 | */ 112 | void Server::acceptError(QAbstractSocket::SocketError) { 113 | qDebug() << tcpServer.errorString(); 114 | } 115 | 116 | /** 117 | * @brief This function is called when there is a new connection pending 118 | */ 119 | void Server::newConnection() { 120 | QTcpSocket *s = tcpServer.nextPendingConnection(); 121 | // socket must not be NULL 122 | if (s) { 123 | auto *curver = PlayerModel::get()->getNewPlayer(); 124 | curver->controller = Curver::Controller::CONTROLLER_REMOTE; 125 | clients[s] = curver; 126 | connect(s, &QTcpSocket::errorOccurred, this, &Server::socketError); 127 | connect(s, &QTcpSocket::disconnected, this, &Server::socketDisconnect); 128 | connect(s, &QTcpSocket::readyRead, this, &Server::socketReadyRead); 129 | 130 | // the username is not sent yet, so we cannot pretty print the name yet 131 | broadcastChatMessage(s->peerAddress().toString() + " joined"); 132 | broadcastSettings(); 133 | } 134 | } 135 | 136 | /** 137 | * @brief This function is called, when there was a socket error 138 | */ 139 | void Server::socketError(QAbstractSocket::SocketError) { 140 | QTcpSocket *s = static_cast(sender()); 141 | qDebug() << s->errorString(); 142 | removePlayer(s); 143 | } 144 | 145 | /** 146 | * @brief This function is called, when a socket disconnected 147 | */ 148 | void Server::socketDisconnect() { 149 | QTcpSocket *s = static_cast(sender()); 150 | removePlayer(s); 151 | } 152 | 153 | /** 154 | * @brief This function is called, when there is data available to read from a socket 155 | */ 156 | void Server::socketReadyRead() { 157 | QTcpSocket *s = static_cast(sender()); 158 | QDataStream in(s); 159 | bool illformedPacket = false; 160 | while (s->bytesAvailable() && !illformedPacket) { 161 | in.startTransaction(); 162 | auto packet = Packet::AbstractPacket::receivePacket(in, InstanceType::Client); 163 | if (in.commitTransaction()) { 164 | handlePacket(packet, s); 165 | } else { 166 | qDebug() << "received ill-formed packet"; 167 | illformedPacket = true; 168 | } 169 | } 170 | } 171 | 172 | /** 173 | * @brief Handles an UDP socket error 174 | */ 175 | void Server::udpSocketError(QAbstractSocket::SocketError) { 176 | qDebug() << "UDP error" << udpSocket.errorString(); 177 | } 178 | 179 | /** 180 | * @brief Handles incoming UDP packets 181 | */ 182 | void Server::udpSocketReadyRead() { 183 | while (udpSocket.hasPendingDatagrams()) { 184 | // get datagram 185 | QByteArray datagram; 186 | datagram.resize(udpSocket.pendingDatagramSize()); 187 | QHostAddress sender; 188 | quint16 port; 189 | udpSocket.readDatagram(datagram.data(), datagram.size(), &sender, &port); 190 | FullNetworkAddress client = {sender, port}; 191 | QDataStream udpStream(&datagram, QIODevice::ReadOnly); 192 | udpStream.startTransaction(); 193 | auto packet = Packet::AbstractPacket::receivePacket(udpStream, InstanceType::Client); 194 | if (udpStream.commitTransaction()) { 195 | handlePacket(packet, nullptr, client); 196 | // subscribe client to updates if not yet subscribed 197 | if (std::ranges::find(udpAddresses, client) == udpAddresses.end()) { 198 | udpAddresses.push_back(client); 199 | } 200 | } else { 201 | qInfo() << "Got an ill-formed UDP packet"; 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * @brief Removes a player permanently 208 | * @param s The socket that defines the Curver to remove 209 | */ 210 | void Server::removePlayer(const QTcpSocket *s) { 211 | auto it = std::ranges::find_if(clients, [=](const auto &c) { return c.first == s; }); 212 | if (it != clients.end()) { 213 | auto *c = it->second; 214 | // remove from TCP list (Qt will delete the socket later, when the server shuts down) 215 | clients.erase(it); 216 | // remove from UDP list 217 | auto udpit = std::ranges::find_if(udpAddresses, [&](const auto &a) { return a.addr == s->peerAddress(); }); 218 | if (udpit != udpAddresses.cend()) { 219 | udpAddresses.erase(udpit); 220 | } 221 | // remove from player model 222 | PlayerModel::get()->removeCurver(c); 223 | broadcastChatMessage(curverNetworkName(s, c) + " left the game"); 224 | } 225 | } 226 | 227 | /** 228 | * @brief Processes an already received packet 229 | * @param p The packet to process 230 | * @param s The socket that the packet was received with 231 | * @param sender The sender of the packet, if sent via UDP 232 | */ 233 | void Server::handlePacket(std::unique_ptr &p, const QTcpSocket *s, FullNetworkAddress sender) { 234 | Curver *curver = nullptr; 235 | if (s != nullptr) { 236 | curver = curverFromSocket(s); 237 | } 238 | // packet types that require curver to be set 239 | const std::array needsCurver = { 240 | Packet::ClientTypes::Chat_Message, 241 | Packet::ClientTypes::PlayerModelEdit, 242 | Packet::ClientTypes::CurverRotation, 243 | }; 244 | const auto packetType = static_cast(p->type); 245 | if (curver == nullptr && std::ranges::find(needsCurver, packetType) != needsCurver.end()) { 246 | qDebug() << "curver is not set"; 247 | return; 248 | } 249 | // TODO: Deal with flags 250 | switch (packetType) { 251 | case Packet::ClientTypes::Chat_Message: 252 | { 253 | QString msg = ((Packet::ClientChatMsg *) p.get())->message; 254 | broadcastChatMessage(curver->userName, msg); 255 | break; 256 | } 257 | case Packet::ClientTypes::PlayerModelEdit: 258 | { 259 | auto *playerData = (Packet::ClientPlayerModel *) p.get(); 260 | curver->userName = playerData->username; 261 | curver->setColor(playerData->color); 262 | PlayerModel::get()->forceRefresh(); 263 | break; 264 | } 265 | case Packet::ClientTypes::CurverRotation: 266 | { 267 | if (curver) { 268 | curver->rotation = ((Packet::ClientCurverRotation *) p.get())->rotation; 269 | } 270 | break; 271 | } 272 | case Packet::ClientTypes::Ping: 273 | { 274 | auto *pingPacket = (Packet::Ping *) p.get(); 275 | // respond with pong 276 | Packet::Pong pongPacket; 277 | pongPacket.sent = pingPacket->sent; 278 | pongPacket.curverIndex = getCurverIndex(sender); 279 | 280 | if (pongPacket.curverIndex != -1) { 281 | // store current ping of this player 282 | PlayerModel::get()->getCurvers()[pongPacket.curverIndex]->ping = pingPacket->delta; 283 | PlayerModel::get()->forceRefresh(); 284 | } 285 | 286 | pongPacket.fill(); 287 | pongPacket.sendPacketUdp(&udpSocket, sender); 288 | break; 289 | } 290 | default: 291 | qDebug() << "Unsupported packet type"; 292 | break; 293 | } 294 | } 295 | 296 | /** 297 | * @brief Broadcasts a packet to every Client 298 | * @param p The packet to broadcast 299 | * @param udp Whether to broadcast using UDP or TCP 300 | */ 301 | void Server::broadcastPacket(Packet::AbstractPacket &p, bool udp) { 302 | if (udp) { 303 | std::ranges::for_each(udpAddresses, [&](auto &c) { p.sendPacketUdp(&udpSocket, c); }); 304 | } else { 305 | std::ranges::for_each(clients, [&](auto &c) { p.sendPacket(c.first); }); 306 | } 307 | } 308 | 309 | /** 310 | * @brief Computes the index belonging to a client 311 | * @param peer The address of the client 312 | * @return The index in the server-side curver array 313 | */ 314 | int Server::getCurverIndex(const FullNetworkAddress peer) { 315 | auto it = std::ranges::find_if(clients, [&](const auto &p) { return p.first->peerAddress() == peer.addr; }); 316 | if (it != clients.cend()) { 317 | auto &curvers = PlayerModel::get()->getCurvers(); 318 | for (size_t i = 0; i < curvers.size(); ++i) { 319 | if (curvers[i].get() == it->second) { 320 | return i; 321 | } 322 | } 323 | } 324 | qDebug() << "Client index not found" << peer.addr << peer.port; 325 | return -1; 326 | } 327 | 328 | /** 329 | * @brief Returns a pretty-printed name for a network user 330 | * @param s The socket corresponding to the user 331 | * @param curver The curver corresponding to the user 332 | * @return A pretty-printed name 333 | */ 334 | QString Server::curverNetworkName(const QTcpSocket *s, const Curver *curver) { 335 | return QString("%1 (%2)").arg(s->peerAddress().toString()).arg(curver->userName); 336 | } 337 | 338 | /** 339 | * @brief Returns the Curver connected to a given socket 340 | * @param s The socket to return the Curver of 341 | * @return The Curver that belongs to \a s 342 | */ 343 | Curver *Server::curverFromSocket(const QTcpSocket *s) const { 344 | auto it = std::ranges::find_if(clients, [&](const auto &c) { return c.first == s; }); 345 | if (it != clients.cend()) { 346 | return it->second; 347 | } else { 348 | return nullptr; 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/network/server.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "network.hpp" 11 | 12 | #define ADMIN_NAME "Chat Bot" 13 | 14 | /** 15 | * @brief A Server network instance 16 | * 17 | * Handles the network communication from the server side 18 | */ 19 | class Server : public QObject { 20 | Q_OBJECT 21 | public: 22 | explicit Server(); 23 | ~Server(); 24 | 25 | void broadcastCurverData(); 26 | void broadcastChatMessage(QString username, QString message); 27 | void broadcastChatMessage(QString msg); 28 | void broadcastSettings(); 29 | void resetRound(); 30 | void reListen(quint16 port); 31 | public slots: 32 | void broadcastPlayerModel(); 33 | void broadcastItemData(bool spawned, unsigned int sequenceNumber, int which, QPointF pos, Item::AllowedUsers allowedUsers, int collectorIndex); 34 | private slots: 35 | // tcpServer 36 | void acceptError(QAbstractSocket::SocketError); 37 | void newConnection(); 38 | // tcpSocket 39 | void socketError(QAbstractSocket::SocketError); 40 | void socketDisconnect(); 41 | void socketReadyRead(); 42 | // udpSocket 43 | void udpSocketError(QAbstractSocket::SocketError); 44 | void udpSocketReadyRead(); 45 | private: 46 | void removePlayer(const QTcpSocket *s); 47 | void handlePacket(std::unique_ptr &p, const QTcpSocket *s = nullptr, FullNetworkAddress sender = {}); 48 | void broadcastPacket(Packet::AbstractPacket &p, bool udp = false); 49 | int getCurverIndex(const FullNetworkAddress peer); 50 | QString curverNetworkName(const QTcpSocket *s, const Curver *curver); 51 | /** 52 | * @brief The server instance that handles every incoming connection 53 | */ 54 | QTcpServer tcpServer; 55 | /** 56 | * @brief The UDP server 57 | */ 58 | QUdpSocket udpSocket; 59 | /** 60 | * @brief The mapping between TCP sockets and their Curver instances 61 | * 62 | * We do not need to delete the QTcpSocket instances, Qt manages the lifetime of them and will delete them once the server goes down. 63 | */ 64 | std::map clients; 65 | Curver *curverFromSocket(const QTcpSocket *s) const; 66 | /** 67 | * @brief Whether the round has to be reset 68 | * 69 | * Usually the next update of Curver data resets this flag, after sending the reset bit to every Client 70 | */ 71 | bool resetDue = false; 72 | /** 73 | * @brief The amount of times that curver data was broadcasted 74 | * 75 | * This value is used together with Settings::networkCurverBlock to reduce used network bandwidth 76 | */ 77 | unsigned dataBroadcastIteration = 0; 78 | /** 79 | * @brief The UDP addresses from all clients 80 | */ 81 | std::vector udpAddresses; 82 | }; 83 | -------------------------------------------------------------------------------- /src/qml/About.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | 4 | Page { 5 | Licenses { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/qml/Chat.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls.Material 4 | import Quartz 5 | 6 | import Backend 7 | 8 | Item { 9 | ColumnLayout { 10 | anchors.fill: parent 11 | anchors.margins: 8 12 | ListView { 13 | header: Label { 14 | text: "Chat" 15 | } 16 | clip: true 17 | model: ChatModel 18 | delegate: RowLayout { 19 | width: parent.width 20 | Label { 21 | id: usernameLabel 22 | text: model.username + ":" 23 | } 24 | Label { 25 | text: model.message 26 | } 27 | Item { 28 | Layout.fillWidth: true 29 | } 30 | Label { 31 | text: Qt.formatDateTime(model.timestamp, "HH:mm") 32 | } 33 | } 34 | Layout.fillHeight: true 35 | Layout.fillWidth: true 36 | onCountChanged: currentIndex = count - 1; 37 | } 38 | RowLayout { 39 | TextField { 40 | id: messageTextField 41 | Layout.fillWidth: true 42 | onAccepted: sendButton.send(); 43 | } 44 | IconButton { 45 | id: sendButton 46 | function send() { 47 | game.sendChatMessage(messageTextField.text); 48 | messageTextField.text = ""; 49 | if (game.isStarted) { 50 | game.forceActiveFocus(); 51 | } 52 | } 53 | ico.name: "send" 54 | ico.color: Material.accent 55 | onClicked: send(); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/qml/Main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Window 3 | import QtQuick.Controls.Material 4 | import QtQuick.Layouts 5 | import QtQuick.Dialogs 6 | import Quartz 7 | 8 | import Game 9 | import Client 10 | import Backend 11 | 12 | ApplicationWindow { 13 | property int connectedToServer: game ? game.client.joinStatus === Client.JOINED : 0 14 | onClosing: { 15 | game.destroy(); 16 | } 17 | id: root 18 | visible: true 19 | width: 1200 20 | height: 900 21 | title: "Quick Curver" 22 | Material.theme: Material.System 23 | Material.primary: Material.Yellow 24 | Material.accent: Material.Blue 25 | onWidthChanged: game.checkDimension(); 26 | Action { 27 | shortcut: StandardKey.Quit 28 | onTriggered: close(); 29 | } 30 | Item { 31 | id: initialPage 32 | anchors.fill: parent 33 | SplitView { 34 | anchors.fill: parent 35 | MouseArea { 36 | id: gameWave 37 | SplitView.preferredWidth: Settings.width 38 | Rectangle { 39 | anchors.fill: parent 40 | color: Material.color(Material.BlueGrey, Material.Shade900) 41 | } 42 | Game { 43 | id: game 44 | anchors.fill: parent 45 | property int realWidth: Settings.width 46 | property int realHeight: Settings.height 47 | function checkDimension() { 48 | if (!root.connectedToServer) { 49 | // we are manually resizing anyway 50 | return; 51 | } 52 | if (realWidth > width || realHeight > height || width > root.width) { 53 | resizeSnackbar.display("The server has set a larger game size.", "Resize automatically"); 54 | } else { 55 | resizeSnackbar.close(); 56 | } 57 | } 58 | onWidthChanged: { 59 | if (!root.connectedToServer) { 60 | Settings.width = width; 61 | } 62 | checkDimension(); 63 | } 64 | onHeightChanged: { 65 | if (!root.connectedToServer) { 66 | Settings.height = height; 67 | } 68 | checkDimension(); 69 | } 70 | onRealWidthChanged: checkDimension(); 71 | onRealHeightChanged: checkDimension(); 72 | onPostInfoBar: (msg) => infoBar.display(msg); 73 | Keys.onPressed: (event) => { 74 | game.processKey(event.key, false); 75 | } 76 | Keys.onReleased: (event) => { 77 | game.processKey(event.key, true); 78 | } 79 | onGameStarted: { 80 | game.forceActiveFocus(); 81 | /* gameWave.openWave(); */ 82 | } 83 | } 84 | MouseArea { 85 | anchors.fill: parent 86 | onClicked: game.forceActiveFocus(); 87 | } 88 | MouseArea { 89 | enabled: Backend.isMobile 90 | anchors.fill: parent 91 | anchors.rightMargin: parent.width / 2 92 | onPressedChanged: game.processKey(Qt.Key_Left, !pressed); 93 | } 94 | MouseArea { 95 | enabled: Backend.isMobile 96 | anchors.fill: parent 97 | anchors.leftMargin: parent.width / 2 98 | onPressedChanged: game.processKey(Qt.Key_Right, !pressed); 99 | } 100 | } 101 | Item { 102 | Item { 103 | id: options 104 | height: optionsLayout.implicitHeight 105 | anchors {top: parent.top; left: parent.left; right: parent.right; margins: 8} 106 | RowLayout { 107 | id: optionsLayout 108 | anchors {left: parent.left; right: parent.right} 109 | IconButton { 110 | ico.name: "device_reset" 111 | enabled: !root.connectedToServer 112 | onClicked: game.resetGame(); 113 | ToolTip.text: "Reset game" 114 | ToolTip.visible: pressed 115 | ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval 116 | } 117 | IconButton { 118 | id: joinDialogButton 119 | ico.name: "cloud_upload" 120 | onClicked: clientDialog.open(); 121 | ToolTip.text: "Join game" 122 | ToolTip.visible: pressed 123 | ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval 124 | Shortcut { 125 | sequence: "Ctrl+J" 126 | onActivated: joinDialogButton.clicked(); 127 | } 128 | } 129 | IconButton { 130 | id: settingsDialogButton 131 | ico.name: "settings" 132 | enabled: !root.connectedToServer 133 | onClicked: pageStack.push(Qt.resolvedUrl("Settings.qml")) 134 | ToolTip.text: "Settings" 135 | ToolTip.visible: pressed 136 | ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval 137 | Shortcut { 138 | sequence: "Ctrl+I" 139 | onActivated: settingsDialogButton.clicked(); 140 | } 141 | } 142 | IconButton { 143 | ico.name: "info" 144 | onClicked: licenseDialog.open(); 145 | ToolTip.text: "About" 146 | ToolTip.visible: pressed 147 | ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval 148 | } 149 | IconButton { 150 | id: startButton 151 | Layout.fillWidth: true 152 | ico.name: "assistant_navigation" 153 | enabled: !root.connectedToServer && (game ? !game.isStarted : false) 154 | text: "Start!" 155 | highlighted: true 156 | onClicked: { 157 | game.startGame(); 158 | } 159 | } 160 | MessageDialog { 161 | id: licenseDialog 162 | title: "About" 163 | informativeText: "This software is free software licensed under the GNU GPL3.\nThe source code repository is available at https://github.com/vimpostor/quickcurver\nYou can also report any issues on the same website." 164 | buttons: MessageDialog.Ok 165 | } 166 | } 167 | } 168 | Chat { 169 | id: chat 170 | anchors {top: options.bottom; left: parent.left; right: parent.right; margins: 8} 171 | height: parent.height / 3 172 | } 173 | Players { 174 | id: players 175 | anchors {top: chat.bottom; left: parent.left; right: parent.right; bottom: parent.bottom; margins: 8} 176 | } 177 | } 178 | } 179 | StackView { 180 | id: pageStack 181 | anchors.fill: parent 182 | } 183 | Snackbar { 184 | id: infoBar 185 | } 186 | Snackbar { 187 | id: resizeSnackbar 188 | timeout: 10000 189 | action: "Resize" 190 | onClicked: { 191 | gameWave.width = Math.min(Settings.width + 16, Screen.width - 16); 192 | root.height = Math.min(Settings.height + 3 * 16, Screen.height - 16); 193 | if (root.width < gameWave.width) { 194 | root.width = gameWave.width + 14 * 16; 195 | } 196 | } 197 | } 198 | Dialog { 199 | property int joinStatus: game ? game.client.joinStatus : 0 200 | 201 | id: clientDialog 202 | title: "Join game" 203 | x: (parent.width - width)/2 204 | y: (parent.height - height)/2 205 | onJoinStatusChanged: { 206 | if (joinStatus === Client.JOINED) { 207 | clientDialog.accept(); 208 | } 209 | } 210 | Column { 211 | width: 150 212 | spacing: 8 213 | TextField { 214 | id: nameTextField 215 | anchors.left: parent.left 216 | anchors.right: parent.right 217 | placeholderText: "Username" 218 | text: "Client" 219 | } 220 | IconButton { 221 | ico.name: "color_lens" 222 | ico.color: clientColorDialog.selectedColor 223 | onClicked: clientColorDialog.open(); 224 | ColorDialog { 225 | id: clientColorDialog 226 | selectedColor: Material.accent 227 | } 228 | } 229 | TextField { 230 | id: ipTextField 231 | anchors.left: parent.left 232 | anchors.right: parent.right 233 | placeholderText: "IP (IPv4 or IPv6)" 234 | text: "127.0.0.1" 235 | } 236 | TextField { 237 | id: portTextField 238 | anchors.left: parent.left 239 | anchors.right: parent.right 240 | placeholderText: "Port" 241 | onAccepted: buttonJoin.clicked(); 242 | } 243 | Button { 244 | id: buttonJoin 245 | enabled: clientDialog.joinStatus === Client.NONE || clientDialog.joinStatus === Client.FAILED || clientDialog.joinStatus === Client.JOINED 246 | text: enabled ? "Join" : clientDialog.joinStatus === Client.DNS_PENDING ? "Looking up hostname..." : clientDialog.joinStatus === Client.TCP_PENDING ? "Connecting TCP..." : clientDialog.joinStatus === Client.UDP_PENDING ? "Connecting UDP..." : "Connected" 247 | anchors.left: parent.left 248 | anchors.right: parent.right 249 | onClicked: { 250 | Settings.setClientName(nameTextField.text); 251 | Settings.setClientColor(clientColorDialog.selectedColor); 252 | game.connectToHost(ipTextField.text, portTextField.text); 253 | } 254 | } 255 | BusyIndicator { 256 | id: busyIndicatorJoining 257 | running: !buttonJoin.enabled 258 | visible: running 259 | anchors.horizontalCenter: parent.horizontalCenter 260 | } 261 | } 262 | onOpened: portTextField.forceActiveFocus(); 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/qml/Players.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Dialogs 3 | import QtQuick.Layouts 4 | import QtQuick.Controls.Material 5 | import Quartz 6 | 7 | import Backend 8 | 9 | Item { 10 | id: playersRoot 11 | ListView { 12 | id: playerListView 13 | header: Label { 14 | text: "Players" 15 | } 16 | clip: true 17 | property int modelIndex: 0 18 | function open(index, controller) { 19 | modelIndex = index; 20 | bottomSheet.playerEditable = controller !== 1; 21 | if (root.connectedToServer) { 22 | bottomSheet.playerEditable = false; 23 | } 24 | if (controller !== 1) { 25 | botCheckbox.checked = controller === 2; 26 | } 27 | bottomSheet.open(); 28 | } 29 | anchors.fill: parent 30 | model: PlayerModel 31 | delegate: Item { 32 | height: playerIcon.implicitHeight 33 | width: ListView.view.width 34 | IconButton { 35 | id: playerIcon 36 | ico.name: model.controller === 0 ? "supervised_user_circle" : model.controller === 2 ? "robot" : "cloud_sync" 37 | anchors.left: parent.left 38 | text: model.name + " " + model.totalScore + "(+" + model.roundScore + ")" 39 | onClicked: playerListView.open(index, model.controller); 40 | } 41 | Label { 42 | property real pingFactor: Math.min(500, model.ping) / 500 43 | text: model.ping 44 | visible: model.ping 45 | color: Qt.rgba(pingFactor, (1 - pingFactor), 0, 1) 46 | Behavior on color { 47 | ColorAnimation { 48 | duration: 800 49 | easing.type: Easing.OutCubic 50 | } 51 | } 52 | anchors.verticalCenter: parent.verticalCenter 53 | anchors.right: parent.right 54 | } 55 | } 56 | Dialog { 57 | id: bottomSheet 58 | property bool playerEditable: true 59 | title: qsTr("Edit Player") 60 | width: 300 61 | ColumnLayout { 62 | IconButton { 63 | text: "Edit name" 64 | enabled: bottomSheet.playerEditable 65 | ico.name: "user_attributes" 66 | onClicked: inputDialog.open(); 67 | } 68 | IconButton { 69 | text: "Edit color" 70 | enabled: bottomSheet.playerEditable 71 | ico.name: "color_lens" 72 | onClicked: colorDialog.open(); 73 | } 74 | IconButton { 75 | text: "Set counterclockwise key" 76 | enabled: bottomSheet.playerEditable 77 | ico.name: "rotate_left" 78 | onClicked: { 79 | infoBar.open("Press a key!"); 80 | leftKeyItem.forceActiveFocus(); 81 | } 82 | } 83 | IconButton { 84 | text: "Set clockwise key" 85 | enabled: bottomSheet.playerEditable 86 | ico.name: "rotate_right" 87 | onClicked: { 88 | infoBar.open("Press a key!"); 89 | rightKeyItem.forceActiveFocus(); 90 | } 91 | } 92 | IconButton { 93 | text: "Bot Settings" 94 | enabled: bottomSheet.playerEditable 95 | ico.name: "robot" 96 | onClicked: botDialog.open(); 97 | } 98 | IconButton { 99 | text: "Delete" 100 | enabled: !root.connectedToServer 101 | ico.name: "delete" 102 | onClicked: { 103 | PlayerModel.removePlayer(playerListView.modelIndex); 104 | bottomSheet.close(); 105 | } 106 | } 107 | } 108 | } 109 | Dialog { 110 | id: inputDialog 111 | title: "Player name" 112 | standardButtons: Dialog.Ok | Dialog.Cancel 113 | ColumnLayout { 114 | TextField { 115 | id: textField 116 | } 117 | } 118 | onAccepted: PlayerModel.setUserName(playerListView.modelIndex, textField.text); 119 | } 120 | ColorDialog { 121 | id: colorDialog 122 | onAccepted: PlayerModel.setColor(playerListView.modelIndex, selectedColor); 123 | } 124 | Dialog { 125 | id: botDialog 126 | RowLayout { 127 | Label { 128 | text: qsTr("Controlled by AI") 129 | height: 65 130 | } 131 | CheckBox { 132 | id: botCheckbox 133 | onCheckedChanged: PlayerModel.setController(playerListView.modelIndex, 2 * botCheckbox.checked); 134 | } 135 | } 136 | } 137 | Item { 138 | id: leftKeyItem 139 | visible: false 140 | Keys.onPressed: (event) => { 141 | PlayerModel.setLeftKey(playerListView.modelIndex, event.key); 142 | game.forceActiveFocus(); 143 | } 144 | } 145 | Item { 146 | id: rightKeyItem 147 | visible: false 148 | Keys.onPressed: (event) => { 149 | PlayerModel.setRightKey(playerListView.modelIndex, event.key); 150 | game.forceActiveFocus(); 151 | } 152 | } 153 | } 154 | FloatingActionButton { 155 | id: addPlayerButton 156 | enabled: !root.connectedToServer 157 | anchors.bottom: parent.bottom 158 | anchors.right: parent.right 159 | anchors.margins: 16 160 | name: "person_add" 161 | onClicked: { 162 | PlayerModel.appendPlayer(); 163 | game.forceActiveFocus(); 164 | } 165 | } 166 | FloatingActionButton { 167 | enabled: !root.connectedToServer 168 | anchors.bottom: addPlayerButton.top 169 | anchors.horizontalCenter: addPlayerButton.horizontalCenter 170 | anchors.margins: 16 171 | size: FloatingActionButton.Size.Small 172 | name: "robot" 173 | onClicked: { 174 | PlayerModel.appendBot(); 175 | game.forceActiveFocus(); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/qml/Settings.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls.Material 3 | import QtQuick.Layouts 4 | import Quartz 5 | 6 | import Backend 7 | 8 | Page { 9 | id: settings 10 | title: "Settings" 11 | IconButton { 12 | id: closeButton 13 | anchors.left: parent.left 14 | anchors.top: parent.top 15 | ico.name: "arrow_back" 16 | onClicked: pageStack.clear(); 17 | } 18 | TabBar { 19 | id: bar 20 | anchors.left: closeButton.right 21 | anchors.right: parent.right 22 | TabButton { 23 | text: "General" 24 | } 25 | TabButton { 26 | text: "Item spawn probabilities" 27 | } 28 | } 29 | StackLayout { 30 | anchors {top: bar.bottom; left: parent.left; right: parent.right; bottom: parent.bottom} 31 | currentIndex: bar.currentIndex 32 | Item { 33 | GridLayout { 34 | anchors.centerIn: parent 35 | columns: 2 36 | Label { 37 | text: "Round time out" 38 | } 39 | Slider { 40 | height: 24 41 | value: Settings.getRoundTimeOut(); 42 | from: 0 43 | to: 5000 44 | onValueChanged: Settings.setRoundTimeOut(value); 45 | } 46 | Label { 47 | text: "Item spawn" 48 | } 49 | RangeSlider { 50 | height: 24 51 | from: 50 52 | to: 10000 53 | first.value: Settings.getItemSpawnIntervalMin(); 54 | second.value: Settings.getItemSpawnIntervalMax(); 55 | first.onValueChanged: Settings.setItemSpawnIntervalMin(first.value); 56 | second.onValueChanged: Settings.setItemSpawnIntervalMax(second.value); 57 | } 58 | Label { 59 | text: "Score to win" 60 | } 61 | TextField { 62 | text: Settings.getTargetScore() 63 | inputMethodHints: Qt.ImhDigitsOnly 64 | onTextChanged: Settings.setTargetScore(text); 65 | } 66 | Label { 67 | text: "Logic update rate" 68 | } 69 | Slider { 70 | height: 24 71 | value: Settings.getUpdatesPerSecond(); 72 | from: 30 73 | to: 144 74 | snapMode: Slider.SnapAlways 75 | stepSize: 1 76 | onValueChanged: Settings.setUpdatesPerSecond(value); 77 | } 78 | Label { 79 | text: "Network update rate" 80 | } 81 | Slider { 82 | height: 24 83 | value: Settings.getNetworkCurverBlock(); 84 | from: 1 85 | to: 8 86 | snapMode: Slider.SnapAlways 87 | stepSize: 1 88 | onValueChanged: Settings.setNetworkCurverBlock(value); 89 | } 90 | } 91 | } 92 | Item { 93 | ListView { 94 | id: itemListView 95 | anchors.fill: parent 96 | anchors.margins: 8 97 | model: ItemModel 98 | spacing: 8 99 | delegate: RowLayout { 100 | width: parent.width 101 | IconButton { 102 | ico.name: model.iconName 103 | } 104 | Label { 105 | text: model.name + " (" + model.description + ")" 106 | Layout.fillWidth: true 107 | } 108 | Slider { 109 | id: probabilitySlider 110 | value: model.probability 111 | onValueChanged: ItemModel.setProbability(index, value); 112 | } 113 | ComboBox { 114 | model: ListModel { 115 | ListElement { name: "Allow all" } 116 | ListElement { name: "Allow others" } 117 | ListElement { name: "Allow collector" } 118 | } 119 | implicitWidth: 160 120 | currentIndex: allowedUsers 121 | onCurrentIndexChanged: ItemModel.setAllowedUsers(index, currentIndex); 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/segment.cpp: -------------------------------------------------------------------------------- 1 | #include "segment.hpp" 2 | 3 | /** 4 | * @brief Constructs a Segment with the given parent node, material and thickness 5 | * @param parentNode The parent node in the scene graph 6 | * @param material The material to use for all drawing calls 7 | * @param thickness The thickness of the segment 8 | */ 9 | Segment::Segment(QSGNode *parentNode, QSGFlatColorMaterial *material, const float thickness) { 10 | this->parentNode = parentNode; 11 | this->thickness = thickness; 12 | 13 | geometry.setLineWidth(thickness); 14 | geometry.setDrawingMode(QSGGeometry::DrawTriangleStrip); 15 | geoNode.setGeometry(&geometry); 16 | geoNode.setMaterial(material); 17 | geometry.allocate(1); 18 | parentNode->appendChildNode(&geoNode); 19 | } 20 | 21 | Segment::~Segment() { 22 | parentNode->removeChildNode(&geoNode); 23 | } 24 | 25 | /** 26 | * @brief Appends a new point to the segment 27 | * @param newPoint The point to append 28 | * @param angle The angle to append the point with. The thickness of the line spreads orthogonal relative to the angle 29 | */ 30 | void Segment::appendPoint(const QPointF newPoint, const float angle) { 31 | const float normalAngle = angle + M_PI / 2; 32 | const QPointF normalVector = thickness * QPointF(cos(normalAngle), sin(normalAngle)); 33 | pos.push_back(newPoint + normalVector); 34 | pos.push_back(newPoint - normalVector); 35 | updateGeometry(); 36 | 37 | lastPoint = newPoint; 38 | } 39 | 40 | /** 41 | * @brief Checks if this segment collides with a line from a to b 42 | * @param a The start point of the line 43 | * @param b The end point of the line 44 | * @return \c True, iif this segment intersects with the line a -> b 45 | */ 46 | bool Segment::checkForIntersection(QPointF a, QPointF b) const { 47 | /* Given a line (a -- b) and (c -- d), we find an intersection as follows: 48 | * 49 | * First compute the equation A*x + B*y = C for both lines 50 | * A = b.y - a.y 51 | * B = a.x - b.x 52 | * C = A*a.x + B*a.y 53 | * 54 | * Then for two lines 1 and 2 in this form, first check if 55 | * det := A1*B2 - A2*B1 == 0 => Lines are parallel 56 | * If this is the case, check if the lines intersect using a bounding rectangle 57 | * 58 | * Otherwise compute the location of the intersection as follows: 59 | * x := (B2*C1 - B1*C2)/det 60 | * y := (A1*C2 - A2*C1)/det 61 | * 62 | * Finally check if this intersection location is contained in both lines 63 | */ 64 | // epsilon is needed, because floating point operations are not that nice, when it comes to comparing them 65 | const float epsilon = 0.015625; 66 | const float firstA = b.y() - a.y(); 67 | const float firstB = a.x() - b.x(); 68 | const float firstC = firstA * a.x() + firstB * a.y(); 69 | const float minX = std::min(a.x(), b.x()) - epsilon; 70 | const float maxX = std::max(a.x(), b.x()) + epsilon; 71 | const float minY = std::min(a.y(), b.y()) - epsilon; 72 | const float maxY = std::max(a.y(), b.y()) + epsilon; 73 | for (int i = 2; i < static_cast(pos.size() - 1); ++i) { 74 | const QPointF c = pos[i - 2]; 75 | const QPointF d = pos[i]; 76 | const float secondA = d.y() - c.y(); 77 | const float secondB = c.x() - d.x(); 78 | const float secondC = secondA * c.x() + secondB * c.y(); 79 | const float det = firstA * secondB - secondA * firstB; 80 | if (static_cast(det)) { 81 | // not parallel 82 | const float x = (secondB * firstC - firstB * secondC) / det; 83 | const float y = (firstA * secondC - secondA * firstC) / det; 84 | // is the intersection location contained in both lines? 85 | if (minX <= x && x <= maxX && minY <= y && y <= maxY && 86 | (std::min(c.x(), d.x()) - epsilon) <= x && (x <= std::max(c.x(), d.x()) + epsilon) && 87 | (std::min(c.y(), d.y()) - epsilon) <= y && (y <= std::max(c.y(), d.y()) + epsilon)) { 88 | return true; 89 | } 90 | } 91 | } 92 | return false; 93 | } 94 | 95 | /** 96 | * @brief Returns the size of the segment 97 | * @return The total internal amount of points stored in this segment 98 | */ 99 | size_t Segment::getSegmentSize() const { 100 | return pos.size(); 101 | } 102 | 103 | /** 104 | * @brief Removes points from the beginning of the segment 105 | * @param amount The number of points to remove 106 | */ 107 | void Segment::popPoints(const size_t amount) { 108 | if (!amount) { 109 | return; 110 | } 111 | pos.erase(pos.begin(), pos.begin() + amount); 112 | updateGeometry(); 113 | } 114 | 115 | /** 116 | * @brief Removes all points from this segment 117 | */ 118 | void Segment::clear() { 119 | pos.clear(); 120 | updateGeometry(); 121 | } 122 | 123 | /** 124 | * @brief Returns the last position in this segment 125 | * @return The last position or \c std::nullopt, if no position exists 126 | */ 127 | std::optional Segment::getFirstPos() const { 128 | if (getSegmentSize()) { 129 | return pos.front(); 130 | } else { 131 | return std::nullopt; 132 | } 133 | } 134 | 135 | /** 136 | * @brief Updates the geometry of the geometry node and flags it dirty for the renderer 137 | */ 138 | void Segment::updateGeometry() { 139 | geometry.allocate(pos.size()); 140 | QSGGeometry::Point2D *vertices = geometry.vertexDataAsPoint2D(); 141 | for (size_t i = 0; i < pos.size(); ++i) { 142 | vertices[i].set(pos[i].x(), pos[i].y()); 143 | } 144 | geoNode.markDirty(QSGNode::DirtyGeometry); 145 | } 146 | -------------------------------------------------------------------------------- /src/segment.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /** 11 | * @brief A class representing a segment of a line 12 | * 13 | * Every Curver consists of multiple Segment lines. This class represents a single instance of such a line. 14 | */ 15 | class Segment : public QObject { 16 | Q_OBJECT 17 | public: 18 | explicit Segment(QSGNode *parentNode, QSGFlatColorMaterial *material, const float thickness); 19 | ~Segment(); 20 | 21 | void appendPoint(const QPointF newPoint, const float angle); 22 | bool checkForIntersection(QPointF a, QPointF b) const; 23 | size_t getSegmentSize() const; 24 | void popPoints(const size_t amount); 25 | void clear(); 26 | std::optional getFirstPos() const; 27 | private: 28 | void updateGeometry(); 29 | 30 | /** 31 | * @brief The parent node in the scene graph 32 | */ 33 | QSGNode *parentNode; 34 | /** 35 | * @brief The thickness of the line 36 | */ 37 | float thickness; 38 | /** 39 | * @brief The node representing this Segment in the scene graph 40 | */ 41 | QSGGeometryNode geoNode; 42 | /** 43 | * @brief The geometry of this Segment. 44 | */ 45 | QSGGeometry geometry = QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); 46 | /** 47 | * @brief Every position that has to be stored in Segment::geometry. 48 | */ 49 | std::vector pos; 50 | /** 51 | * @brief The last point that was added to this Segment. 52 | */ 53 | QPointF lastPoint; 54 | }; 55 | -------------------------------------------------------------------------------- /src/settings.cpp: -------------------------------------------------------------------------------- 1 | #include "settings.hpp" 2 | 3 | /** 4 | * @brief Sets the dimension of the game 5 | * @param dimension The new dimension 6 | */ 7 | void Settings::setDimension(QPoint dimension) { 8 | this->dimension = dimension; 9 | widthChanged(dimension.x()); 10 | heightChanged(dimension.y()); 11 | dimensionChanged(); 12 | } 13 | 14 | /** 15 | * @brief Returns the current dimension of the game 16 | * @return The current dimension 17 | */ 18 | QPoint Settings::getDimension() const { 19 | return dimension; 20 | } 21 | 22 | /** 23 | * @brief Sets the width only of the game 24 | * @param width The new width 25 | */ 26 | void Settings::setWidth(int width) { 27 | dimension.setX(width); 28 | setDimension(dimension); 29 | } 30 | 31 | /** 32 | * @brief Returns the width of the game 33 | * @return The width 34 | */ 35 | int Settings::getWidth() const { 36 | return dimension.x(); 37 | } 38 | 39 | /** 40 | * @brief Sets the height only of the game 41 | * @param height The new height 42 | */ 43 | void Settings::setHeight(int height) { 44 | dimension.setY(height); 45 | setDimension(dimension); 46 | } 47 | 48 | /** 49 | * @brief Returns the height of the game 50 | * @return The height of the game 51 | */ 52 | int Settings::getHeight() const { 53 | return dimension.y(); 54 | } 55 | 56 | /** 57 | * @brief Sets the timeout waiting for the new round after the last one was finished 58 | * @param roundTimeOut The new timeout 59 | */ 60 | void Settings::setRoundTimeOut(int roundTimeOut) { 61 | this->roundTimeOut = roundTimeOut; 62 | } 63 | 64 | /** 65 | * @brief Returns the current round timeout 66 | * @return The round timeout 67 | */ 68 | int Settings::getRoundTimeOut() const { 69 | return roundTimeOut; 70 | } 71 | 72 | /** 73 | * @brief Sets the minimum amount of time that item spawns are apart 74 | * @param interval The new minimum amount of time 75 | */ 76 | void Settings::setItemSpawnIntervalMin(const int interval) { 77 | this->itemSpawnIntervalMin = interval; 78 | } 79 | 80 | /** 81 | * @brief Returns the current minimum amount of time that item spawns are apart 82 | * @return Settings::itemSpawnIntervalMin 83 | */ 84 | int Settings::getItemSpawnIntervalMin() const { 85 | return itemSpawnIntervalMin; 86 | } 87 | 88 | /** 89 | * @brief Sets the maximum amount of time that item spawns are apart 90 | * @param interval The new maximum amount of time 91 | */ 92 | void Settings::setItemSpawnIntervalMax(const int interval) { 93 | this->itemSpawnIntervalMax = interval; 94 | } 95 | 96 | /** 97 | * @brief Returns hte current maximum amount of time that item spawns are apart 98 | * @return Settings::itemSpawnIntervalMax 99 | */ 100 | int Settings::getItemSpawnIntervalMax() const { 101 | return itemSpawnIntervalMax; 102 | } 103 | 104 | /** 105 | * @brief Sets the username of the client 106 | * @param name The new username 107 | */ 108 | void Settings::setClientName(const QString name) { 109 | clientName = name; 110 | } 111 | 112 | /** 113 | * @brief Returns the username of the client 114 | * @return Settings::clientName 115 | */ 116 | QString Settings::getClientName() const { 117 | return clientName; 118 | } 119 | 120 | /** 121 | * @brief Sets the color of the client 122 | * @param color The new color 123 | */ 124 | void Settings::setClientColor(const QColor color) { 125 | clientColor = color; 126 | } 127 | 128 | /** 129 | * @brief Returns the color of the client 130 | * @return Settings::clientColor 131 | */ 132 | QColor Settings::getClientColor() const { 133 | return clientColor; 134 | } 135 | 136 | /** 137 | * @brief Sets the amount of points that have to be scored to win the game 138 | * @param score The new target score 139 | */ 140 | void Settings::setTargetScore(const int score) { 141 | targetScore = score; 142 | } 143 | 144 | /** 145 | * @brief Returns the current target score 146 | * @return Settings::targetScore 147 | */ 148 | int Settings::getTargetScore() const { 149 | return targetScore; 150 | } 151 | 152 | /** 153 | * @brief Sets the network blocking value 154 | * @param val The new value 155 | */ 156 | void Settings::setNetworkCurverBlock(const unsigned val) { 157 | networkCurverBlock = val; 158 | } 159 | 160 | /** 161 | * @brief Returns the network blocking value 162 | * @return Settings::networkCurverBlock 163 | */ 164 | unsigned Settings::getNetworkCurverBlock() const { 165 | return networkCurverBlock; 166 | } 167 | 168 | /** 169 | * @brief Sets the amount of logic updates per second 170 | * @param val The new value 171 | */ 172 | void Settings::setUpdatesPerSecond(const unsigned val) { 173 | updatesPerSecond = val; 174 | } 175 | 176 | /** 177 | * @brief Returns the amount of logic updates per second 178 | */ 179 | unsigned Settings::getUpdatesPerSecond() const { 180 | return updatesPerSecond; 181 | } 182 | 183 | /** 184 | * @brief Returns whether the application is started headless 185 | * @return Whether the application is started offscreen 186 | */ 187 | bool Settings::getOffscreen() const { 188 | return QGuiApplication::platformName() == "offscreen"; 189 | } 190 | -------------------------------------------------------------------------------- /src/settings.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | /** 10 | * @brief This class represents settings that affect the game in any way 11 | */ 12 | class Settings : public QObject { 13 | Q_OBJECT 14 | QML_ELEMENT 15 | QML_SINGLETON 16 | 17 | Q_PROPERTY(int width READ getWidth WRITE setWidth NOTIFY widthChanged) 18 | Q_PROPERTY(int height READ getHeight WRITE setHeight NOTIFY heightChanged) 19 | public: 20 | QML_CPP_SINGLETON(Settings) 21 | 22 | void setDimension(QPoint dimension); 23 | QPoint getDimension() const; 24 | Q_INVOKABLE void setWidth(int width); 25 | Q_INVOKABLE int getWidth() const; 26 | Q_INVOKABLE void setHeight(int height); 27 | Q_INVOKABLE int getHeight() const; 28 | Q_INVOKABLE void setRoundTimeOut(int roundTimeOut); 29 | Q_INVOKABLE int getRoundTimeOut() const; 30 | Q_INVOKABLE void setItemSpawnIntervalMin(const int interval); 31 | Q_INVOKABLE int getItemSpawnIntervalMin() const; 32 | Q_INVOKABLE void setItemSpawnIntervalMax(const int interval); 33 | Q_INVOKABLE int getItemSpawnIntervalMax() const; 34 | Q_INVOKABLE void setClientName(const QString name); 35 | QString getClientName() const; 36 | Q_INVOKABLE void setClientColor(const QColor color); 37 | QColor getClientColor() const; 38 | Q_INVOKABLE void setTargetScore(const int score); 39 | Q_INVOKABLE int getTargetScore() const; 40 | Q_INVOKABLE void setNetworkCurverBlock(const unsigned val); 41 | Q_INVOKABLE unsigned getNetworkCurverBlock() const; 42 | Q_INVOKABLE void setUpdatesPerSecond(const unsigned val); 43 | Q_INVOKABLE unsigned getUpdatesPerSecond() const; 44 | Q_INVOKABLE bool getOffscreen() const; 45 | signals: 46 | /** 47 | * @brief Emitted, when the dimension of the game changed 48 | */ 49 | void dimensionChanged(); 50 | /** 51 | * @brief Emitted, when the width changed 52 | * @param width The new width 53 | */ 54 | void widthChanged(int width); 55 | /** 56 | * @brief Emitted, when the height changed 57 | * @param height The new height 58 | */ 59 | void heightChanged(int height); 60 | private: 61 | /** 62 | * @brief The username of the client 63 | */ 64 | QString clientName; 65 | /** 66 | * @brief The color of the client 67 | */ 68 | QColor clientColor; 69 | /** 70 | * @brief The dimension of the game 71 | */ 72 | QPoint dimension {700, 836}; 73 | /** 74 | * @brief The current round time out 75 | * 76 | * This is the amount of time that has to be waited for the next round, after the old one was finished 77 | */ 78 | int roundTimeOut = 1000; 79 | /** 80 | * @brief The minimum amount of time between two item spawns 81 | */ 82 | int itemSpawnIntervalMin = 1000; 83 | /** 84 | * @brief The maximum amount of time between two item spawns 85 | */ 86 | int itemSpawnIntervalMax = 5000; 87 | /** 88 | * @brief The score to achieve to win the game 89 | */ 90 | int targetScore = 15; 91 | /** 92 | * @brief Determines how often Curver data should be sent by the Server 93 | * 94 | * A value of n means, that n-1 times no data will be sent before data will be sent again. 95 | * A value of 1 means, that every iteration all data will be sent. 96 | */ 97 | unsigned networkCurverBlock = 2; 98 | /** 99 | * @brief The number of logic updates per second 100 | */ 101 | unsigned updatesPerSecond = 60; 102 | }; 103 | -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | #include "util.hpp" 2 | 3 | #include 4 | #include 5 | 6 | /** 7 | * @brief Returns a random number between 0 and 1 8 | * @return A random number between 0 and 1 9 | */ 10 | double Util::rand() { 11 | return QRandomGenerator::global()->generateDouble(); 12 | } 13 | 14 | /** 15 | * @brief Returns a random QPointF with values between 0 and 1 each 16 | * @return A random QPointF 17 | */ 18 | QPointF Util::randQPointF() { 19 | return QPointF(rand(), rand()); 20 | } 21 | 22 | /** 23 | * @brief Returns a random integer in the given range 24 | * @param lower Lower boundary 25 | * @param upper Upper boundary 26 | * @return A random integer in the given range 27 | */ 28 | int Util::randInt(const int lower, const int upper) { 29 | return lower + rand() * (upper - lower); 30 | } 31 | 32 | /** 33 | * @brief Returns a random Material design color 34 | * @return A random color 35 | */ 36 | QColor Util::randColor() { 37 | auto it = colors.begin(); 38 | std::advance(it, randInt(0, static_cast(colors.size()) - 1)); 39 | return it->second; 40 | } 41 | 42 | /** 43 | * @brief Returns a Material design color 44 | * @param color The color to look up 45 | * @return The Material design color 46 | */ 47 | const QColor Util::getColor(const QString color) { 48 | return colors.find(color)->second; 49 | } 50 | 51 | /** 52 | * @brief Returns the bit at the position \a pos in \a byte counting from right 53 | * @param byte The byte to extract the bit from 54 | * @param pos The position of the bit 55 | * @return The extracted bit 56 | */ 57 | bool Util::getBit(const uint8_t byte, const int pos) { 58 | return (byte >> pos) % 2; 59 | } 60 | 61 | /** 62 | * @brief Sets the bit at position \a pos in \a byte to \a value 63 | * @param byte The byte to set a bit in 64 | * @param pos The position of the bit 65 | * @param value The new value of the bit 66 | */ 67 | void Util::setBit(uint8_t &byte, const int pos, bool value) { 68 | byte |= value << pos; 69 | } 70 | 71 | /** 72 | * @brief Calculates the time difference between a time and now 73 | * @param t The time to calculate the difference from 74 | * @return The resulting difference in milliseconds 75 | */ 76 | qint64 Util::getTimeDiff(const QTime &t) { 77 | return t.msecsTo(QTime::currentTime()); 78 | } 79 | 80 | /** 81 | * @brief Interpolates a value from 0.0 to 1.0 in a pre-defined way 82 | * @param a The value to interpolate 83 | * @return The interpolated value 84 | */ 85 | float Util::easeInOutSine(const float &a) { 86 | return 0.5 * (1 - std::cos(M_PI * a)); 87 | } 88 | -------------------------------------------------------------------------------- /src/util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | /** 15 | * @brief Contains frequently used useful routines that are available for every class 16 | */ 17 | namespace Util { 18 | double rand(); 19 | QPointF randQPointF(); 20 | int randInt(const int lower, const int upper); 21 | QColor randColor(); 22 | const QColor getColor(const QString color); 23 | /** 24 | * @brief Material design colors 25 | */ 26 | const std::map colors = { 27 | {"Red", QColor(0xF4, 0x43, 0x36)}, 28 | {"Pink", QColor(0xE9, 0x1E, 0x63)}, 29 | {"Purple", QColor(0x9C, 0x27, 0xB0)}, 30 | {"Deep Purple", QColor(0x67, 0x3A, 0xB7)}, 31 | {"Indigo", QColor(0x3F, 0x51, 0xB5)}, 32 | {"Blue", QColor(0x21, 0x96, 0xF3)}, 33 | {"Light Blue", QColor(0x03, 0xA9, 0xF4)}, 34 | {"Cyan", QColor(0x00, 0xBC, 0xD4)}, 35 | {"Teal", QColor(0x00, 0x96, 0x88)}, 36 | {"Green", QColor(0x4C, 0xAF, 0x50)}, 37 | {"Light Green", QColor(0x8B, 0xC3, 0x4A)}, 38 | {"Lime", QColor(0xCD, 0xDC, 0x39)}, 39 | {"Yellow", QColor(0xFF, 0xEB, 0x3B)}, 40 | {"Amber", QColor(0xFF, 0xC1, 0x07)}, 41 | {"Orange", QColor(0xFF, 0x98, 0x00)}, 42 | {"Deep Orange", QColor(0xFF, 0x57, 0x22)}, 43 | {"Brown", QColor(0x79, 0x55, 0x48)}, 44 | {"Grey", QColor(0x9E, 0x9E, 0x9E)}, 45 | {"Blue Grey", QColor(0x60, 0x7D, 0x8B)}}; 46 | bool getBit(const uint8_t byte, const int pos); 47 | void setBit(uint8_t &byte, const int pos, bool value); 48 | qint64 getTimeDiff(const QTime &t); 49 | float easeInOutSine(const float &a); 50 | 51 | // std algorithm wrappers 52 | 53 | /** 54 | * @brief Accumulates all elements in a given container using the + operator 55 | * @param cnt The container to accumulate elements from 56 | * @param init The initial sum value 57 | * @return The accumulated sum 58 | */ 59 | template 60 | T accumulate(Cnt_T &cnt, T init) { 61 | return std::accumulate(std::begin(cnt), std::end(cnt), init); 62 | } 63 | 64 | /** 65 | * @brief Calculates the partial sum 66 | * @param in The container to calculate the partial sum of 67 | * @param out The container to save the result in 68 | * @return Iterator to the element past the last element written 69 | */ 70 | template 71 | auto partial_sum(Cnt_T &in, Cnt_T &out) { 72 | return std::partial_sum(std::begin(in), std::end(in), std::begin(out)); 73 | } 74 | 75 | // container serialization 76 | 77 | /** 78 | * @brief Serializes a container 79 | * @param out The stream to serialize into 80 | * @param cnt The container to serialize 81 | */ 82 | template 83 | void serializeCnt(QDataStream &out, Cnt_T &cnt) { 84 | out << static_cast(cnt.size()); 85 | for (auto &i : cnt) { 86 | out << i; 87 | } 88 | } 89 | 90 | /** 91 | * @brief Parses data from a stream into a container 92 | * @param in The stream to parse from 93 | * @param cnt The container to parse into 94 | */ 95 | template 96 | void parseCnt(QDataStream &in, Cnt_T &cnt) { 97 | unsigned size; 98 | in >> size; 99 | cnt.resize(size); 100 | for (auto &i : cnt) { 101 | in >> i; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/version.cpp: -------------------------------------------------------------------------------- 1 | #include "version.hpp" 2 | 3 | namespace Version { 4 | 5 | const char *version_string() { 6 | return QUICKCURVER_VERSION; 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/version.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Version { 4 | 5 | const char *version_string(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/wall.cpp: -------------------------------------------------------------------------------- 1 | #include "wall.hpp" 2 | 3 | #define WALL_SIZE 4 4 | 5 | Wall::Wall() { 6 | geometry.setLineWidth(WALL_SIZE); 7 | geometry.setDrawingMode(QSGGeometry::DrawLineStrip); 8 | geoNode.setGeometry(&geometry); 9 | material.setColor(Util::getColor("Cyan")); 10 | geoNode.setMaterial(&material); 11 | geometry.allocate(5); 12 | updateDimension(); 13 | resize(); 14 | connect(Settings::get(), &Settings::dimensionChanged, this, &Wall::updateDimension); 15 | } 16 | 17 | /** 18 | * @brief Sets the parent node in the scene graph 19 | * @param parentNode The new parent node 20 | */ 21 | void Wall::setParentNode(QSGNode *parentNode) { 22 | parentNode->appendChildNode(&geoNode); 23 | } 24 | 25 | /** 26 | * @brief Updates the dimension of the game 27 | */ 28 | void Wall::updateDimension() { 29 | this->dimension = Settings::get()->getDimension(); 30 | resize(); 31 | } 32 | 33 | /** 34 | * @brief Resizes the Wall according to the game's dimension 35 | */ 36 | void Wall::resize() { 37 | QSGGeometry::Point2D *vertices = geometry.vertexDataAsPoint2D(); 38 | vertices[0].set(0, 0); 39 | vertices[1].set(dimension.x(), 0); 40 | vertices[2].set(dimension.x(), dimension.y()); 41 | vertices[3].set(0, dimension.y()); 42 | vertices[4].set(0, 0); 43 | geoNode.markDirty(QSGNode::DirtyGeometry); 44 | } 45 | -------------------------------------------------------------------------------- /src/wall.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "settings.hpp" 7 | #include "util.hpp" 8 | 9 | /** 10 | * @brief A node that visualizes the game borders in the scene graph 11 | */ 12 | class Wall : public QObject { 13 | Q_OBJECT 14 | public: 15 | explicit Wall(); 16 | void setParentNode(QSGNode *parentNode); 17 | void updateDimension(); 18 | private: 19 | void resize(); 20 | 21 | /** 22 | * @brief The dimension of the game 23 | */ 24 | QPoint dimension {20, 20}; 25 | /** 26 | * @brief The node handling the geometry 27 | */ 28 | QSGGeometryNode geoNode; 29 | /** 30 | * @brief The actual geometry 31 | */ 32 | QSGGeometry geometry = QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); 33 | /** 34 | * @brief The material of the Wall 35 | */ 36 | QSGFlatColorMaterial material; 37 | }; 38 | --------------------------------------------------------------------------------