Compare commits

...

19 commits

Author SHA1 Message Date
Robert Müller 8ef01fcbaf
Merge 4df4fa237e into 306d3c7b58 2024-09-07 13:09:06 +02:00
Dennis Felsing 306d3c7b58
Merge pull request #8910 from Robyt3/Android-Manifest-Flags-SDL
Adopt changes to `AndroidManifest.xml` from SDL sample project
2024-09-07 10:29:41 +00:00
Robert Müller 9832288983 Adopt changes to AndroidManifest.xml from SDL sample project
Specify `android:installLocation="auto"` so the app can be installed on and move to the external storage.

Specify optional features which the app may use (touchscreen, game controller, external mouse).

Specify `android:preferMinimalPostProcessing="true"` so lower latency HDMI mode is enabled when available. Specify `android:hardwareAccelerated="true"` for consistency (it is already the default setting).

Specify same `android:configChanges` and `android:alwaysRetainTaskState` values as SDL to avoid potential bugs due to inconsistency with what the `SDLActivity` expects.

See f5ed158d1f/android-project/app/src/main/AndroidManifest.xml
2024-09-07 12:09:41 +02:00
Dennis Felsing fc058fa432
Merge pull request #8906 from Robyt3/Client-Download-Gameserver-Fix
Fix map download fallback from game server
2024-09-06 22:11:02 +00:00
Dennis Felsing 3b22a3e02f
Merge pull request #8905 from furo321/hot-reload-super-crash
Fix crash with `hot_reload` while in `super`
2024-09-06 22:08:14 +00:00
Robert Müller ed1ef4e694 Fix map download fallback from game server
Do not reset the active map download's information before using the fallback map download.

Remove redundant calls of `ResetMapDownload` before disconnecting, as this already resets the map download.

Closes #8885. Regression from #8848.
2024-09-06 20:55:02 +02:00
furo 9103332e36 Fix crash with hot_reload while in super 2024-09-06 20:12:07 +02:00
Dennis Felsing 0948a53648
Merge pull request #8904 from furo321/fix-force-yes
Fix `random_unfinished_map` not working with `vote yes`
2024-09-06 17:34:21 +00:00
furo bbd34c9452 Fix random_unfinished_map not working with vote yes 2024-09-06 18:56:58 +02:00
Dennis Felsing e4282f100a
Merge pull request #8902 from KebsCS/pr-hotreload-lasttp
Add /lasttp to hot reload
2024-09-06 16:24:48 +00:00
KebsCS eb9e73f68b
Add /lasttp to hot reload 2024-09-06 16:31:46 +02:00
Dennis Felsing e0a95d14a6
Merge pull request #8898 from KebsCS/pr-country-filters
Change country and types filter behavior
2024-09-05 13:16:21 +00:00
KebsCS fa28ed77a6
Change country and types filter behavior 2024-09-05 14:24:11 +02:00
Dennis Felsing 5335813629
Merge pull request #8897 from KebsCS/pr-command-argument-validation
Fix /tpxy arg validation
2024-09-05 09:26:15 +00:00
KebsCS 796fa4275f
Fix tpxy arg validation 2024-09-05 11:04:03 +02:00
Dennis Felsing 5b0163d069
Merge pull request #8896 from dobrykafe/pr-auto-map-reload-setting
Add `ed_auto_map_reload` setting
2024-09-05 07:33:35 +00:00
dobrykafe c9e7e0f01b add ed_auto_map_reload setting 2024-09-05 00:23:03 +02:00
Robert Müller 4df4fa237e Add ingame touch controls
Add new client component `CTouchControls` for playing the game with touch inputs. The touch controls can be enabled/disabled with the config variable `cl_touch_controls`, which defaults to `1` on Android and `0` on other platforms. The touch controls consist of various on-screen touch buttons. The different buttons are only shown when they are usable depending on the context. The touch button configuration is stored in `touch_controls.json`.

Default touch button configuration
----------------------------------

Movement buttons for left, right and jump actions are arranged in a `⊥`-pattern similar to WASD controls.

For the fire and hook action, two modes are implemented:

1. Direct touch input: the mouse is moved exactly where the player touches on the screen.
2. Virtual joystick: a button is used to emulate a joystick, which moves the mouse relative to the center of the screen.

In either mode, a button is used to switch between the active actions (fire and hook). While the virtual joystick is being held down, this button uses the other action directly instead of switching it.

The direct touch input mode can be enabled/disabled separately for ingame/spectating, to prevent accidental direct touch input when using a joystick.

When spectating, direct touch input is used to allow panning the map directly like in an image/map viewer. Virtual joysticks can also be used to pan the map while spectating.

Two separate buttons are shown to switch to the previous and next weapons.

A hamburger menu button `☰` is used to toggle the visibility of lesser used touch buttons. This includes buttons for showing the scoreboard, showing the emoticon selector, showing the spectator menu, opening team and team chat, voting yes/no, and zooming. Long pressing the hamburger menu button will open the regular menu.

When the dummy is connected, a button for swapping between main and dummy is shown.

The emoticon selector and spectator menu are activated with the respective touch buttons and can be deactivated by touching outside of them or by using the back-button, as toggling them while the ingame touch button is pressed down is currently not feasible and also inconvenient at least for using the spectator menu.

Touch button configuration format
---------------------------------

The default button layout described above is loaded from `data/touch_controls.json`. This layout can be overridden by creating `touch_controls.json` in the user directory. The following button properties are adjustable:

- Position and size (attributes `x`, `y`, `w`, `h`): the X/Y position and width/height are integers on a 1,000,000² grid. The unit grid values are converted to screen grid values at runtime in relation to the aspect ratio of the screen. This means buttons may appear stretched if the resolution is changed, but it allows us to provide a reasonable default for slightly different aspect ratios. If we save the absolute position instead, we would have the problem of buttons appearing outside the screen or not being aligned on different aspect ratios.
- Shape (attribute `shape`): determines the shape of the button being rendered.
    - `"rect"`: rectangle shape.
    - `"circle"`: circle shape. The button size will automatically be adjusted so that width and height are identical.
- Visibility (attribute `visibilities`): an array of predefined visibility classes can be selected and the button is only shown if all conditions are satisfied. An empty array means that the button is always shown. The following visibility classes are defined:
    - `"ingame"`: player is ingame, i.e. not spectating.
    - `"extra-menu"`: the extra menu is activated.
    - `"zoom-allowed"`: zoom is allowed on the this server.
    - `"vote-active"`: a vote is currently active.
    - `"dummy-allowed"`: dummy is allowed on this server.
    - `"dummy-connected"`: dummy is currently connected.
    - `"rcon-authed"`: player is currently authed in rcon.
    - All visibility classes can be inverted by prefixing them with `-`, e.g. `"-ingame"` is satisfied when the player is not ingame, i.e. spectating.
- Behavior (attribute `behavior`): an object which describes the behavior of this touch button when it is activated/deactivated as well as its label. The attribute `type` is used to differentiate which type of behavior is used. The behavior is either predefined (hard-coded) or a generic console command (bind). Predefined behavior is only used where necessary, all other buttons are represented as generic binds.
    - Predefined behavior (attribute `type` set to `"predefined"`): The attribute `id` is set to a fixed string value which determines the specific predefined behavior. The following predefined behaviors exist:
        - `"extra-menu"`: The extra menu button which toggles visibility of buttons with `"extra-menu"` visibility. Also opens the normal menu on long press.
        - `"emoticon"`: Opens the emoticon selector (this does not work with binds).
        - `"spectate"`: Opens the spectator menu (this does not work with binds).
        - `"swap-action"`: Swaps the active action (fire and hook) for direct touch input and virtual joysticks.
        - `"use-action"`: Uses the active action with the current aiming position.
        - `"joystick-action"`: Virtual joystick which uses the active action.
        - `"joystick-fire"`: Virtual joystick which always uses fire.
        - `"joystick-hook"`: Virtual joystick which always uses hook.
    - Bind behavior (attribute `type` set to `"bind"`). Buttons with this behavior work like regular key binds.
        - The attribute `"label"` specifies as a string the label of the button.
        - The attribute `"label-type"` specifies as a string the type of the label of the button, i.e. how the attribute `"label"` is interpreted:
            - `"plain"`: Label is used as is.
            - `"localized"`: Label is localized. Only usable for the default buttons for which there are translations available.
            - `"icon"`: Icon font is used for the label. Icons must be encoded using `\uXXXX`, e.g. `\uf3ce` for the mobile phone icon.
        - The attribute `"command"` specifies as a string the command to execute in the console like a bind when this button is used.

