├── .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 | [](https://github.com/vimpostor/quickcurver/actions/workflows/ci.yml)
5 |
6 | 
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 |
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