Ingame menu buttons
-------------------

In addition to the separate on-screen touch controls, a second row is added to the main page of the ingame menu when `cl_touch_controls` is enabled for less frequently used functions which are otherwise not usable without a keyboard:

- Buttons to open the local and remote consoles. Opening the local console without the touch controls is useful because error messages would be shown there if the touch controls configuration could not be loaded.
- Button to close the menu, which is more convenient than using the virtual back button if it's not always shown.
- Checkbox for toggling the touch controls editor UI (see below).

Ingame touch controls editor
----------------------------

The user interface to adjust the touch controls is rendered over the main screen of the ingame menu when enabled. For now, the implementation of the touch controls editor is limited to basic configuration management functions.

- Saving the configuration to the `touch_controls.json` file in config directory.
- Discarding the current changes by reloading the `touch_controls.json` file from config directory.
- Restoring the default configuration by reloading the `touch_controls.json` file from data directory.
- Displaying whether there are unsaved changes.
- Importing and exporting the configuration from and to the clipboard. This is the only way to edit the configuration on newer Android versions, as editing files within apps' storage is not possible anymore.

Furthermore, the global touch controls settings can be adjusted in this UI:

- Direct touch input can be enabled/disabled separately while ingame and while spectating.

While the touch controls editor is active, all buttons are shown regardless of their visibility, to better support arranging the buttons.

Future tasks
------------

- Implement more usable UI for adjusting the button layout.
- Add `color` property to touch buttons to adjust the default background color?
- Support multiple independent extra menu visibilities and buttons with separate states, which would also add support for nesting buttons in menus.
- Support zooming gesture.
- Optimize touch button rendering using text and quad containers.
2024-08-25 15:41:57 +02:00
Robert Müller 5d4e6cf86a Support handling touch state as part of the client update
So the touch state can be handled by gameclient components independently from their rendering, so touch inputs are not skipped when frames are.
2024-08-25 12:26:47 +02:00
27 changed files with 2333 additions and 61 deletions

View file

@ -1845,6 +1845,7 @@ set(EXPECTED_DATA
themes/winter.png
themes/winter_day.map
themes/winter_night.map
touch_controls.json
wordlist.txt
)
@ -2399,6 +2400,8 @@ if(CLIENT)
components/statboard.h
components/tooltips.cpp
components/tooltips.h
components/touch_controls.cpp
components/touch_controls.h
components/voting.cpp
components/voting.h
gameclient.cpp

305
data/touch_controls.json Normal file
View file

@ -0,0 +1,305 @@
{
"direct-touch-ingame": true,
"direct-touch-spectate": true,
"touch-buttons": [
{
"x": 0,
"y": 833333,
"w": 200000,
"h": 166667,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Move left",
"label-type": "localized",
"command": "+left"
}
},
{
"x": 200000,
"y": 833333,
"w": 200000,
"h": 166667,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Move right",
"label-type": "localized",
"command": "+right"
}
},
{
"x": 100000,
"y": 666667,
"w": 200000,
"h": 166667,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Jump",
"label-type": "localized",
"command": "+jump"
}
},
{
"x": 116667,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Prev. weapon",
"label-type": "localized",
"command": "+prevweapon"
}
},
{
"x": 200000,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Next weapon",
"label-type": "localized",
"command": "+nextweapon"
}
},
{
"x": 16667,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
],
"behavior": {
"type": "predefined",
"id": "extra-menu"
}
},
{
"x": 300000,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"extra-menu",
"zoom-allowed"
],
"behavior": {
"type": "bind",
"label": "Zoom out",
"label-type": "localized",
"command": "zoom-"
}
},
{
"x": 383333,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"extra-menu",
"zoom-allowed"
],
"behavior": {
"type": "bind",
"label": "Default zoom",
"label-type": "localized",
"command": "zoom"
}
},
{
"x": 466666,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"extra-menu",
"zoom-allowed"
],
"behavior": {
"type": "bind",
"label": "Zoom in",
"label-type": "localized",
"command": "zoom+"
}
},
{
"x": 16667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu"
],
"behavior": {
"type": "bind",
"label": "Scoreboard",
"label-type": "localized",
"command": "+scoreboard"
}
},
{
"x": 116667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"ingame",
"extra-menu"
],
"behavior": {
"type": "predefined",
"id": "emoticon"
}
},
{
"x": 116667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"-ingame",
"extra-menu"
],
"behavior": {
"type": "predefined",
"id": "spectate"
}
},
{
"x": 216667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu"
],
"behavior": {
"type": "bind",
"label": "Chat",
"label-type": "localized",
"command": "chat all"
}
},
{
"x": 316667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu"
],
"behavior": {
"type": "bind",
"label": "Team chat",
"label-type": "localized",
"command": "chat team"
}
},
{
"x": 16667,
"y": 333333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu",
"vote-active"
],
"behavior": {
"type": "bind",
"label": "Vote yes",
"label-type": "localized",
"command": "vote yes"
}
},
{
"x": 116667,
"y": 333333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu",
"vote-active"
],
"behavior": {
"type": "bind",
"label": "Vote no",
"label-type": "localized",
"command": "vote no"
}
},
{
"x": 766667,
"y": 16667,
"w": 100000,
"h": 100000,
"shape": "rect",
"visibilities": [
"dummy-connected"
],
"behavior": {
"type": "bind",
"label": "Toggle dummy",
"label-type": "localized",
"command": "toggle cl_dummy 0 1"
}
},
{
"x": 883333,
"y": 16667,
"w": 100000,
"h": 100000,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "predefined",
"id": "swap-action"
}
},
{
"x": 755000,
"y": 580000,
"w": 225000,
"h": 400000,
"shape": "circle",
"visibilities": [
],
"behavior": {
"type": "predefined",
"id": "joystick-action"
}
}
]
}

View file

@ -1,15 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="auto">
<!-- Vulkan 1.1.0 is used if supported -->
<uses-feature
android:name="android.hardware.vulkan.version"
android:required="false"
android:version="0x00401000" />
<!-- android:glEsVersion is not specified as OpenGL ES 1.0 is supported as fallback -->
<!-- Only playable in landscape mode -->
<uses-feature
android:name="android.hardware.screen.landscape"
android:required="true" />
<!-- Touchscreen support -->
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<!-- Game controller support -->
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature
android:name="android.hardware.gamepad"
android:required="false" />
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />
<!-- External mouse input events -->
<uses-feature
android:name="android.hardware.type.pc"
android:required="false" />
<!-- Teeworlds does broadcasts over local networks -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
@ -25,17 +50,24 @@
android:isGame="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:hardwareAccelerated="true">
<activity
android:name="org.ddnet.client.NativeMain"
android:alwaysRetainTaskState="true"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:preferMinimalPostProcessing="true"
android:screenOrientation="landscape"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Let Android know that we can handle some USB devices and should receive this event -->
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="DDNet" />

View file

@ -668,10 +668,7 @@ void CClient::DisconnectWithReason(const char *pReason)
m_CurrentServerCurrentPingTime = -1;
m_CurrentServerNextPingTime = -1;
ResetMapDownload();
m_aMapdownloadFilename[0] = '\0';
m_aMapdownloadFilenameTemp[0] = '\0';
m_aMapdownloadName[0] = '\0';
ResetMapDownload(true);
// clear the current server info
mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
@ -1528,7 +1525,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy)
DummyDisconnect(0);
}
ResetMapDownload();
ResetMapDownload(true);
SHA256_DIGEST *pMapSha256 = nullptr;
const char *pMapUrl = nullptr;
@ -2196,7 +2193,7 @@ int CClient::UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo)
return Builder.Finish(pTo);
}
void CClient::ResetMapDownload()
void CClient::ResetMapDownload(bool ResetActive)
{
if(m_pMapdownloadTask)
{
@ -2215,19 +2212,24 @@ void CClient::ResetMapDownload()
Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE);
}
m_MapdownloadChunk = 0;
m_MapdownloadSha256Present = false;
m_MapdownloadSha256 = SHA256_ZEROED;
m_MapdownloadCrc = 0;
m_MapdownloadTotalsize = -1;
m_MapdownloadAmount = 0;
if(ResetActive)
{
m_MapdownloadChunk = 0;
m_MapdownloadSha256Present = false;
m_MapdownloadSha256 = SHA256_ZEROED;
m_MapdownloadCrc = 0;
m_MapdownloadTotalsize = -1;
m_MapdownloadAmount = 0;
m_aMapdownloadFilename[0] = '\0';
m_aMapdownloadFilenameTemp[0] = '\0';
m_aMapdownloadName[0] = '\0';
}
}
void CClient::FinishMapDownload()
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "download complete, loading map");
const int PrevMapdownloadTotalsize = m_MapdownloadTotalsize;
SHA256_DIGEST *pSha256 = m_MapdownloadSha256Present ? &m_MapdownloadSha256 : nullptr;
bool FileSuccess = true;
@ -2236,7 +2238,6 @@ void CClient::FinishMapDownload()
FileSuccess &= Storage()->RenameFile(m_aMapdownloadFilenameTemp, m_aMapdownloadFilename, IStorage::TYPE_SAVE);
if(!FileSuccess)
{
ResetMapDownload();
char aError[128 + IO_MAX_PATH_LENGTH];
str_format(aError, sizeof(aError), Localize("Could not save downloaded map. Try manually deleting this file: %s"), m_aMapdownloadFilename);
DisconnectWithReason(aError);
@ -2246,19 +2247,17 @@ void CClient::FinishMapDownload()
const char *pError = LoadMap(m_aMapdownloadName, m_aMapdownloadFilename, pSha256, m_MapdownloadCrc);
if(!pError)
{
ResetMapDownload();
ResetMapDownload(true);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "loading done");
SendReady(CONN_MAIN);
}
else if(m_pMapdownloadTask) // fallback
{
ResetMapDownload();
m_MapdownloadTotalsize = PrevMapdownloadTotalsize;
ResetMapDownload(false);
SendMapRequest();
}
else
{
ResetMapDownload();
DisconnectWithReason(pError);
}
}
@ -2784,7 +2783,7 @@ void CClient::Update()
else if(m_pMapdownloadTask->State() == EHttpState::ERROR || m_pMapdownloadTask->State() == EHttpState::ABORTED)
{
dbg_msg("webdl", "http failed, falling back to gameserver");
ResetMapDownload();
ResetMapDownload(false);
SendMapRequest();
}
}

View file

@ -360,7 +360,7 @@ public:
int UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo);
void ResetMapDownload();
void ResetMapDownload(bool ResetActive);
void FinishMapDownload();
void RequestDDNetInfo() override;

View file

@ -300,6 +300,14 @@ const std::vector<IInput::CTouchFingerState> &CInput::TouchFingerStates() const
return m_vTouchFingerStates;
}
void CInput::ClearTouchDeltas()
{
for(CTouchFingerState &TouchFingerState : m_vTouchFingerStates)
{
TouchFingerState.m_Delta = vec2(0.0f, 0.0f);
}
}
std::string CInput::GetClipboardText()
{
char *pClipboardText = SDL_GetClipboardText();
@ -347,10 +355,7 @@ void CInput::Clear()
mem_zero(m_aInputState, sizeof(m_aInputState));
mem_zero(m_aInputCount, sizeof(m_aInputCount));
m_vInputEvents.clear();
for(CTouchFingerState &TouchFingerState : m_vTouchFingerStates)
{
TouchFingerState.m_Delta = vec2(0.0f, 0.0f);
}
ClearTouchDeltas();
}
float CInput::GetUpdateTime() const

View file

@ -144,6 +144,7 @@ public:
bool NativeMousePressed(int Index) const override;
const std::vector<CTouchFingerState> &TouchFingerStates() const override;
void ClearTouchDeltas() override;
std::string GetClipboardText() override;
void SetClipboardText(const char *pText) override;

View file

@ -27,9 +27,6 @@
#include <engine/http.h>
#include <engine/storage.h>
static constexpr const char *COMMUNITY_COUNTRY_NONE = "none";
static constexpr const char *COMMUNITY_TYPE_NONE = "None";
class CSortWrap
{
typedef bool (CServerBrowser::*SortFunc)(int, int) const;

View file

@ -137,6 +137,12 @@ public:
* @return vector of all touch finger states
*/
virtual const std::vector<CTouchFingerState> &TouchFingerStates() const = 0;
/**
* Must be called after the touch finger states have been used during the client update to ensure that
* touch deltas are only accumulated until the next update. If the touch states are only used during
* rendering, i.e. for user interfaces, then this is called automatically by calling @link Clear @endlink.
*/
virtual void ClearTouchDeltas() = 0;
// clipboard
virtual std::string GetClipboardText() = 0;

View file

@ -309,6 +309,9 @@ public:
static constexpr const char *COMMUNITY_DDNET = "ddnet";
static constexpr const char *COMMUNITY_NONE = "none";
static constexpr const char *COMMUNITY_COUNTRY_NONE = "none";
static constexpr const char *COMMUNITY_TYPE_NONE = "None";
/**
* Special community value for country/type filters that
* affect all communities.

View file

@ -22,6 +22,11 @@ MACRO_CONFIG_INT(ClAntiPingSmooth, cl_antiping_smooth, 0, 0, 1, CFGFLAG_CLIENT |
MACRO_CONFIG_INT(ClAntiPingGunfire, cl_antiping_gunfire, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Predict gunfire and show predicted weapon physics (with cl_antiping_grenade 1 and cl_antiping_weapons 1)")
MACRO_CONFIG_INT(ClPredictionMargin, cl_prediction_margin, 10, 1, 300, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Prediction margin in ms (adds latency, can reduce lag from ping jumps)")
MACRO_CONFIG_INT(ClSubTickAiming, cl_sub_tick_aiming, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Send aiming data at sub-tick accuracy")
#if defined(CONF_PLATFORM_ANDROID)
MACRO_CONFIG_INT(ClTouchControls, cl_touch_controls, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable ingame touch controls")
#else
MACRO_CONFIG_INT(ClTouchControls, cl_touch_controls, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable ingame touch controls")
#endif
MACRO_CONFIG_INT(ClNameplates, cl_nameplates, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show name plates")
MACRO_CONFIG_INT(ClAfkEmote, cl_afk_emote, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show zzz emote next to afk players")
@ -105,6 +110,7 @@ MACRO_CONFIG_INT(EdZoomTarget, ed_zoom_target, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG
MACRO_CONFIG_INT(EdShowkeys, ed_showkeys, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show pressed keys")
MACRO_CONFIG_INT(EdAlignQuads, ed_align_quads, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable/disable quad alignment. When enabled, red lines appear to show how quad/points are aligned and snapped to other quads/points when moving them")
MACRO_CONFIG_INT(EdShowQuadsRect, ed_show_quads_rect, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show the bounds of the selected quad. In case of multiple quads, it shows the bounds of the englobing rect. Can be helpful when aligning a group of quads")
MACRO_CONFIG_INT(EdAutoMapReload, ed_auto_map_reload, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Run 'hot_reload' on the local server while rcon authed on map save")
MACRO_CONFIG_INT(ClShowWelcome, cl_show_welcome, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show welcome message indicating the first launch of the client")
MACRO_CONFIG_INT(ClMotdTime, cl_motd_time, 10, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "How long to show the server message of the day")

View file

@ -136,7 +136,7 @@ int CConsole::ParseStart(CResult *pResult, const char *pString, int Length)
return 0;
}
int CConsole::ParseArgs(CResult *pResult, const char *pFormat, FCommandCallback pfnCallback)
int CConsole::ParseArgs(CResult *pResult, const char *pFormat, bool IsColor)
{
char Command = *pFormat;
char *pStr;
@ -235,7 +235,7 @@ int CConsole::ParseArgs(CResult *pResult, const char *pFormat, FCommandCallback
if(Command == 'i')
{
// don't validate colors here
if(pfnCallback != &SColorConfigVariable::CommandCallback)
if(!IsColor)
{
int Value;
if(!str_toint(pResult->GetString(pResult->NumArguments() - 1), &Value) ||
@ -514,7 +514,7 @@ void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientId, bo
if(Stroke || IsStrokeCommand)
{
if(int Error = ParseArgs(&Result, pCommand->m_pParams, pCommand->m_pfnCallback))
if(int Error = ParseArgs(&Result, pCommand->m_pParams, pCommand->m_pfnCallback == &SColorConfigVariable::CommandCallback))
{
char aBuf[CMDLINE_LENGTH + 64];
if(Error == PARSEARGS_INVALID_INTEGER)

View file

@ -153,7 +153,7 @@ class CConsole : public IConsole
PARSEARGS_INVALID_FLOAT,
};
int ParseArgs(CResult *pResult, const char *pFormat, FCommandCallback pfnCallback = 0);
int ParseArgs(CResult *pResult, const char *pFormat, bool IsColor = false);
/*
this function will set pFormat to the next parameter (i,s,r,v,?) it contains and

View file

@ -212,6 +212,14 @@ public:
* @param Event The input event.
*/
virtual bool OnInput(const IInput::CEvent &Event) { return false; }
/**
* Called with all current touch finger states.
*
* @param vTouchFingerStates The touch finger states to be handled.
*
* @return `true` if the component used the touch events, `false` otherwise
*/
virtual bool OnTouchState(const std::vector<IInput::CTouchFingerState> &vTouchFingerStates) { return false; }
};
#endif

View file

@ -12,10 +12,10 @@
class CControls : public CComponent
{
public:
float GetMinMouseDistance() const;
float GetMaxMouseDistance() const;
public:
vec2 m_aMousePos[NUM_DUMMIES];
vec2 m_aMousePosOnAction[NUM_DUMMIES];
vec2 m_aTargetPos[NUM_DUMMIES];

View file

@ -474,8 +474,12 @@ protected:
// found in menus_ingame.cpp
STextContainerIndex m_MotdTextContainerIndex;
void RenderGame(CUIRect MainView);
void RenderTouchControlsEditor(CUIRect MainView);
void PopupConfirmDisconnect();
void PopupConfirmDisconnectDummy();
void PopupConfirmDiscardTouchControlsChanged();
void PopupConfirmResetTouchControls();
void PopupConfirmImportTouchControlsClipboard();
void RenderPlayers(CUIRect MainView);
void RenderServerInfo(CUIRect MainView);
void RenderServerInfoMotd(CUIRect Motd);
@ -637,7 +641,6 @@ protected:
static CUi::EPopupMenuFunctionResult PopupMapPicker(void *pContext, CUIRect View, bool Active);
void SetNeedSendInfo();
void SetActive(bool Active);
void UpdateColors();
IGraphics::CTextureHandle m_TextureBlob;
@ -657,6 +660,8 @@ public:
bool IsInit() { return m_IsInit; }
bool IsActive() const { return m_MenuActive; }
void SetActive(bool Active);
void KillServer();
virtual void OnInit() override;

View file

@ -869,11 +869,14 @@ void CMenus::RenderServerbrowserDDNetFilter(CUIRect View,
{
if(Click == 1)
{
// Left click: when all are active, only activate one
// Left click: when all are active, only activate one and none
for(int j = 0; j < MaxItems; ++j)
{
if(j != ItemIndex)
Filter.Add(GetItemName(j));
if(const char *pItemName = GetItemName(j);
j != ItemIndex &&
!((&Filter == &ServerBrowser()->CountriesFilter() && str_comp(pItemName, IServerBrowser::COMMUNITY_COUNTRY_NONE) == 0) ||
(&Filter == &ServerBrowser()->TypesFilter() && str_comp(pItemName, IServerBrowser::COMMUNITY_TYPE_NONE) == 0)))
Filter.Add(pItemName);
}
}
else if(Click == 2)
@ -890,7 +893,10 @@ void CMenus::RenderServerbrowserDDNetFilter(CUIRect View,
bool AllFilteredExceptUs = true;
for(int j = 0; j < MaxItems; ++j)
{
if(j != ItemIndex && !Filter.Filtered(GetItemName(j)))
if(const char *pItemName = GetItemName(j);
j != ItemIndex && !Filter.Filtered(pItemName) &&
!((&Filter == &ServerBrowser()->CountriesFilter() && str_comp(pItemName, IServerBrowser::COMMUNITY_COUNTRY_NONE) == 0) ||
(&Filter == &ServerBrowser()->TypesFilter() && str_comp(pItemName, IServerBrowser::COMMUNITY_TYPE_NONE) == 0)))
{
AllFilteredExceptUs = false;
break;
@ -898,7 +904,7 @@ void CMenus::RenderServerbrowserDDNetFilter(CUIRect View,
}
// When last one is removed, re-enable all currently selectable items.
// Don't use Clear, to avoid enabling also currently unselectable items.
if(AllFilteredExceptUs)
if(AllFilteredExceptUs && Active)
{
for(int j = 0; j < MaxItems; ++j)
{

View file

@ -18,6 +18,7 @@
#include <game/client/animstate.h>
#include <game/client/components/countryflags.h>
#include <game/client/components/touch_controls.h>
#include <game/client/gameclient.h>
#include <game/client/render.h>
#include <game/client/ui.h>
@ -40,21 +41,19 @@ using namespace std::chrono_literals;
void CMenus::RenderGame(CUIRect MainView)
{
CUIRect Button, ButtonBar, ButtonBar2;
CUIRect Button, ButtonBars, ButtonBar, ButtonBar2;
bool ShowDDRaceButtons = MainView.w > 855.0f;
MainView.HSplitTop(45.0f, &ButtonBar, &MainView);
ButtonBar.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f);
// button bar
ButtonBar.HSplitTop(10.0f, 0, &ButtonBar);
ButtonBar.HSplitTop(25.0f, &ButtonBar, 0);
ButtonBar.VMargin(10.0f, &ButtonBar);
ButtonBar.HSplitTop(30.0f, 0, &ButtonBar2);
ButtonBar2.HSplitTop(25.0f, &ButtonBar2, 0);
MainView.HSplitTop(45.0f + (g_Config.m_ClTouchControls ? 35.0f : 0.0f), &ButtonBars, &MainView);
ButtonBars.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f);
ButtonBars.Margin(10.0f, &ButtonBars);
ButtonBars.HSplitTop(25.0f, &ButtonBar, &ButtonBars);
if(g_Config.m_ClTouchControls)
{
ButtonBars.HSplitTop(10.0f, nullptr, &ButtonBars);
ButtonBars.HSplitTop(25.0f, &ButtonBar2, &ButtonBars);
}
ButtonBar.VSplitRight(120.0f, &ButtonBar, &Button);
static CButtonContainer s_DisconnectButton;
if(DoButton_Menu(&s_DisconnectButton, Localize("Disconnect"), 0, &Button))
{
@ -214,6 +213,147 @@ void CMenus::RenderGame(CUIRect MainView)
}
}
}
if(g_Config.m_ClTouchControls)
{
ButtonBar2.VSplitLeft(200.0f, &Button, &ButtonBar2);
static char s_TouchControlsEditCheckbox;
if(DoButton_CheckBox(&s_TouchControlsEditCheckbox, Localize("Edit touch controls"), GameClient()->m_TouchControls.IsEditingActive(), &Button))
{
GameClient()->m_TouchControls.SetEditingActive(!GameClient()->m_TouchControls.IsEditingActive());
}
ButtonBar2.VSplitRight(80.0f, &ButtonBar2, &Button);
static CButtonContainer s_CloseButton;
if(DoButton_Menu(&s_CloseButton, Localize("Close"), 0, &Button))
{
SetActive(false);
}
ButtonBar2.VSplitRight(5.0f, &ButtonBar2, nullptr);
ButtonBar2.VSplitRight(160.0f, &ButtonBar2, &Button);
static CButtonContainer s_RemoveConsoleButton;
if(DoButton_Menu(&s_RemoveConsoleButton, Localize("Remote console"), 0, &Button))
{
Console()->ExecuteLine("toggle_remote_console");
}
ButtonBar2.VSplitRight(5.0f, &ButtonBar2, nullptr);
ButtonBar2.VSplitRight(120.0f, &ButtonBar2, &Button);
static CButtonContainer s_LocalConsoleButton;
if(DoButton_Menu(&s_LocalConsoleButton, Localize("Console"), 0, &Button))
{
Console()->ExecuteLine("toggle_local_console");
}
if(GameClient()->m_TouchControls.IsEditingActive())
{
CUIRect TouchControlsEditor;
MainView.VMargin((MainView.w - 505.0f) / 2.0f, &TouchControlsEditor);
TouchControlsEditor.HMargin((TouchControlsEditor.h - 195.0f) / 2.0f, &TouchControlsEditor);
RenderTouchControlsEditor(TouchControlsEditor);
}
}
}
void CMenus::RenderTouchControlsEditor(CUIRect MainView)
{
CUIRect Button, Row;
MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_ALL, 10.0f);
MainView.Margin(10.0f, &MainView);
MainView.HSplitTop(25.0f, &Button, &MainView);
MainView.HSplitTop(5.0f, nullptr, &MainView);
Ui()->DoLabel(&Button, Localize("Edit touch controls"), 20.0f, TEXTALIGN_MC);
MainView.HSplitTop(25.0f, &Row, &MainView);
MainView.HSplitTop(5.0f, nullptr, &MainView);
Row.VSplitLeft(240.0f, &Button, &Row);
static CButtonContainer s_SaveConfigurationButton;
if(DoButton_Menu(&s_SaveConfigurationButton, Localize("Save changes"), GameClient()->m_TouchControls.HasEditingChanges() ? 0 : 1, &Button))
{
if(GameClient()->m_TouchControls.SaveConfigurationToFile())
{
GameClient()->m_TouchControls.SetEditingChanges(false);
}
else
{
SWarning Warning(Localize("Error saving touch controls"), Localize("Could not save touch controls to file. See local console for details."));
Warning.m_AutoHide = false;
Client()->AddWarning(Warning);
}
}
Row.VSplitLeft(5.0f, nullptr, &Row);
Row.VSplitLeft(240.0f, &Button, &Row);
if(GameClient()->m_TouchControls.HasEditingChanges())
{
TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
Ui()->DoLabel(&Button, Localize("Unsaved changes"), 14.0f, TEXTALIGN_MC);
TextRender()->TextColor(TextRender()->DefaultTextColor());
}
MainView.HSplitTop(25.0f, &Row, &MainView);
MainView.HSplitTop(5.0f, nullptr, &MainView);
Row.VSplitLeft(240.0f, &Button, &Row);
static CButtonContainer s_DiscardChangesButton;
if(DoButton_Menu(&s_DiscardChangesButton, Localize("Discard changes"), GameClient()->m_TouchControls.HasEditingChanges() ? 0 : 1, &Button))
{
PopupConfirm(Localize("Discard changes"),
Localize("Are you sure that you want to discard the current changes to the touch controls?"),
Localize("Yes"), Localize("No"),
&CMenus::PopupConfirmDiscardTouchControlsChanged);
}
Row.VSplitLeft(5.0f, nullptr, &Row);
Row.VSplitLeft(240.0f, &Button, &Row);
static CButtonContainer s_ResetButton;
if(DoButton_Menu(&s_ResetButton, Localize("Reset to default"), 0, &Button))
{
PopupConfirm(Localize("Reset to default"),
Localize("Are you sure that you want to reset the touch controls to default?"),
Localize("Yes"), Localize("No"),
&CMenus::PopupConfirmResetTouchControls);
}
MainView.HSplitTop(25.0f, &Row, &MainView);
MainView.HSplitTop(5.0f, nullptr, &MainView);
Row.VSplitLeft(240.0f, &Button, &Row);
static CButtonContainer s_ClipboardImportButton;
if(DoButton_Menu(&s_ClipboardImportButton, Localize("Import from clipboard"), 0, &Button))
{
PopupConfirm(Localize("Import from clipboard"),
Localize("Are you sure that you want to import the touch controls from the clipboard? The will overwrite your current touch controls."),
Localize("Yes"), Localize("No"),
&CMenus::PopupConfirmImportTouchControlsClipboard);
}
Row.VSplitLeft(5.0f, nullptr, &Row);
Row.VSplitLeft(240.0f, &Button, &Row);
static CButtonContainer s_ClipboardExportButton;
if(DoButton_Menu(&s_ClipboardExportButton, Localize("Export to clipboard"), 0, &Button))
{
GameClient()->m_TouchControls.SaveConfigurationToClipboard();
}
MainView.HSplitTop(25.0f, &Button, &MainView);
MainView.HSplitTop(5.0f, nullptr, &MainView);
static char s_DirectTouchIngameButton;
if(DoButton_CheckBox(&s_DirectTouchIngameButton, Localize("Direct touch input while ingame"), GameClient()->m_TouchControls.IsDirectTouchIngame(), &Button))
{
GameClient()->m_TouchControls.SetDirectTouchIngame(!GameClient()->m_TouchControls.IsDirectTouchIngame());
}
MainView.HSplitTop(25.0f, &Button, &MainView);
MainView.HSplitTop(5.0f, nullptr, &MainView);
static char s_DirectTouchSpectateButton;
if(DoButton_CheckBox(&s_DirectTouchSpectateButton, Localize("Direct touch input while spectate"), GameClient()->m_TouchControls.IsDirectTouchSpectate(), &Button))
{
GameClient()->m_TouchControls.SetDirectTouchSpectate(!GameClient()->m_TouchControls.IsDirectTouchSpectate());
}
}
void CMenus::PopupConfirmDisconnect()
@ -227,6 +367,57 @@ void CMenus::PopupConfirmDisconnectDummy()
SetActive(false);
}
void CMenus::PopupConfirmDiscardTouchControlsChanged()
{
if(GameClient()->m_TouchControls.LoadConfigurationFromFile(IStorage::TYPE_ALL))
{
GameClient()->m_TouchControls.SetEditingChanges(false);
}
else
{
SWarning Warning(Localize("Error loading touch controls"), Localize("Could not load touch controls from file. See local console for details."));
Warning.m_AutoHide = false;
Client()->AddWarning(Warning);
}
}
void CMenus::PopupConfirmResetTouchControls()
{
bool Success = false;
for(int StorageType = 1; StorageType < Storage()->NumPaths(); ++StorageType)
{
if(GameClient()->m_TouchControls.LoadConfigurationFromFile(StorageType))
{
Success = true;
break;
}
}
if(Success)
{
GameClient()->m_TouchControls.SetEditingChanges(true);
}
else
{
SWarning Warning(Localize("Error loading touch controls"), Localize("Could not load default touch controls from file. See local console for details."));
Warning.m_AutoHide = false;
Client()->AddWarning(Warning);
}
}
void CMenus::PopupConfirmImportTouchControlsClipboard()
{
if(GameClient()->m_TouchControls.LoadConfigurationFromClipboard())
{
GameClient()->m_TouchControls.SetEditingChanges(true);
}
else
{
SWarning Warning(Localize("Error loading touch controls"), Localize("Could not load touch controls from clipboard. See local console for details."));
Warning.m_AutoHide = false;
Client()->AddWarning(Warning);
}
}
void CMenus::RenderPlayers(CUIRect MainView)
{
CUIRect Button, Button2, ButtonBar, PlayerList, Player;
@ -1292,6 +1483,9 @@ void CMenus::RenderGhost(CUIRect MainView)
void CMenus::RenderIngameHint()
{
if(g_Config.m_ClTouchControls)
return;
float Width = 300 * Graphics()->ScreenAspect();
Graphics()->MapScreen(0, 0, Width, 300);
TextRender()->TextColor(1, 1, 1, 1);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,402 @@
#ifndef GAME_CLIENT_COMPONENTS_TOUCH_CONTROLS_H
#define GAME_CLIENT_COMPONENTS_TOUCH_CONTROLS_H
#include <base/vmath.h>
#include <engine/input.h>
#include <game/client/component.h>
#include <game/client/ui_rect.h>
#include <chrono>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
class CJsonWriter;
typedef struct _json_value json_value;
class CTouchControls : public CComponent
{
enum class EButtonShape
{
RECT,
CIRCLE,
NUM_SHAPES
};
static constexpr const char *SHAPE_NAMES[(int)EButtonShape::NUM_SHAPES] = {"rect", "circle"};
enum class EButtonVisibility
{
INGAME,
EXTRA_MENU,
ZOOM_ALLOWED,
VOTE_ACTIVE,
DUMMY_ALLOWED,
DUMMY_CONNECTED,
RCON_AUTHED,
NUM_VISIBILITIES
};
class CButtonVisibility
{
public:
EButtonVisibility m_Type;
bool m_Parity;
CButtonVisibility(EButtonVisibility Type, bool Parity) :
m_Type(Type), m_Parity(Parity) {}
};
class CButtonVisibilityData
{
public:
const char *m_pId;
std::function<bool()> m_Function;
};
CButtonVisibilityData m_aVisibilityFunctions[(int)EButtonVisibility::NUM_VISIBILITIES];
enum
{
ACTION_FIRE,
ACTION_HOOK,
NUM_ACTIONS
};
class CButtonLabel
{
public:
enum class EType
{
/**
* Label is used as is.
*/
PLAIN,
/**
* Label is localized. Only usable for default button labels for which there must be
* corresponding `Localizable`-calls in code and string in the translation files.
*/
LOCALIZED,
/**
* Icon font is used for the label.
*/
ICON,
/**
* Number of label types.
*/
NUM_TYPES
};
EType m_Type;
const char *m_pLabel;
};
static constexpr const char *LABEL_TYPE_NAMES[(int)CButtonLabel::EType::NUM_TYPES] = {"plain", "localized", "icon"};
class CUnitRect
{
public:
int m_X;
int m_Y;
int m_W;
int m_H;
};
class CTouchButtonBehavior;
class CTouchButton
{
public:
CTouchButton(CTouchControls *pTouchControls);
CTouchButton(CTouchButton &&Other) noexcept;
CTouchButton(const CTouchButton &Other) = delete;
CTouchButton &operator=(const CTouchButton &Other) = delete;
CTouchButton &operator=(CTouchButton &&Other) noexcept;
CTouchControls *m_pTouchControls;
CUnitRect m_UnitRect;
CUIRect m_ScreenRect;
EButtonShape m_Shape;
int m_BackgroundCorners; // only used with EButtonShape::RECT
std::vector<CButtonVisibility> m_vVisibilities;
std::unique_ptr<CTouchButtonBehavior> m_pBehavior;
void UpdatePointers();
void UpdateScreenFromUnitRect();
void UpdateUnitFromScreenRect();
void UpdateBackgroundCorners();
vec2 ClampTouchPosition(vec2 TouchPosition) const;
bool IsInside(vec2 TouchPosition) const;
bool IsVisible() const;
void Render() const;
void WriteToConfiguration(CJsonWriter *pWriter);
};
class CTouchButtonBehavior
{
public:
CTouchButton *m_pTouchButton;
CTouchControls *m_pTouchControls;
bool m_Active; // variables below must only be used when active
IInput::CTouchFinger m_Finger;
vec2 m_ActivePosition;
vec2 m_AccumulatedDelta;
std::chrono::nanoseconds m_ActivationStartTime;
virtual ~CTouchButtonBehavior() = default;
virtual void Init(CTouchButton *pTouchButton);
void Reset();
void SetActive(const IInput::CTouchFingerState &FingerState);
void SetInactive();
bool IsActive() const;
bool IsActive(const IInput::CTouchFinger &Finger) const;
virtual CButtonLabel GetLabel() const = 0;
virtual void OnActivate() {}
virtual void OnDeactivate() {}
virtual void OnUpdate() {}
virtual void WriteToConfiguration(CJsonWriter *pWriter) = 0;
};
/**
* Abstract class for predefined behaviors.
*
* This implements the serialization for predefined behaviors.
*
* Subclasses must implemented the actual behavior and provide the label.
*/
class CPredefinedTouchButtonBehavior : public CTouchButtonBehavior
{
const char *m_pId;
public:
static constexpr const char *BEHAVIOR_TYPE = "predefined";
CPredefinedTouchButtonBehavior(const char *pId) :
m_pId(pId) {}
void WriteToConfiguration(CJsonWriter *pWriter) override;
};
class CExtraMenuTouchButtonBehavior : public CPredefinedTouchButtonBehavior
{
public:
static constexpr const char *BEHAVIOR_ID = "extra-menu";
CExtraMenuTouchButtonBehavior() :
CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {}
CButtonLabel GetLabel() const override;
void OnDeactivate() override;
};
class CEmoticonTouchButtonBehavior : public CPredefinedTouchButtonBehavior
{
public:
static constexpr const char *BEHAVIOR_ID = "emoticon";
CEmoticonTouchButtonBehavior() :
CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {}
CButtonLabel GetLabel() const override;
void OnDeactivate() override;
};
class CSpectateTouchButtonBehavior : public CPredefinedTouchButtonBehavior
{
public:
static constexpr const char *BEHAVIOR_ID = "spectate";
CSpectateTouchButtonBehavior() :
CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {}
CButtonLabel GetLabel() const override;
void OnDeactivate() override;
};
class CSwapActionTouchButtonBehavior : public CPredefinedTouchButtonBehavior
{
int m_ActiveAction = NUM_ACTIONS;
public:
static constexpr const char *BEHAVIOR_ID = "swap-action";
CSwapActionTouchButtonBehavior() :
CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {}
CButtonLabel GetLabel() const override;
void OnActivate() override;
void OnDeactivate() override;
};
class CUseActionTouchButtonBehavior : public CPredefinedTouchButtonBehavior
{
int m_ActiveAction = NUM_ACTIONS;
public:
static constexpr const char *BEHAVIOR_ID = "use-action";
CUseActionTouchButtonBehavior() :
CPredefinedTouchButtonBehavior(BEHAVIOR_ID) {}
CButtonLabel GetLabel() const override;
void OnActivate() override;
void OnDeactivate() override;
};
class CJoystickTouchButtonBehavior : public CPredefinedTouchButtonBehavior
{
int m_ActiveAction = NUM_ACTIONS;
public:
CJoystickTouchButtonBehavior(const char *pId) :
CPredefinedTouchButtonBehavior(pId) {}
CButtonLabel GetLabel() const override;
void OnActivate() override;
void OnDeactivate() override;
void OnUpdate() override;
int ActiveAction() const { return m_ActiveAction; }
virtual int SelectedAction() const = 0;
};
class CJoystickActionTouchButtonBehavior : public CJoystickTouchButtonBehavior
{
public:
static constexpr const char *BEHAVIOR_ID = "joystick-action";
CJoystickActionTouchButtonBehavior() :
CJoystickTouchButtonBehavior(BEHAVIOR_ID) {}
~CJoystickActionTouchButtonBehavior();
void Init(CTouchButton *pTouchButton) override;
int SelectedAction() const override;
};
class CJoystickFireTouchButtonBehavior : public CJoystickTouchButtonBehavior
{
public:
static constexpr const char *BEHAVIOR_ID = "joystick-fire";
CJoystickFireTouchButtonBehavior() :
CJoystickTouchButtonBehavior(BEHAVIOR_ID) {}
int SelectedAction() const override;
};
class CJoystickHookTouchButtonBehavior : public CJoystickTouchButtonBehavior
{
public:
static constexpr const char *BEHAVIOR_ID = "joystick-hook";
CJoystickHookTouchButtonBehavior() :
CJoystickTouchButtonBehavior(BEHAVIOR_ID) {}
int SelectedAction() const override;
};
/**
* Generic behavior implementation that executes a console command like a bind.
*/
class CBindTouchButtonBehavior : public CTouchButtonBehavior
{
std::string m_Label;
CButtonLabel::EType m_LabelType;
std::string m_Command;
bool m_Repeating = false;
std::chrono::nanoseconds m_LastUpdateTime;
std::chrono::nanoseconds m_AccumulatedRepeatingTime;
public:
static constexpr const char *BEHAVIOR_TYPE = "bind";
CBindTouchButtonBehavior(const char *pLabel, CButtonLabel::EType LabelType, const char *pCommand) :
m_Label(pLabel),
m_LabelType(LabelType),
m_Command(pCommand) {}
CButtonLabel GetLabel() const override;
void OnActivate() override;
void OnDeactivate() override;
void OnUpdate() override;
void WriteToConfiguration(CJsonWriter *pWriter) override;
};
bool m_DirectTouchIngame = true;
bool m_DirectTouchSpectate = true;
std::vector<CTouchButton> m_vTouchButtons;
bool m_ExtraMenuActive = false;
class CActionState
{
public:
bool m_Active = false;
IInput::CTouchFinger m_Finger;
};
int m_ActionSelected = ACTION_FIRE;
int m_ActionLastActivated = ACTION_FIRE;
CActionState m_aActionStates[NUM_ACTIONS];
CJoystickActionTouchButtonBehavior *m_pPrimaryJoystickTouchButtonBehavior;
bool m_EditingActive = false;
bool m_EditingChanges = false;
void InitVisibilityFunctions();
void UpdateButtons(const std::vector<IInput::CTouchFingerState> &vTouchFingerStates);
void RenderButtons();
vec2 CalculateScreenSize() const;
std::unique_ptr<CPredefinedTouchButtonBehavior> ParsePredefinedBehavior(const json_value *pBehaviorObject);
std::unique_ptr<CBindTouchButtonBehavior> ParseBindBehavior(const json_value *pBehaviorObject);
std::unique_ptr<CTouchButtonBehavior> ParseBehavior(const json_value *pBehaviorObject);
std::optional<CTouchButton> ParseButton(const json_value *pButtonObject);
bool ParseConfiguration(const void *pFileData, unsigned FileLength);
void WriteConfiguration(CJsonWriter *pWriter);
public:
int Sizeof() const override { return sizeof(*this); }
void OnInit() override;
void OnReset() override;
void OnWindowResize() override;
bool OnTouchState(const std::vector<IInput::CTouchFingerState> &vTouchFingerStates) override;
void OnRender() override;
bool LoadConfigurationFromFile(int StorageType);
bool LoadConfigurationFromClipboard();
bool SaveConfigurationToFile();
void SaveConfigurationToClipboard();
bool IsDirectTouchIngame() const { return m_DirectTouchIngame; }
void SetDirectTouchIngame(bool DirectTouchIngame)
{
m_DirectTouchIngame = DirectTouchIngame;
m_EditingChanges = true;
}
bool IsDirectTouchSpectate() const { return m_DirectTouchSpectate; }
void SetDirectTouchSpectate(bool DirectTouchSpectate)
{
m_DirectTouchSpectate = DirectTouchSpectate;
m_EditingChanges = true;
}
bool IsEditingActive() const { return m_EditingActive; }
void SetEditingActive(bool EditingActive) { m_EditingActive = EditingActive; }
bool HasEditingChanges() const { return m_EditingChanges; }
void SetEditingChanges(bool EditingChanges) { m_EditingChanges = EditingChanges; }
};
#endif

View file

@ -145,6 +145,7 @@ void CGameClient::OnConsoleInit()
&m_Chat,
&m_Broadcast,
&m_DebugHud,
&m_TouchControls,
&m_Scoreboard,
&m_Statboard,
&m_Motd,
@ -164,6 +165,7 @@ void CGameClient::OnConsoleInit()
&m_Emoticon,
&m_Menus,
&m_Controls,
&m_TouchControls,
&m_Binds});
// add basic console commands
@ -443,6 +445,23 @@ void CGameClient::OnUpdate()
}
}
// handle touch events
const std::vector<IInput::CTouchFingerState> &vTouchFingerStates = Input()->TouchFingerStates();
bool TouchHandled = false;
for(auto &pComponent : m_vpInput)
{
if(TouchHandled)
{
// Also update inactive components so they can handle touch fingers being released.
pComponent->OnTouchState({});
}
else if(pComponent->OnTouchState(vTouchFingerStates))
{
Input()->ClearTouchDeltas();
TouchHandled = true;
}
}
// handle key presses
Input()->ConsumeEvents([&](const IInput::CEvent &Event) {
for(auto &pComponent : m_vpInput)

View file

@ -58,6 +58,7 @@
#include "components/spectator.h"
#include "components/statboard.h"
#include "components/tooltips.h"
#include "components/touch_controls.h"
#include "components/voting.h"
class CGameInfo
@ -146,6 +147,7 @@ public:
CSounds m_Sounds;
CEmoticon m_Emoticon;
CDamageInd m_DamageInd;
CTouchControls m_TouchControls;
CVoting m_Voting;
CSpectator m_Spectator;

View file

@ -7691,7 +7691,7 @@ void CEditor::RenderMenubar(CUIRect MenuBar)
if(DoButton_Ex(&s_SettingsButton, "Settings", 0, &SettingsButton, 0, nullptr, IGraphics::CORNER_T, EditorFontSizes::MENU, TEXTALIGN_ML))
{
static SPopupMenuId s_PopupMenuEntitiesId;
Ui()->DoPopupMenu(&s_PopupMenuEntitiesId, SettingsButton.x, SettingsButton.y + SettingsButton.h - 1.0f, 200.0f, 92.0f, this, PopupMenuSettings, PopupProperties);
Ui()->DoPopupMenu(&s_PopupMenuEntitiesId, SettingsButton.x, SettingsButton.y + SettingsButton.h - 1.0f, 200.0f, 106.0f, this, PopupMenuSettings, PopupProperties);
}
CUIRect ChangedIndicator, Info, Help, Close;
@ -8592,7 +8592,7 @@ void CEditor::HandleWriterFinishJobs()
Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor/save", aBuf);
// send rcon.. if we can
if(Client()->RconAuthed())
if(Client()->RconAuthed() && g_Config.m_EdAutoMapReload)
{
CServerInfo CurrentServerInfo;
Client()->GetServerInfo(&CurrentServerInfo);

View file

@ -371,6 +371,30 @@ CUi::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect
}
}
View.HSplitTop(2.0f, nullptr, &View);
View.HSplitTop(12.0f, &Slot, &View);
{
Slot.VMargin(5.0f, &Slot);
CUIRect Label, Selector;
Slot.VSplitMid(&Label, &Selector);
CUIRect No, Yes;
Selector.VSplitMid(&No, &Yes);
pEditor->Ui()->DoLabel(&Label, "Auto map reload", 10.0f, TEXTALIGN_ML);
static int s_ButtonNo = 0;
static int s_ButtonYes = 0;
if(pEditor->DoButton_Ex(&s_ButtonNo, "No", !g_Config.m_EdAutoMapReload, &No, 0, "Do not run 'hot_reload' on the local server while rcon authed on map save", IGraphics::CORNER_L))
{
g_Config.m_EdAutoMapReload = false;
}
if(pEditor->DoButton_Ex(&s_ButtonYes, "Yes", g_Config.m_EdAutoMapReload, &Yes, 0, "Run 'hot_reload' on the local server while rcon authed on map save", IGraphics::CORNER_R))
{
g_Config.m_EdAutoMapReload = true;
}
}
return CUi::POPUP_KEEP_OPEN;
}

View file

@ -123,6 +123,13 @@ bool CCharacter::Spawn(CPlayer *pPlayer, vec2 Pos)
delete GameServer()->m_apSavedTees[m_pPlayer->GetCid()];
GameServer()->m_apSavedTees[m_pPlayer->GetCid()] = nullptr;
}
if(GameServer()->m_apSavedTeleTees[m_pPlayer->GetCid()])
{
m_pPlayer->m_LastTeleTee = *GameServer()->m_apSavedTeleTees[m_pPlayer->GetCid()];
delete GameServer()->m_apSavedTeleTees[m_pPlayer->GetCid()];
GameServer()->m_apSavedTeleTees[m_pPlayer->GetCid()] = nullptr;
}
}
return true;

View file

@ -108,6 +108,9 @@ void CGameContext::Construct(int Resetting)
for(auto &pSavedTee : m_apSavedTees)
pSavedTee = nullptr;
for(auto &pSavedTeleTee : m_apSavedTeleTees)
pSavedTeleTee = nullptr;
for(auto &pSavedTeam : m_apSavedTeams)
pSavedTeam = nullptr;
@ -131,6 +134,9 @@ void CGameContext::Destruct(int Resetting)
for(auto &pSavedTee : m_apSavedTees)
delete pSavedTee;
for(auto &pSavedTeleTee : m_apSavedTeleTees)
delete pSavedTeleTee;
for(auto &pSavedTeam : m_apSavedTeams)
delete pSavedTeam;
@ -773,7 +779,6 @@ void CGameContext::StartVote(const char *pDesc, const char *pCommand, const char
{
// reset votes
m_VoteEnforce = VOTE_ENFORCE_UNKNOWN;
m_VoteEnforcer = -1;
for(auto &pPlayer : m_apPlayers)
{
if(pPlayer)
@ -1204,7 +1209,7 @@ void CGameContext::OnTick()
}
else if(m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN)
{
Console()->ExecuteLine(m_aVoteCommand, m_VoteEnforcer);
Console()->ExecuteLine(m_aVoteCommand, m_VoteCreator);
SendChat(-1, TEAM_ALL, "Vote passed enforced by authorized player", -1, FLAG_SIX);
EndVote();
}
@ -1712,6 +1717,9 @@ void CGameContext::OnClientDrop(int ClientId, const char *pReason)
delete m_apSavedTees[ClientId];
m_apSavedTees[ClientId] = nullptr;
delete m_apSavedTeleTees[ClientId];
m_apSavedTeleTees[ClientId] = nullptr;
m_aTeamMapping[ClientId] = -1;
m_VoteUpdate = true;
@ -3208,12 +3216,19 @@ void CGameContext::ConHotReload(IConsole::IResult *pResult, void *pUserData)
if(!pSelf->GetPlayerChar(i))
continue;
CCharacter *pChar = pSelf->GetPlayerChar(i);
// Save the tee individually
pSelf->m_apSavedTees[i] = new CSaveTee();
pSelf->m_apSavedTees[i]->Save(pSelf->GetPlayerChar(i), false);
pSelf->m_apSavedTees[i]->Save(pChar, false);
if(pSelf->m_apPlayers[i])
pSelf->m_apSavedTeleTees[i] = new CSaveTee(pSelf->m_apPlayers[i]->m_LastTeleTee);
// Save the team state
pSelf->m_aTeamMapping[i] = pSelf->GetDDRaceTeam(i);
if(pSelf->m_aTeamMapping[i] == TEAM_SUPER)
pSelf->m_aTeamMapping[i] = pChar->m_TeamBeforeSuper;
if(pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]])
continue;
@ -3778,7 +3793,7 @@ void CGameContext::RegisterChatCommands()
Console()->Register("rescuemode", "?r['auto'|'manual']", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConRescueMode, this, "Sets one of the two rescue modes (auto or manual). Prints current mode if no arguments provided");
Console()->Register("tp", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleTo, this, "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name");
Console()->Register("teleport", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleTo, this, "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name");
Console()->Register("tpxy", "f[x] f[y]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleXY, this, "Teleport yourself to the specified coordinates. A tilde (~) can be used to denote your current position, e.g. '/tpxy ~1 ~' to teleport one tile to the right");
Console()->Register("tpxy", "s[x] s[y]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleXY, this, "Teleport yourself to the specified coordinates. A tilde (~) can be used to denote your current position, e.g. '/tpxy ~1 ~' to teleport one tile to the right");
Console()->Register("lasttp", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConLastTele, this, "Teleport yourself to the last location you teleported to");
Console()->Register("tc", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleCursor, this, "Teleport yourself to player or to where you are spectating/or looking if no player name is given");
Console()->Register("telecursor", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleCursor, this, "Teleport yourself to player or to where you are spectating/or looking if no player name is given");
@ -4820,7 +4835,6 @@ void CGameContext::ForceVote(int EnforcerId, bool Success)
return;
m_VoteEnforce = Success ? CGameContext::VOTE_ENFORCE_YES_ADMIN : CGameContext::VOTE_ENFORCE_NO_ADMIN;
m_VoteEnforcer = EnforcerId;
char aBuf[256];
const char *pOption = Success ? "yes" : "no";

View file

@ -183,6 +183,7 @@ public:
bool m_aPlayerHasInput[MAX_CLIENTS];
CSaveTeam *m_apSavedTeams[MAX_CLIENTS];
CSaveTee *m_apSavedTees[MAX_CLIENTS];
CSaveTee *m_apSavedTeleTees[MAX_CLIENTS];
int m_aTeamMapping[MAX_CLIENTS];
// returns last input if available otherwise nulled PlayerInput object
@ -575,7 +576,6 @@ public:
VOTE_TYPE_SPECTATE,
};
int m_VoteVictim;
int m_VoteEnforcer;
inline bool IsOptionVote() const { return m_VoteType == VOTE_TYPE_OPTION; }
inline bool IsKickVote() const { return m_VoteType == VOTE_TYPE_KICK; }