88 changes: 73 additions & 15 deletions data/languages/russian.txt
Expand Up @@ -13,6 +13,8 @@
# gerdoe 2020-09-17 22:49:22
# Vy0x2 2021-06-08 15:48:25
# Anime.pdf 2021-06-13 13:08:50
# banan 2022-06-12 11:36:50
# ban 2023-01-5 10:22:50
##### /authors #####

##### translated strings #####
Expand Down Expand Up @@ -1362,9 +1364,6 @@ UI controller sens.
Controller jitter tolerance
== Устойчивость к движению контроллера

No controller found. Plug in a controller and restart the game.
== Контроллер не найден. Подключите контроллер и перезапустите игру.

Device
== Устройство

Expand Down Expand Up @@ -1606,37 +1605,96 @@ Set all to Rifle
== Цвет всех как у лазера

Cancel
==
== Отмена

File '%s' already exists, do you want to overwrite it?
==
== файл '%s' уже существует, вы уверены что хотите его перезаписать?

Are you sure that you want to remove the player '%s' from your friends list?
==
== Вы уверены что хотите удалить игрока '%s' из списка ваших друзей?

Are you sure that you want to remove the clan '%s' from your friends list?
==
== Вы уверены что хотите удалить клан '%s' из списка ваших друзей?

Go back one tick
==
== Отмотать назад на один тик

Go forward one tick
==
== Отмотать вперед на один тик

Go back one marker
==
== Отмотать назад на один маркер

Go forward one marker
==
== Отмотать вперед на один маркер

Are you sure that you want to delete the demo '%s'?
==
== Вы уверены что хотите удалить демо '%s'?

Unable to delete the demo '%s'
==
== Невозможно удалить демо '%s'

Reset controls
==
== Сброс настроек

Are you sure that you want to reset the controls to their defaults?
==
== Вы уверены что хотите сбросить настройки на их стандартные значения?

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
== Ошибка во время инициализации. Попробуйте изменить gfx_backend на OpenGL или Vulkan в settings_ddnet.cfg в папке config и повторите попытку.

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
== Нехватка памяти VRAM. Попробуйте удалить пользовательские ресурсы (скины, энтити и т.д.), особенно те, которые имеют высокое разрешение.

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
== Произошла ошибка во время выполнения команды записи. Попробуйте обновить драйверы видеокарты.

[Graphics error]
A render command failed. Try to update your GPU drivers.
== Команда рендеринга не выполнена. Попробуйте обновить драйверы видеокарты.

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
== Отправка команд рендеринга не удалась. Попробуйте обновить драйверы видеокарты.

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
== Не удалось выполнить подкачку кадровых буферов. Попробуйте обновить драйверы видеокарты.

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
== Неизвестная ошибка. Попробуйте изменить gfx_backend на OpenGL или Vulkan в settings_ddnet.cfg в папке config и повторите попытку.

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
== Не удалось инициализировать заданный графический модуль, возврат к модулю по умолчанию.

Open the directory that contains the demo files
== Открытие папки, содержащего файлы демок

Save power by lowering refresh rate (higher input latency)
== Экономия энергии за счет снижения частоты кадров (более высокая задержка ввода)

Open the settings file
== Открытие файла настроек

Open the directory that contains the configuration and user files
== Открытие папки, содержащей файлы конфигурации и пользовательские файлы

Open the directory to add custom themes
== Открытие папки для добавления пользовательских тем

Open the directory to add custom skins
== Открытие папки для добавления пользовательских скинов

No controller found. Plug in a controller.
== Контроллер не найден. Подключите контроллер.

Unregister protocol and file extensions
== Незарегистрированный протокол и расширения файлов

Open the directory to add custom assets
== Открытие папки для добавления пользовательских ресурсов
64 changes: 61 additions & 3 deletions data/languages/serbian.txt
Expand Up @@ -1316,9 +1316,6 @@ UI controller sens.
Controller jitter tolerance
== Tolerancija kontrolera

No controller found. Plug in a controller and restart the game.
== Nema pronađenog kontrolera, uključite ga i ponovo pokrenite igru.

Device
== Uređaj

Expand Down Expand Up @@ -1535,6 +1532,40 @@ Team %d
Loading sound files
== Učitavam zvučne fajlove

[Graphics error]

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
==

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
==

[Graphics error]
A render command failed. Try to update your GPU drivers.
==

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
==

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
==

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
==

Initializing components
==

Expand Down Expand Up @@ -1592,6 +1623,9 @@ Go forward one marker
Toggle keyboard shortcuts
==

Open the directory that contains the demo files
==

Are you sure that you want to delete the demo '%s'?
==

Expand All @@ -1601,6 +1635,24 @@ Unable to delete the demo '%s'
Menu opened. Press Esc key again to close menu.
==

Save power by lowering refresh rate (higher input latency)
==

Open the settings file
==

Open the directory that contains the configuration and user files
==

Open the directory to add custom themes
==

Open the directory to add custom skins
==

No controller found. Plug in a controller.
==

Reset controls
==

Expand Down Expand Up @@ -1636,3 +1688,9 @@ Freeze Laser Inner Color

Set all to Rifle
==

Unregister protocol and file extensions
==

Open the directory to add custom assets
==
64 changes: 61 additions & 3 deletions data/languages/serbian_cyrillic.txt
Expand Up @@ -1315,9 +1315,6 @@ UI controller sens.
Controller jitter tolerance
== Толеранција контролера

No controller found. Plug in a controller and restart the game.
== Нема пронађеног контролера, укључите га и поново покрените игру.

Device
== Уређај

Expand Down Expand Up @@ -1534,6 +1531,40 @@ Team %d
Loading sound files
== Учитавам звучне фајлове

[Graphics error]

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
==

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
==

[Graphics error]
A render command failed. Try to update your GPU drivers.
==

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
==

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
==

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
==

Initializing components
==

Expand Down Expand Up @@ -1588,6 +1619,9 @@ Go forward one marker
Toggle keyboard shortcuts
==

Open the directory that contains the demo files
==

Are you sure that you want to delete the demo '%s'?
==

Expand All @@ -1597,6 +1631,24 @@ Unable to delete the demo '%s'
Menu opened. Press Esc key again to close menu.
==

Save power by lowering refresh rate (higher input latency)
==

Open the settings file
==

Open the directory that contains the configuration and user files
==

Open the directory to add custom themes
==

Open the directory to add custom skins
==

No controller found. Plug in a controller.
==

Reset controls
==

Expand Down Expand Up @@ -1632,3 +1684,9 @@ Freeze Laser Inner Color

Set all to Rifle
==

Unregister protocol and file extensions
==

Open the directory to add custom assets
==
90 changes: 75 additions & 15 deletions data/languages/simplified_chinese.txt
Expand Up @@ -29,6 +29,8 @@
# 2022-08-08 cheeser0613
# 2022-09-17 cheeser0613
# 2022-10-24 cheeser0613
# 2022-12-11 cheeser0613
# 2023-01-11 cheeser0613
##### /authors #####

##### translated strings #####
Expand Down Expand Up @@ -1408,9 +1410,6 @@ UI controller sens.
Controller jitter tolerance
== 摇杆死区

No controller found. Plug in a controller and restart the game.
== 未检测到任何控制器。请尝试重新连接控制器并重启游戏。

Device
== 输入设备

Expand Down Expand Up @@ -1628,37 +1627,98 @@ Set all to Rifle
== 统一为激光枪颜色

Cancel
==
== 取消

File '%s' already exists, do you want to overwrite it?
==
== 文件名 "%s" 已经存在,是否要覆盖该文件?

Are you sure that you want to remove the player '%s' from your friends list?
==
== 你确定要将好友 "%s" 从你的好友列表中删除?

Are you sure that you want to remove the clan '%s' from your friends list?
==
== 你确定要将战队 "%s" 从你的好友列表中删除?

Go back one tick
==
== 上一个 tick

Go forward one tick
==
== 下一个 tick

Go back one marker
==
== 返回上一个标记

Go forward one marker
==
== 跳至下一个标记

Are you sure that you want to delete the demo '%s'?
==
== 你确定要删除回放文件 "%s" ?

Unable to delete the demo '%s'
==
== 无法删除回放文件 "%s"

Reset controls
==
== 恢复默认设置

Are you sure that you want to reset the controls to their defaults?
==
== 你确定要将当前的控制恢复至默认设置?

[Graphics error]

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
== 初始化失败。请尝试打开配置目录中的设置文件(settings_ddnet.cfg)并将“gfx_backend OpenGL”修改为“gfx_backend Vulkan”(若没有前者则可直接输入后者)再重试。

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
== 显存不足。请尝试移除自定义资源(如皮肤,实体层等等),尤其高分辨率的自定义资源。

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
== 指令记录期间检测到错误。请尝试更新显卡驱动程序。

[Graphics error]
A render command failed. Try to update your GPU drivers.
== 渲染指令错误。请尝试更新显卡驱动程序

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
== 渲染指令提交失败。请尝试更新显卡驱动程序。

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
== 帧缓存交替失败。请尝试更新显卡驱动程序。

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
== 未知错误。请尝试打开配置目录中的设置文件(settings_ddnet.cfg)并将“gfx_backend OpenGL”修改为“gfx_backend Vulkan”(若没有前者则可直接输入后者)再重试。

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
== 无法初始化当前配置的图形后端,正在将配置回退至默认图形后端。

Open the directory that contains the demo files
== 打开存有回放文件的文件夹路径

Save power by lowering refresh rate (higher input latency)
== 省电模式(限制帧率上限以降低功耗但也将提高输入延迟)

Open the settings file
== 打开设置文件

Open the directory that contains the configuration and user files
== 打开存有配置文件与用户文件的文件夹路径

Open the directory to add custom themes
== 打开用以添加自定义主题的文件夹路径

Open the directory to add custom skins
== 打开用以添加自定义皮肤的文件夹路径

No controller found. Plug in a controller.
== 未检测到任何控制器。请尝试重新连接控制器。

Unregister protocol and file extensions
== 未注册的协议与扩充文件

Open the directory to add custom assets
== 打开用以添加自定义资源的文件夹路径
60 changes: 59 additions & 1 deletion data/languages/slovak.txt
Expand Up @@ -595,6 +595,40 @@ Name plates size
Type:
== Typ:

[Graphics error]

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
==

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
==

[Graphics error]
A render command failed. Try to update your GPU drivers.
==

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
==

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
==

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
==

Successfully saved the replay!
==

Expand Down Expand Up @@ -910,6 +944,9 @@ Fetch Info
Demos directory
==

Open the directory that contains the demo files
==

Are you sure that you want to delete the demo '%s'?
==

Expand Down Expand Up @@ -973,15 +1010,27 @@ Skip the main menu
Refresh Rate
==

Save power by lowering refresh rate (higher input latency)
==

Settings file
==

Open the settings file
==

Config directory
==

Open the directory that contains the configuration and user files
==

Themes directory
==

Open the directory to add custom themes
==

Automatically take statboard screenshot
==

Expand Down Expand Up @@ -1024,6 +1073,9 @@ Skin Database
Skins directory
==

Open the directory to add custom skins
==

Hook collisions
==

Expand Down Expand Up @@ -1104,7 +1156,7 @@ UI controller sens.
Controller jitter tolerance
==

No controller found. Plug in a controller and restart the game.
No controller found. Plug in a controller.
==

Device
Expand Down Expand Up @@ -1485,6 +1537,9 @@ Run on join
Chat command (e.g. showall 1)
==

Unregister protocol and file extensions
==

DDNet %s is available:
==

Expand Down Expand Up @@ -1518,6 +1573,9 @@ Loading assets
Assets directory
==

Open the directory to add custom assets
==

Discord
==

Expand Down
95 changes: 77 additions & 18 deletions data/languages/spanish.txt
Expand Up @@ -18,6 +18,7 @@
# Deëivid! 2022-06-22 19:26:00
# Deëivid! 2022-07-05 22:30:00
# Deëivid! 2022-10-08 19:20:00
# Deëivid! 2023-01-05 21:05:00
##### /authors #####

##### translated strings #####
Expand Down Expand Up @@ -560,7 +561,7 @@ Laser
== Láser

Netversion:
== Versión Net
== Versión Net:

Map:
== Mapa:
Expand Down Expand Up @@ -908,7 +909,7 @@ DDNet Client needs to be restarted to complete update!
== ¡El Cliente DDNet debe reiniciarse para completar la actualización!

Use DDRace Scoreboard
== Utiliza el marcador DDRace
== Utilizar el marcador DDRace

Show score
== Mostrar puntaje
Expand Down Expand Up @@ -1064,7 +1065,7 @@ Warning
== Advertencia

Debug mode enabled. Press Ctrl+Shift+D to disable debug mode.
== Modo debug activado. Pulsa Ctrl+Shift+D para desactivar.
== Modo debug activado. Pulsa Ctrl+Shift+D para desactivarlo.

Use k key to kill (restart), q to pause and watch other players. See settings for other key binds.
== Usa la tecla K para suicidarte (reiniciar), Q para pausar y ver a otros jugadores. Mira la configuración para otras teclas.
Expand Down Expand Up @@ -1390,9 +1391,6 @@ UI controller sens.
Controller jitter tolerance
== Tolerancia a la fluctuación del mando

No controller found. Plug in a controller and restart the game.
== No se encontró un mando. Conecta uno y reinicia el juego.

Device
== Dispositivo

Expand Down Expand Up @@ -1613,37 +1611,98 @@ Loading sound files
== Cargando archivos de sonido

Cancel
==
== Cancelar

File '%s' already exists, do you want to overwrite it?
==
== El archivo '%s' ya existe, ¿quieres sobrescribirlo?

Are you sure that you want to remove the player '%s' from your friends list?
==
== ¿Estás seguro de que quieres quitar al jugador '%s' de tu lista de amigos?

Are you sure that you want to remove the clan '%s' from your friends list?
==
== ¿Estás seguro de que quieres quitar al clan '%s' de tu lista de amigos?

Go back one tick
==
== Retroceder un tick

Go forward one tick
==
== Avanzar un tick

Go back one marker
==
== Retroceder un marcador

Go forward one marker
==
== Avanzar un marcador

Are you sure that you want to delete the demo '%s'?
==
== ¿Estás seguro de que quieres eliminar la demo '%s'?

Unable to delete the demo '%s'
==
== No se pudo eliminar la demo '%s'

Reset controls
==
== Restablecer controles

Are you sure that you want to reset the controls to their defaults?
==
== ¿Estás seguro de que quieres restablecer a los controles predeterminados?

[Graphics error]

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
== Error durante la iniciación. Intenta cambiar gfx_backend a OpenGL o Vulkan desde settings_ddnet.cfg en la carpeta de configuraciones e inténtalo otra vez.

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
== La VRAM se ha llenado. Intenta quitar recursos personalizados (skins, entidades, etc.), especialmente los de resolución alta.

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
== Ocurrió un error durante la grabación de comandos. Intenta actualizar los controladores de tu GPU.

[Graphics error]
A render command failed. Try to update your GPU drivers.
== Un comando de renderización falló. Intenta actualizar los controladores de tu GPU.

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
== Error al enviar los comandos de renderización. Intenta actualizar los controladores de tu GPU.

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
== Error al intercambiar búferes de fotogramas. Intenta actualizar los controladores de tu GPU.

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
== Error desconocido. Intenta cambiar gfx_backend a OpenGL o Vulkan desde settings_ddnet.cfg en el directorio de configuraciones e inténtalo otra vez.

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
== No se pudo inicializar el backend de gráficos dado, volviendo al backend predeterminado.

Open the directory that contains the demo files
== Abrir la carpeta que contiene los archivos de demo

Save power by lowering refresh rate (higher input latency)
== Ahorra energía al reducir la frecuencia de actualización (mayor latencia de entrada)

Open the settings file
== Abrir el archivo de configuraciones

Open the directory that contains the configuration and user files
== Abrir la carpeta que contiene las configuraciones y archivos del usuario

Open the directory to add custom themes
== Abrir la carpeta para agregar temas personalizados

Open the directory to add custom skins
== Abrir la carpeta para agregar skins personalizadas

No controller found. Plug in a controller.
== No se encontró un mando. Conecta uno.

Unregister protocol and file extensions
== Anular registro de protocolo y extensiones de archivo

Open the directory to add custom assets
== Abrir la carpeta para agregar recursos personalizados
60 changes: 59 additions & 1 deletion data/languages/swedish.txt
Expand Up @@ -1267,6 +1267,40 @@ Discord
https://ddnet.org/discord
== https://ddnet.org/discord

[Graphics error]

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
==

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
==

[Graphics error]
A render command failed. Try to update your GPU drivers.
==

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
==

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
==

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
==

The format of texture %s is not RGBA which will cause visual bugs.
==

Expand Down Expand Up @@ -1387,6 +1421,9 @@ Toggle keyboard shortcuts
Loading demo files
==

Open the directory that contains the demo files
==

Are you sure that you want to delete the demo '%s'?
==

Expand All @@ -1399,6 +1436,18 @@ Loading ghost files
Menu opened. Press Esc key again to close menu.
==

Save power by lowering refresh rate (higher input latency)
==

Open the settings file
==

Open the directory that contains the configuration and user files
==

Open the directory to add custom themes
==

Loading skin files
==

Expand All @@ -1411,6 +1460,9 @@ Download community skins
Choose default eyes when joining a server
==

Open the directory to add custom skins
==

Enable controller
==

Expand Down Expand Up @@ -1440,7 +1492,7 @@ UI controller sens.
Controller jitter tolerance
==

No controller found. Plug in a controller and restart the game.
No controller found. Plug in a controller.
==

Device
Expand Down Expand Up @@ -1611,12 +1663,18 @@ Quads are used for background decoration
Tries to predict other entities to give a feel of low latency
==

Unregister protocol and file extensions
==

Extras
==

Loading assets
==

Open the directory to add custom assets
==

Tutorial
==

Expand Down
100 changes: 80 additions & 20 deletions data/languages/traditional_chinese.txt
Expand Up @@ -18,6 +18,8 @@
# 2022-08-08 cheeser0613
# 2022-09-17 cheeser0613
# 2022-10-24 cheeser0613
# 2022-12-11 cheeser0613
# 2023-01-11 cheeser0613
##### /authors #####

##### translated strings #####
Expand Down Expand Up @@ -498,7 +500,7 @@ The server is running a non-standard tuning on a pure game type.
== 此伺服器執行著經過調整的非標準遊戲模式。

There's an unsaved map in the editor, you might want to save it before you quit the game.
== 編輯器中的地圖尚未儲存,建議您在退出遊戲之前返回編輯器進行儲存
== 編輯器中的地圖尚未儲存,建議你在退出遊戲之前返回編輯器進行儲存

Time limit
== 規定時限
Expand Down Expand Up @@ -906,7 +908,7 @@ Show DDNet map finishes in server browser
== 在伺服器瀏覽器中顯示已完成的 DDNet 地圖

transmits your player name to info.ddnet.org
== 將會發送您的玩家名稱到 info.ddnet.org
== 將會發送你的玩家名稱到 info.ddnet.org

Reload
== 重新整理
Expand Down Expand Up @@ -1126,7 +1128,7 @@ Existing Player
== 玩家已存在

Your nickname '%s' is already used (%d points). Do you still want to use it?
== 你使用的暱稱"%s"在DDNet中有%d分的記錄,這可能代表這個暱稱已經被其他人使用過。確認要使用這個名字嗎?
== 你使用的暱稱 "%s" 在DDNet中有%d分的記錄,這可能代表這個暱稱已經被其他人使用過。確認要使用這個名字嗎?

Checking for existing player with your name
== 正在查詢該暱稱的DDNet記錄
Expand Down Expand Up @@ -1258,7 +1260,7 @@ https://ddnet.org/discord
== https://ddnet.org/discord

Are you sure that you want to disconnect and switch to a different server?
== 您確定要中斷此伺服器并嘗試加入其他伺服器嗎
== 你確定要中斷此伺服器并嘗試加入其他伺服器嗎

Refreshing...
== 正在重整...
Expand Down Expand Up @@ -1397,9 +1399,6 @@ UI controller sens.
Controller jitter tolerance
== 搖桿錯位容錯

No controller found. Plug in a controller and restart the game.
== 未檢測到任何控制器。請重試重新連接控制器並重啓游戲。

Device
== 輸入設備

Expand Down Expand Up @@ -1476,7 +1475,7 @@ Sending initial client info
== 正在發送初始客戶端資訊

Uploading map data to GPU
== 正在將地圖數據傳輸至顯卡
== 正在將地圖數據傳輸至顯示卡

Getting game info
== 正在獲取游戲資訊
Expand Down Expand Up @@ -1617,37 +1616,98 @@ Set all to Rifle
== 統一為鐳射槍顔色

Cancel
==
== 取消

File '%s' already exists, do you want to overwrite it?
==
== 檔案名 "%s" 已經存在,是否要取代該檔案?

Are you sure that you want to remove the player '%s' from your friends list?
==
== 你確定要將好友 "%s" 從你的好友列表中移除?

Are you sure that you want to remove the clan '%s' from your friends list?
==
== 你確定要將戰隊 "%s" 從你的好友列表中移除?

Go back one tick
==
== 上一個 tick

Go forward one tick
==
== 下一個 tick

Go back one marker
==
== 返回上一個標記

Go forward one marker
==
== 跳至下一個標記

Are you sure that you want to delete the demo '%s'?
==
== 你確定要刪除回放檔案 "%s" ?

Unable to delete the demo '%s'
==
== 無法刪除回放檔案 "%s"

Reset controls
==
== 恢復預設設定

Are you sure that you want to reset the controls to their defaults?
==
== 你確定要將當前的控制恢復至預設設定

[Graphics error]

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
== 初始化失敗。請嘗試打開配置目錄中的設定檔案(settings_ddnet.cfg)并將“gfx_backend OpenGL”修改為“gfx_backend Vulkan”(若沒有前者則可直接輸入後者)再重試。

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
== 顯存不足。請嘗試移除自定義材質(如外觀,實體層等等),尤其高分辨率的自定義材質。

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
== 指令記錄期間檢測到錯誤。請嘗試更新顯示卡驅動程序。

[Graphics error]
A render command failed. Try to update your GPU drivers.
== 渲染指令錯誤。請嘗試更新顯示卡驅動程序。

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
== 渲染指令提交失敗。請嘗試更新顯示卡驅動程序。

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
== 幀緩存交替失敗。請嘗試更新顯示卡驅動程序。

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
== 未知錯誤。請嘗試打開配置目錄中的設定檔案(settings_ddnet.cfg)并將“gfx_backend OpenGL”修改為“gfx_backend Vulkan”(若沒有前者則可直接輸入後者)再重試。

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
== 無法初始化當前配置的圖形後端,正在將配置回退至預設圖形後端。

Open the directory that contains the demo files
== 打開存有回放檔案的資料夾路徑

Save power by lowering refresh rate (higher input latency)
== 節能模式(限制幀率上限以降低功耗但也將提高輸入延遲)

Open the settings file
== 打開設定檔案

Open the directory that contains the configuration and user files
== 打開存有配置檔案與用戶檔案的資料夾路徑

Open the directory to add custom themes
== 打開用以添加自定義主題的資料夾路徑

Open the directory to add custom skins
== 打開用以添加自定義外觀的資料夾路徑

No controller found. Plug in a controller.
== 未檢測到任何控制器。請嘗試重新連接控制器。

Unregister protocol and file extensions
== 未注冊的協議與擴充檔案

Open the directory to add custom assets
== 打開用以添加自定義材質的資料夾路徑
60 changes: 59 additions & 1 deletion data/languages/turkish.txt
Expand Up @@ -1068,6 +1068,40 @@ Replay
https://wiki.ddnet.org/
== https://wiki.ddnet.org/wiki/Main_Page/tr

[Graphics error]

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
==

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
==

[Graphics error]
A render command failed. Try to update your GPU drivers.
==

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
==

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
==

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
==

Saving ddnet-settings.cfg failed
==

Expand Down Expand Up @@ -1266,6 +1300,9 @@ Loading demo files
Demos directory
==

Open the directory that contains the demo files
==

Are you sure that you want to delete the demo '%s'?
==

Expand All @@ -1284,15 +1321,27 @@ Smooth Dynamic Camera
Skip the main menu
==

Save power by lowering refresh rate (higher input latency)
==

Settings file
==

Open the settings file
==

Config directory
==

Open the directory that contains the configuration and user files
==

Themes directory
==

Open the directory to add custom themes
==

Loading skin files
==

Expand All @@ -1314,6 +1363,9 @@ Skin Database
Skins directory
==

Open the directory to add custom skins
==

Chat command
==

Expand Down Expand Up @@ -1346,7 +1398,7 @@ UI controller sens.
Controller jitter tolerance
==

No controller found. Plug in a controller and restart the game.
No controller found. Plug in a controller.
==

Device
Expand Down Expand Up @@ -1577,6 +1629,9 @@ Run on join
Chat command (e.g. showall 1)
==

Unregister protocol and file extensions
==

Emoticons
==

Expand All @@ -1592,6 +1647,9 @@ Loading assets
Assets directory
==

Open the directory to add custom assets
==

Discord
==

Expand Down
60 changes: 59 additions & 1 deletion data/languages/ukrainian.txt
Expand Up @@ -1250,6 +1250,40 @@ Editor
Play
== Грати

[Graphics error]

[Graphics error]
Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.
==

[Graphics error]
An error during command recording occurred. Try to update your GPU drivers.
==

[Graphics error]
A render command failed. Try to update your GPU drivers.
==

[Graphics error]
Submitting the render commands failed. Try to update your GPU drivers.
==

[Graphics error]
Failed to swap framebuffers. Try to update your GPU drivers.
==

[Graphics error]
Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.
==

[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
==

The format of texture %s is not RGBA which will cause visual bugs.
==

Expand Down Expand Up @@ -1370,6 +1404,9 @@ Toggle keyboard shortcuts
Loading demo files
==

Open the directory that contains the demo files
==

Are you sure that you want to delete the demo '%s'?
==

Expand All @@ -1382,12 +1419,24 @@ Loading ghost files
Menu opened. Press Esc key again to close menu.
==

Save power by lowering refresh rate (higher input latency)
==

Settings file
==

Open the settings file
==

Config directory
==

Open the directory that contains the configuration and user files
==

Open the directory to add custom themes
==

Loading skin files
==

Expand All @@ -1400,6 +1449,9 @@ Download community skins
Choose default eyes when joining a server
==

Open the directory to add custom skins
==

Enable controller
==

Expand Down Expand Up @@ -1429,7 +1481,7 @@ UI controller sens.
Controller jitter tolerance
==

No controller found. Plug in a controller and restart the game.
No controller found. Plug in a controller.
==

Device
Expand Down Expand Up @@ -1609,12 +1661,18 @@ Run on join
Chat command (e.g. showall 1)
==

Unregister protocol and file extensions
==

Extras
==

Loading assets
==

Open the directory to add custom assets
==

Tutorial
==

Expand Down
2 changes: 1 addition & 1 deletion datasrc/network.py
Expand Up @@ -282,7 +282,7 @@
NetIntAny("m_FromX"),
NetIntAny("m_FromY"),
NetTick("m_StartTick"),
NetIntRange("m_Owner", 0, 'MAX_CLIENTS-1'),
NetIntRange("m_Owner", -1, 'MAX_CLIENTS-1'),
NetIntAny("m_Type"),
]),

Expand Down
34 changes: 34 additions & 0 deletions other/bundle/client/Info.plist.in
Expand Up @@ -18,6 +18,40 @@
<string>${PROJECT_VERSION}</string>
<key>CFBundleIdentifier</key>
<string>org.DDNetClient.app</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>DDNet server link</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ddnet</string>
</array>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>DDNet demo</string>
<key>CFBundleTypeRole</key>
<string>DDNet demo viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>dyn.ah62d4rv4ge80k3prr6</string>
<string>public.data</string>
</array>
<key>CFBundleTypeName</key>
<string>DDNet map</string>
<key>CFBundleTypeRole</key>
<string>DDNet map editor</string>
<key>LSItemContentTypes</key>
<array>
<string>dyn.ah62d4rv4ge8042pu</string>
<string>public.data</string>
</array>
</dict>
</array>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSPrefersDisplaySafeAreaCompatibilityMode</key>
Expand Down
14 changes: 2 additions & 12 deletions scripts/gen_keys.py
Expand Up @@ -64,22 +64,14 @@
print(f"\tKEY_JOYSTICK_BUTTON_10 = {int(highestid)},", file=f); keynames[highestid] = "joystick10"; highestid += 1
print(f"\tKEY_JOYSTICK_BUTTON_11 = {int(highestid)},", file=f); keynames[highestid] = "joystick11"; highestid += 1
print("", file=f)
print(f"\tKEY_JOY_HAT0_LEFTUP = {int(highestid)},", file=f); keynames[highestid] = "joy_hat0_leftup"; highestid += 1
print(f"\tKEY_JOY_HAT0_UP = {int(highestid)},", file=f); keynames[highestid] = "joy_hat0_up"; highestid += 1
print(f"\tKEY_JOY_HAT0_RIGHTUP = {int(highestid)},", file=f); keynames[highestid] = "joy_hat0_rightup"; highestid += 1
print(f"\tKEY_JOY_HAT0_LEFT = {int(highestid)},", file=f); keynames[highestid] = "joy_hat0_left"; highestid += 1
print(f"\tKEY_JOY_HAT0_RIGHT = {int(highestid)},", file=f); keynames[highestid] = "joy_hat0_right"; highestid += 1
print(f"\tKEY_JOY_HAT0_LEFTDOWN = {int(highestid)},", file=f); keynames[highestid] = "joy_hat0_leftdown"; highestid += 1
print(f"\tKEY_JOY_HAT0_DOWN = {int(highestid)},", file=f); keynames[highestid] = "joy_hat0_down"; highestid += 1
print(f"\tKEY_JOY_HAT0_RIGHTDOWN = {int(highestid)},", file=f); keynames[highestid] = "joy_hat0_rightdown"; highestid += 1
print(f"\tKEY_JOY_HAT1_LEFTUP = {int(highestid)},", file=f); keynames[highestid] = "joy_hat1_leftup"; highestid += 1
print(f"\tKEY_JOY_HAT1_UP = {int(highestid)},", file=f); keynames[highestid] = "joy_hat1_up"; highestid += 1
print(f"\tKEY_JOY_HAT1_RIGHTUP = {int(highestid)},", file=f); keynames[highestid] = "joy_hat1_rightup"; highestid += 1
print(f"\tKEY_JOY_HAT1_LEFT = {int(highestid)},", file=f); keynames[highestid] = "joy_hat1_left"; highestid += 1
print(f"\tKEY_JOY_HAT1_RIGHT = {int(highestid)},", file=f); keynames[highestid] = "joy_hat1_right"; highestid += 1
print(f"\tKEY_JOY_HAT1_LEFTDOWN = {int(highestid)},", file=f); keynames[highestid] = "joy_hat1_leftdown"; highestid += 1
print(f"\tKEY_JOY_HAT1_DOWN = {int(highestid)},", file=f); keynames[highestid] = "joy_hat1_down"; highestid += 1
print(f"\tKEY_JOY_HAT1_RIGHTDOWN = {int(highestid)},", file=f); keynames[highestid] = "joy_hat1_rightdown"; highestid += 1
print("", file=f)
print(f"\tKEY_JOY_AXIS_0_LEFT = {int(highestid)},", file=f); keynames[highestid] = "joy_axis0_left"; highestid += 1
print(f"\tKEY_JOY_AXIS_0_RIGHT = {int(highestid)},", file=f); keynames[highestid] = "joy_axis0_right"; highestid += 1
Expand Down Expand Up @@ -112,8 +104,8 @@
print("\tNUM_JOYSTICK_AXES_BUTTONS = KEY_JOY_AXIS_11_RIGHT - KEY_JOY_AXIS_0_LEFT + 1,", file=f)
print("\tNUM_JOYSTICK_BUTTONS_PER_AXIS = KEY_JOY_AXIS_0_RIGHT - KEY_JOY_AXIS_0_LEFT + 1,", file=f)
print("\tNUM_JOYSTICK_AXES = NUM_JOYSTICK_AXES_BUTTONS / NUM_JOYSTICK_BUTTONS_PER_AXIS,", file=f)
print("\tNUM_JOYSTICK_HAT_BUTTONS = KEY_JOY_HAT1_RIGHTDOWN - KEY_JOY_HAT0_LEFTUP + 1,", file=f)
print("\tNUM_JOYSTICK_BUTTONS_PER_HAT = KEY_JOY_HAT1_RIGHTDOWN - KEY_JOY_HAT1_LEFTUP + 1,", file=f)
print("\tNUM_JOYSTICK_HAT_BUTTONS = KEY_JOY_HAT1_DOWN - KEY_JOY_HAT0_UP + 1,", file=f)
print("\tNUM_JOYSTICK_BUTTONS_PER_HAT = KEY_JOY_HAT1_DOWN - KEY_JOY_HAT1_UP + 1,", file=f)
print("\tNUM_JOYSTICK_HATS = NUM_JOYSTICK_HAT_BUTTONS / NUM_JOYSTICK_BUTTONS_PER_HAT,", file=f)

print("};", file=f)
Expand All @@ -128,8 +120,6 @@
print('#error do not include this header!', file=f)
print('#endif', file=f)
print('', file=f)
print("#include <string.h>", file=f)
print("", file=f)
print("const char g_aaKeyStrings[512][20] = // NOLINT(misc-definitions-in-headers)", file=f)
print("{", file=f)
for n in keynames:
Expand Down
4 changes: 2 additions & 2 deletions scripts/languages/twlang.py
Expand Up @@ -69,7 +69,7 @@ def decode(fileobj, elements_per_key):

def check_file(path):
with open(path, encoding="utf-8") as fileobj:
matches = re.findall(r"Localize\s*\(\s*\"([^\"]+)\"(?:\s*,\s*\"([^\"]+)\")?\s*\)", fileobj.read())
matches = re.findall(r"(Localize|Localizable)\s*\(\s*\"([^\"]+)\"(?:\s*,\s*\"([^\"]+)\")?\s*\)", fileobj.read())
return matches


Expand All @@ -81,7 +81,7 @@ def check_folder(path):
if not any(f.endswith(x) for x in ".cpp .c .h".split()):
continue
for sentence in check_file(os.path.join(path2, f)):
englishlist[sentence] = None
englishlist[sentence[1:]] = None
return englishlist


Expand Down
79 changes: 60 additions & 19 deletions scripts/move_sqlite.py
Expand Up @@ -16,45 +16,76 @@
import argparse
from time import strftime
import os
from datetime import datetime, timedelta

TABLES = ['record_race', 'record_teamrace', 'record_saves']

def sqlite_table_exists(cursor, table):
cursor.execute(f"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='{table}'")
return cursor.fetchone()[0] != 0

def sqlite_num_transfer(conn, table):
def sqlite_num_transfer(conn, table, date):
c = conn.cursor()
if not sqlite_table_exists(c, table):
return 0
c.execute(f'SELECT COUNT(*) FROM {table}')
query = f'SELECT COUNT(*) FROM {table}'
if date is not None:
query += f' WHERE Timestamp < DATETIME("{date}", "utc")'
c.execute(query)
num = c.fetchone()[0]
return num

def transfer(file_from, file_to):
def transfer(file_from, file_to, date, keep_timestamp_utc):
conn_to = sqlite3.connect(file_to, isolation_level='EXCLUSIVE')
cursor_to = conn_to.cursor()

conn_from = sqlite3.connect(file_from, isolation_level='EXCLUSIVE')
conn_from.text_factory = lambda b: b.decode(errors = 'ignore').rstrip()
for line in conn_from.iterdump():
cursor_to.execute(line)
print(line.encode('utf-8'))
for table in TABLES:
cursor_to.execute(f'INSERT INTO {table} SELECT * FROM {table}_backup WHERE Timestamp < DATETIME("{date}", "utc")')
cursor_to.close()
conn_to.commit()
conn_to.close()

cursor_from = conn_from.cursor()
if sqlite_table_exists(cursor_from, 'record_race'):
cursor_from.execute('DELETE FROM record_race')
if sqlite_table_exists(cursor_from, 'record_teamrace'):
cursor_from.execute('DELETE FROM record_teamrace')
if sqlite_table_exists(cursor_from, 'record_saves'):
cursor_from.execute('DELETE FROM record_saves')
for table in TABLES:
if sqlite_table_exists(cursor_from, table):
cursor_from.execute(f'DELETE FROM {table}')
backup_table = f'{table}_backup'
if sqlite_table_exists(cursor_from, backup_table):
cursor_from.execute(f'DELETE FROM {backup_table} WHERE Timestamp < DATETIME("{date}", "utc")')
cursor_from.close()
conn_from.commit()
conn_from.close()

cursor_to = conn_to.cursor()
# delete non-moved backup-rows:
for table in TABLES:
backup_table = f'{table}_backup'
if sqlite_table_exists(cursor_to, backup_table):
cursor_to.execute(f'DELETE FROM {backup_table}')

if not keep_timestamp_utc:
# change date from utc to wanted current timezone for mysql https://github.com/ddnet/ddnet/issues/6105
for table in TABLES:
cursor_to.execute(f'''
UPDATE {table}
SET Timestamp = DATETIME(original.Timestamp, "localtime")
FROM (
SELECT rowid, Timestamp FROM {table}
) as original
WHERE {table}.rowid = original.rowid''')

cursor_to.close()
conn_to.commit()

for line in conn_to.iterdump():
print(line.encode('utf-8'))
conn_to.close()

def main():
default_output = 'ddnet-server-' + strftime('%Y-%m-%d') + '.sqlite'
default_output = 'ddnet-server-' + strftime('%Y-%m-%dT%H:%M:%S') + '.sqlite'
parser = argparse.ArgumentParser(
description='Move DDNet ranks, teamranks and saves from a possible active SQLite3 to a new one',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Expand All @@ -64,22 +95,32 @@ def main():
parser.add_argument('--to', '-t',
default=default_output,
help='Output file where ranks are saved adds current date by default')
parser.add_argument('--backup-timeout',
default=60,
type=int,
help='Time in minutes until when a rank is moved from the _backup tables')
parser.add_argument('--keep-timestamp-utc',
default=False,
action="store_true",
help='Timestamps are converted to localtime by default. To keep them utc set this config option')
args = parser.parse_args()

if not os.path.exists(args.f):
print(f"Warning: '{args.f}' does not exist (yet). Is the path specified correctly?")
return

date = (datetime.now() - timedelta(minutes=args.backup_timeout)).strftime('%Y-%m-%d %H:%M:%S')

conn = sqlite3.connect(args.f)
num_ranks = sqlite_num_transfer(conn, 'record_race')
num_teamranks = sqlite_num_transfer(conn, 'record_teamrace')
num_saves = sqlite_num_transfer(conn, 'record_saves')
num = num_ranks + num_teamranks + num_saves
num = {}
for table in TABLES:
num[table] = sqlite_num_transfer(conn, table, None)
num[table] += sqlite_num_transfer(conn, f'{table}_backup', date)
conn.close()
if num == 0:
if sum(num.values()) == 0:
return

print(f'{num} new entries in backup database found ({num_ranks} ranks, {num_teamranks} teamranks, {num_saves} saves)')
print(f'{num} new entries in backup database found ({num["record_race"]} ranks, {num["record_teamrace"]} teamranks, {num["record_saves"]} saves)')
print(f'Moving entries from {os.path.abspath(args.f)} to {os.path.abspath(args.to)}')
print("You can use the following commands to import the entries to MySQL (using https://github.com/techouse/sqlite3-to-mysql/):")
print()
Expand All @@ -89,7 +130,7 @@ def main():
print("Log of the transfer:")
print()

transfer(args.f, args.to)
transfer(args.f, args.to, date, args.keep_timestamp_utc)

if __name__ == '__main__':
main()
2 changes: 1 addition & 1 deletion src/base/log.cpp
Expand Up @@ -116,7 +116,7 @@ void log_log_impl(LEVEL level, bool have_color, LOG_COLOR color, const char *sys
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
#if defined(CONF_FAMILY_WINDOWS)
_vsnprintf(pMessage, MessageSize, fmt, args);
_vsprintf_p(pMessage, MessageSize, fmt, args);
#else
vsnprintf(pMessage, MessageSize, fmt, args);
#endif
Expand Down
385 changes: 379 additions & 6 deletions src/base/system.cpp

Large diffs are not rendered by default.

99 changes: 98 additions & 1 deletion src/base/system.h
Expand Up @@ -14,9 +14,20 @@
#define __USE_GNU
#endif

#include <inttypes.h>
#include <stdint.h>
#include <time.h>

#ifdef __MINGW32__
#undef PRId64
#undef PRIu64
#define PRId64 "I64d"
#define PRIu64 "I64u"
#define PRIzu "Iu"
#else
#define PRIzu "zu"
#endif

#ifdef CONF_FAMILY_UNIX
#include <sys/un.h>
#endif
Expand Down Expand Up @@ -2430,7 +2441,7 @@ int kill_process(PROCESS process);
random - Pointer to a randomly-initialized array of shorts.
random_length - Length of the short array.
*/
void generate_password(char *buffer, unsigned length, unsigned short *random, unsigned random_length);
void generate_password(char *buffer, unsigned length, const unsigned short *random, unsigned random_length);

/*
Function: secure_random_init
Expand Down Expand Up @@ -2570,6 +2581,92 @@ class CWindowsComLifecycle
CWindowsComLifecycle(bool HasWindow);
~CWindowsComLifecycle();
};

/**
* Registers a protocol handler.
*
* @ingroup Shell
*
* @param protocol_name The name of the protocol.
* @param executable The absolute path of the executable that will be associated with the protocol.
* @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated.
*
* @return true on success, false on failure.
*
* @remark The caller must later call shell_update, iff the shell needs to be updated.
*/
bool shell_register_protocol(const char *protocol_name, const char *executable, bool *updated);

/**
* Registers a file extension.
*
* @ingroup Shell
*
* @param extension The file extension, including the leading dot.
* @param description A readable description for the file extension.
* @param executable_name A unique name that will used to describe the application.
* @param executable The absolute path of the executable that will be associated with the file extension.
* @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated.
*
* @return true on success, false on failure.
*
* @remark The caller must later call shell_update, iff the shell needs to be updated.
*/
bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable, bool *updated);

/**
* Registers an application.
*
* @ingroup Shell
*
* @param name Readable name of the application.
* @param executable The absolute path of the executable being registered.
* @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated.
*
* @return true on success, false on failure.
*
* @remark The caller must later call shell_update, iff the shell needs to be updated.
*/
bool shell_register_application(const char *name, const char *executable, bool *updated);

/**
* Unregisters a protocol or file extension handler.
*
* @ingroup Shell
*
* @param shell_class The shell class to delete.
* For protocols this is the name of the protocol.
* For file extensions this is the program ID associated with the file extension.
* @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated.
*
* @return true on success, false on failure.
*
* @remark The caller must later call shell_update, iff the shell needs to be updated.
*/
bool shell_unregister_class(const char *shell_class, bool *updated);

/**
* Unregisters an application.
*
* @ingroup Shell
*
* @param executable The absolute path of the executable being unregistered.
* @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated.
*
* @return true on success, false on failure.
*
* @remark The caller must later call shell_update, iff the shell needs to be updated.
*/
bool shell_unregister_application(const char *executable, bool *updated);

/**
* Notifies the system that a protocol or file extension has been changed and the shell needs to be updated.
*
* @ingroup Shell
*
* @remark This is a potentially expensive operation, so it should only be called when necessary.
*/
void shell_update();
#endif

/**
Expand Down
7 changes: 7 additions & 0 deletions src/engine/client.h
Expand Up @@ -84,6 +84,7 @@ class IClient : public IInterface
float m_aPredIntraTick[NUM_DUMMIES];

float m_LocalTime;
float m_GlobalTime;
float m_RenderFrameTime;

int m_GameTickSpeed;
Expand Down Expand Up @@ -146,6 +147,7 @@ class IClient : public IInterface
// other time access
inline float RenderFrameTime() const { return m_RenderFrameTime; }
inline float LocalTime() const { return m_LocalTime; }
inline float GlobalTime() const { return m_GlobalTime; }
inline float FrameTimeAvg() const { return m_FrameTimeAvg; }

// actions
Expand Down Expand Up @@ -278,6 +280,11 @@ class IClient : public IInterface
virtual CChecksumData *ChecksumData() = 0;
virtual bool InfoTaskRunning() = 0;
virtual int UdpConnectivity(int NetType) = 0;

#if defined(CONF_FAMILY_WINDOWS)
virtual void ShellRegister() = 0;
virtual void ShellUnregister() = 0;
#endif
};

class IGameClient : public IInterface
Expand Down
69 changes: 66 additions & 3 deletions src/engine/client/backend/backend_base.h
Expand Up @@ -8,8 +8,9 @@
#include <SDL_video.h>

#include <atomic>
#include <stddef.h>
#include <stdint.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

struct SBackendCapabilites;
Expand All @@ -23,9 +24,66 @@ enum EDebugGFXModes
DEBUG_GFX_MODE_ALL,
};

enum ERunCommandReturnTypes
{
RUN_COMMAND_COMMAND_HANDLED = 0,
RUN_COMMAND_COMMAND_UNHANDLED,
RUN_COMMAND_COMMAND_WARNING,
RUN_COMMAND_COMMAND_ERROR,
};

enum EGFXErrorType
{
GFX_ERROR_TYPE_NONE = 0,
GFX_ERROR_TYPE_INIT,
GFX_ERROR_TYPE_OUT_OF_MEMORY_IMAGE,
GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER,
GFX_ERROR_TYPE_OUT_OF_MEMORY_STAGING,
GFX_ERROR_TYPE_RENDER_RECORDING,
GFX_ERROR_TYPE_RENDER_CMD_FAILED,
GFX_ERROR_TYPE_RENDER_SUBMIT_FAILED,
GFX_ERROR_TYPE_SWAP_FAILED,
GFX_ERROR_TYPE_UNKNOWN,
};

enum EGFXWarningType
{
GFX_WARNING_TYPE_NONE = 0,
GFX_WARNING_TYPE_INIT_FAILED,
GFX_WARNING_TYPE_INIT_FAILED_MISSING_INTEGRATED_GPU_DRIVER,
GFX_WARNING_LOW_ON_MEMORY,
GFX_WARNING_MISSING_EXTENSION,
GFX_WARNING_TYPE_UNKNOWN,
};

struct SGFXErrorContainer
{
struct SError
{
bool m_RequiresTranslation;
std::string m_Err;

bool operator==(const SError &Other) const
{
return m_RequiresTranslation == Other.m_RequiresTranslation && m_Err == Other.m_Err;
}
};
EGFXErrorType m_ErrorType = EGFXErrorType::GFX_ERROR_TYPE_NONE;
std::vector<SError> m_vErrors;
};

struct SGFXWarningContainer
{
EGFXWarningType m_WarningType = EGFXWarningType::GFX_WARNING_TYPE_NONE;
std::vector<std::string> m_vWarnings;
};

class CCommandProcessorFragment_GLBase
{
protected:
SGFXErrorContainer m_Error;
SGFXWarningContainer m_Warning;

static size_t TexFormatToImageColorChannelCount(int TexFormat);
static void *Resize(const unsigned char *pData, int Width, int Height, int NewWidth, int NewHeight, int BPP);

Expand All @@ -35,11 +93,16 @@ class CCommandProcessorFragment_GLBase

public:
virtual ~CCommandProcessorFragment_GLBase() = default;
virtual bool RunCommand(const CCommandBuffer::SCommand *pBaseCommand) = 0;
virtual ERunCommandReturnTypes RunCommand(const CCommandBuffer::SCommand *pBaseCommand) = 0;

virtual void StartCommands(size_t CommandCount, size_t EstimatedRenderCallCount) {}
virtual void EndCommands() {}

const SGFXErrorContainer &GetError() { return m_Error; }
virtual void ErroneousCleanup() {}

const SGFXWarningContainer &GetWarning() { return m_Warning; }

enum
{
CMD_PRE_INIT = CCommandBuffer::CMDGROUP_PLATFORM_GL,
Expand Down
4 changes: 2 additions & 2 deletions src/engine/client/backend/glsl_shader_compiler.cpp
Expand Up @@ -67,7 +67,7 @@ void CGLSLCompiler::ParseLine(std::string &Line, const char *pReadLine, EGLSLSha
++pBuff;
}

if(*pBuff == ' ' && *(pBuff + 1) && *(pBuff + 1) == 'i' && *(pBuff + 2) == 'n')
if(*pBuff == ' ' && *(pBuff + 1) == 'i' && *(pBuff + 2) == 'n')
{
pBuff += 3;
Line.append("attribute");
Expand Down Expand Up @@ -95,7 +95,7 @@ void CGLSLCompiler::ParseLine(std::string &Line, const char *pReadLine, EGLSLSha
pBuff += 2;
Found = true;
}
else if(*pBuff == 'o' && *(pBuff + 1) && *(pBuff + 1) == 'u' && *(pBuff + 2) == 't')
else if(*pBuff == 'o' && *(pBuff + 1) == 'u' && *(pBuff + 2) == 't')
{
pBuff += 3;
Found = true;
Expand Down
4 changes: 2 additions & 2 deletions src/engine/client/backend/null/backend_null.cpp
Expand Up @@ -2,7 +2,7 @@

#include <engine/client/backend_sdl.h>

bool CCommandProcessorFragment_Null::RunCommand(const CCommandBuffer::SCommand *pBaseCommand)
ERunCommandReturnTypes CCommandProcessorFragment_Null::RunCommand(const CCommandBuffer::SCommand *pBaseCommand)
{
switch(pBaseCommand->m_Cmd)
{
Expand All @@ -19,7 +19,7 @@ bool CCommandProcessorFragment_Null::RunCommand(const CCommandBuffer::SCommand *
Cmd_TextTexture_Update(static_cast<const CCommandBuffer::SCommand_TextTexture_Update *>(pBaseCommand));
break;
}
return true;
return ERunCommandReturnTypes::RUN_COMMAND_COMMAND_HANDLED;
}

bool CCommandProcessorFragment_Null::Cmd_Init(const SCommand_Init *pCommand)
Expand Down
2 changes: 1 addition & 1 deletion src/engine/client/backend/null/backend_null.h
Expand Up @@ -6,7 +6,7 @@
class CCommandProcessorFragment_Null : public CCommandProcessorFragment_GLBase
{
bool GetPresentedImageData(uint32_t &Width, uint32_t &Height, uint32_t &Format, std::vector<uint8_t> &vDstData) override { return false; };
bool RunCommand(const CCommandBuffer::SCommand *pBaseCommand) override;
ERunCommandReturnTypes RunCommand(const CCommandBuffer::SCommand *pBaseCommand) override;
bool Cmd_Init(const SCommand_Init *pCommand);
virtual void Cmd_Texture_Create(const CCommandBuffer::SCommand_Texture_Create *pCommand);
virtual void Cmd_TextTextures_Create(const CCommandBuffer::SCommand_TextTextures_Create *pCommand);
Expand Down
12 changes: 5 additions & 7 deletions src/engine/client/backend/opengl/backend_opengl.cpp
Expand Up @@ -204,14 +204,12 @@ static void ParseVersionString(EBackendType BackendType, const char *pStr, int &
aCurNumberStr[CurNumberStrLen++] = (char)*pStr;
LastWasNumber = true;
}
else if(LastWasNumber && (*pStr == '.' || *pStr == ' ' || *pStr == '\0'))
else if(LastWasNumber && (*pStr == '.' || *pStr == ' '))
{
int CurNumber = 0;
if(CurNumberStrLen > 0)
{
aCurNumberStr[CurNumberStrLen] = 0;
CurNumber = str_toint(aCurNumberStr);
aNumbers[TotalNumbersPassed++] = CurNumber;
aNumbers[TotalNumbersPassed++] = str_toint(aCurNumberStr);
CurNumberStrLen = 0;
}

Expand Down Expand Up @@ -1061,7 +1059,7 @@ CCommandProcessorFragment_OpenGL::CCommandProcessorFragment_OpenGL()
m_HasShaders = false;
}

bool CCommandProcessorFragment_OpenGL::RunCommand(const CCommandBuffer::SCommand *pBaseCommand)
ERunCommandReturnTypes CCommandProcessorFragment_OpenGL::RunCommand(const CCommandBuffer::SCommand *pBaseCommand)
{
switch(pBaseCommand->m_Cmd)
{
Expand Down Expand Up @@ -1127,10 +1125,10 @@ bool CCommandProcessorFragment_OpenGL::RunCommand(const CCommandBuffer::SCommand
case CCommandBuffer::CMD_RENDER_QUAD_CONTAINER: Cmd_RenderQuadContainer(static_cast<const CCommandBuffer::SCommand_RenderQuadContainer *>(pBaseCommand)); break;
case CCommandBuffer::CMD_RENDER_QUAD_CONTAINER_EX: Cmd_RenderQuadContainerEx(static_cast<const CCommandBuffer::SCommand_RenderQuadContainerEx *>(pBaseCommand)); break;
case CCommandBuffer::CMD_RENDER_QUAD_CONTAINER_SPRITE_MULTIPLE: Cmd_RenderQuadContainerAsSpriteMultiple(static_cast<const CCommandBuffer::SCommand_RenderQuadContainerAsSpriteMultiple *>(pBaseCommand)); break;
default: return false;
default: return ERunCommandReturnTypes::RUN_COMMAND_COMMAND_UNHANDLED;
}

return true;
return ERunCommandReturnTypes::RUN_COMMAND_COMMAND_HANDLED;
}

// ------------ CCommandProcessorFragment_OpenGL2
Expand Down
2 changes: 1 addition & 1 deletion src/engine/client/backend/opengl/backend_opengl.h
Expand Up @@ -128,7 +128,7 @@ class CCommandProcessorFragment_OpenGL : public CCommandProcessorFragment_GLBase
CCommandProcessorFragment_OpenGL();
virtual ~CCommandProcessorFragment_OpenGL() = default;

bool RunCommand(const CCommandBuffer::SCommand *pBaseCommand) override;
ERunCommandReturnTypes RunCommand(const CCommandBuffer::SCommand *pBaseCommand) override;
};

class CCommandProcessorFragment_OpenGL2 : public CCommandProcessorFragment_OpenGL
Expand Down
2 changes: 1 addition & 1 deletion src/engine/client/backend/opengl/opengl_sl.cpp
Expand Up @@ -71,7 +71,7 @@ bool CGLSL::LoadShader(CGLSLCompiler *pCompiler, IStorage *pStorage, const char
}
}

for(CGLSLCompiler::SGLSLCompilerDefine &Define : pCompiler->m_vDefines)
for(const CGLSLCompiler::SGLSLCompilerDefine &Define : pCompiler->m_vDefines)
{
vLines.push_back(std::string("#define ") + Define.m_DefineName + std::string(" ") + Define.m_DefineValue + std::string("\r\n"));
}
Expand Down
1,240 changes: 800 additions & 440 deletions src/engine/client/backend/vulkan/backend_vulkan.cpp

Large diffs are not rendered by default.

158 changes: 150 additions & 8 deletions src/engine/client/backend_sdl.cpp
Expand Up @@ -5,11 +5,13 @@
#endif

#include <SDL.h>
#include <SDL_messagebox.h>

#include <base/math.h>
#include <cstdlib>

#include <engine/shared/config.h>
#include <engine/shared/localization.h>

#include <base/tl/threading.h>

Expand Down Expand Up @@ -72,7 +74,8 @@ void CGraphicsBackend_Threaded::ThreadFunc(void *pUser)
}
}

CGraphicsBackend_Threaded::CGraphicsBackend_Threaded()
CGraphicsBackend_Threaded::CGraphicsBackend_Threaded(TTranslateFunc &&TranslateFunc) :
m_TranslateFunc(std::move(TranslateFunc))
{
m_pBuffer = nullptr;
m_pProcessor = nullptr;
Expand All @@ -97,6 +100,7 @@ void CGraphicsBackend_Threaded::StopProcessor()
m_Shutdown = true;
{
std::unique_lock<std::mutex> Lock(m_BufferSwapMutex);
m_Warning = m_pProcessor->GetWarning();
m_BufferSwapCond.notify_all();
}
thread_wait(m_pThread);
Expand All @@ -106,13 +110,27 @@ void CGraphicsBackend_Threaded::RunBuffer(CCommandBuffer *pBuffer)
{
#ifdef CONF_WEBASM
// run everything single threaded for now, context binding in a thread seems to not work as of now
RunBufferSingleThreadedUnsafe(pBuffer);
if(!m_pProcessor->HasError())
{
RunBufferSingleThreadedUnsafe(pBuffer);
}
else
{
ProcessError();
}
#else
WaitForIdle();
std::unique_lock<std::mutex> Lock(m_BufferSwapMutex);
m_pBuffer = pBuffer;
m_BufferInProcess.store(true, std::memory_order_relaxed);
m_BufferSwapCond.notify_all();
if(!m_pProcessor->HasError())
{
m_pBuffer = pBuffer;
m_BufferInProcess.store(true, std::memory_order_relaxed);
m_BufferSwapCond.notify_all();
}
else
{
ProcessError();
}
#endif
}

Expand All @@ -132,6 +150,33 @@ void CGraphicsBackend_Threaded::WaitForIdle()
m_BufferSwapCond.wait(Lock, [this]() { return m_pBuffer == nullptr; });
}

void CGraphicsBackend_Threaded::ProcessError()
{
const auto &Error = m_pProcessor->GetError();
std::string VerboseStr;
for(const auto &ErrStr : Error.m_vErrors)
{
if(ErrStr.m_RequiresTranslation)
VerboseStr.append(std::string(m_TranslateFunc(ErrStr.m_Err.c_str(), "")) + "\n");
else
VerboseStr.append(ErrStr.m_Err + "\n");
}
const auto CreatedMsgBox = TryCreateMsgBox(true, "Graphics Assertion", VerboseStr.c_str());
// check if error msg can be shown, then assert
dbg_assert(!CreatedMsgBox, VerboseStr.c_str());
}

bool CGraphicsBackend_Threaded::GetWarning(std::vector<std::string> &WarningStrings)
{
if(HasWarning())
{
m_Warning.m_WarningType = GFX_WARNING_TYPE_NONE;
WarningStrings = m_Warning.m_vWarnings;
return true;
}
return false;
}

// ------------ CCommandProcessorFragment_General

void CCommandProcessorFragment_General::Cmd_Signal(const CCommandBuffer::SCommand_Signal *pCommand)
Expand Down Expand Up @@ -223,14 +268,87 @@ bool CCommandProcessorFragment_SDL::RunCommand(const CCommandBuffer::SCommand *p

// ------------ CCommandProcessor_SDL_GL

void CCommandProcessor_SDL_GL::HandleError()
{
auto &Error = GetError();
switch(Error.m_ErrorType)
{
case GFX_ERROR_TYPE_INIT:
Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.", "Graphics error")});
break;
case GFX_ERROR_TYPE_OUT_OF_MEMORY_IMAGE:
[[fallthrough]];
case GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER:
[[fallthrough]];
case GFX_ERROR_TYPE_OUT_OF_MEMORY_STAGING:
Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.", "Graphics error")});
break;
case GFX_ERROR_TYPE_RENDER_RECORDING:
Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("An error during command recording occurred. Try to update your GPU drivers.", "Graphics error")});
break;
case GFX_ERROR_TYPE_RENDER_CMD_FAILED:
Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("A render command failed. Try to update your GPU drivers.", "Graphics error")});
break;
case GFX_ERROR_TYPE_RENDER_SUBMIT_FAILED:
Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Submitting the render commands failed. Try to update your GPU drivers.", "Graphics error")});
break;
case GFX_ERROR_TYPE_SWAP_FAILED:
Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Failed to swap framebuffers. Try to update your GPU drivers.", "Graphics error")});
break;
case GFX_ERROR_TYPE_UNKNOWN:
[[fallthrough]];
default:
Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.", "Graphics error")});
break;
}
}

void CCommandProcessor_SDL_GL::HandleWarning()
{
auto &Warn = GetWarning();
switch(Warn.m_WarningType)
{
case GFX_WARNING_TYPE_INIT_FAILED:
Warn.m_vWarnings.emplace_back(Localizable("Could not initialize the given graphics backend, reverting to the default backend now.", "Graphics error"));
break;
case GFX_WARNING_TYPE_INIT_FAILED_MISSING_INTEGRATED_GPU_DRIVER:
Warn.m_vWarnings.emplace_back(Localizable("Could not initialize the given graphics backend, this is probably caused because you didn't install the driver of the integrated graphics card.", "Graphics error"));
break;
case GFX_WARNING_MISSING_EXTENSION:
// ignore this warning for now
return;
case GFX_WARNING_LOW_ON_MEMORY:
// ignore this warning for now
return;
default:
dbg_msg("gfx", "unhandled warning %d", (int)Warn.m_WarningType);
break;
}
}

void CCommandProcessor_SDL_GL::RunBuffer(CCommandBuffer *pBuffer)
{
m_pGLBackend->StartCommands(pBuffer->m_CommandCount, pBuffer->m_RenderCallCount);

for(CCommandBuffer::SCommand *pCommand = pBuffer->Head(); pCommand; pCommand = pCommand->m_pNext)
{
if(m_pGLBackend->RunCommand(pCommand))
auto Res = m_pGLBackend->RunCommand(pCommand);
if(Res == ERunCommandReturnTypes::RUN_COMMAND_COMMAND_HANDLED)
{
continue;
}
else if(Res == ERunCommandReturnTypes::RUN_COMMAND_COMMAND_ERROR)
{
m_Error = m_pGLBackend->GetError();
HandleError();
return;
}
else if(Res == ERunCommandReturnTypes::RUN_COMMAND_COMMAND_WARNING)
{
m_Warning = m_pGLBackend->GetWarning();
HandleWarning();
return;
}

if(m_SDL.RunCommand(pCommand))
continue;
Expand Down Expand Up @@ -299,6 +417,21 @@ CCommandProcessor_SDL_GL::~CCommandProcessor_SDL_GL()
delete m_pGLBackend;
}

SGFXErrorContainer &CCommandProcessor_SDL_GL::GetError()
{
return m_Error;
}

void CCommandProcessor_SDL_GL::ErroneousCleanup()
{
return m_pGLBackend->ErroneousCleanup();
}

SGFXWarningContainer &CCommandProcessor_SDL_GL::GetWarning()
{
return m_Warning;
}

// ------------ CGraphicsBackend_SDL_GL

static bool BackendInitGlew(EBackendType BackendType, int &GlewMajor, int &GlewMinor, int &GlewPatch)
Expand Down Expand Up @@ -641,6 +774,14 @@ void CGraphicsBackend_SDL_GL::ClampDriverVersion(EBackendType BackendType)
}
}

bool CGraphicsBackend_SDL_GL::TryCreateMsgBox(bool AsError, const char *pTitle, const char *pMsg)
{
m_pProcessor->ErroneousCleanup();
SDL_DestroyWindow(m_pWindow);
SDL_ShowSimpleMessageBox(AsError ? SDL_MESSAGEBOX_ERROR : SDL_MESSAGEBOX_WARNING, pTitle, pMsg, nullptr);
return true;
}

bool CGraphicsBackend_SDL_GL::IsModernAPI(EBackendType BackendType)
{
if(BackendType == BACKEND_TYPE_OPENGL)
Expand Down Expand Up @@ -838,7 +979,8 @@ void CGraphicsBackend_SDL_GL::GetCurrentVideoMode(CVideoMode &CurMode, int HiDPI
DisplayToVideoMode(&CurMode, &DPMode, HiDPIScale, DPMode.refresh_rate);
}

CGraphicsBackend_SDL_GL::CGraphicsBackend_SDL_GL()
CGraphicsBackend_SDL_GL::CGraphicsBackend_SDL_GL(TTranslateFunc &&TranslateFunc) :
CGraphicsBackend_Threaded(std::move(TranslateFunc))
{
mem_zero(m_aErrorString, std::size(m_aErrorString));
}
Expand Down Expand Up @@ -1473,4 +1615,4 @@ TGLBackendReadPresentedImageData &CGraphicsBackend_SDL_GL::GetReadPresentedImage
return m_ReadPresentedImageDataFunc;
}

IGraphicsBackend *CreateGraphicsBackend() { return new CGraphicsBackend_SDL_GL; }
IGraphicsBackend *CreateGraphicsBackend(TTranslateFunc &&TranslateFunc) { return new CGraphicsBackend_SDL_GL(std::move(TranslateFunc)); }
66 changes: 58 additions & 8 deletions src/engine/client/backend_sdl.h
Expand Up @@ -5,14 +5,18 @@

#include <base/detect.h>

#include "engine/graphics.h"
#include "graphics_threaded.h"
#include <engine/graphics.h>

#include <engine/client/graphics_threaded.h>

#include <engine/client/backend/backend_base.h>

#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <stddef.h>
#include <stdint.h>
#include <vector>

#if defined(CONF_PLATFORM_MACOS)
#include <objc/objc-runtime.h>
Expand Down Expand Up @@ -42,26 +46,55 @@ class CAutoreleasePool
// basic threaded backend, abstract, missing init and shutdown functions
class CGraphicsBackend_Threaded : public IGraphicsBackend
{
private:
TTranslateFunc m_TranslateFunc;
SGFXWarningContainer m_Warning;

public:
// constructed on the main thread, the rest of the functions is run on the render thread
class ICommandProcessor
{
public:
virtual ~ICommandProcessor() {}
virtual ~ICommandProcessor() = default;
virtual void RunBuffer(CCommandBuffer *pBuffer) = 0;

virtual SGFXErrorContainer &GetError() = 0;
virtual void ErroneousCleanup() = 0;

virtual SGFXWarningContainer &GetWarning() = 0;

bool HasError()
{
return GetError().m_ErrorType != GFX_ERROR_TYPE_NONE;
}

bool HasWarning()
{
return GetWarning().m_WarningType != GFX_WARNING_TYPE_NONE;
}
};

CGraphicsBackend_Threaded();
CGraphicsBackend_Threaded(TTranslateFunc &&TranslateFunc);

void RunBuffer(CCommandBuffer *pBuffer) override;
void RunBufferSingleThreadedUnsafe(CCommandBuffer *pBuffer) override;
bool IsIdle() const override;
void WaitForIdle() override;

void ProcessError();

protected:
void StartProcessor(ICommandProcessor *pProcessor);
void StopProcessor();

bool HasWarning()
{
return m_Warning.m_WarningType != GFX_WARNING_TYPE_NONE;
}

// returns true if the error msg was shown
virtual bool TryCreateMsgBox(bool AsError, const char *pTitle, const char *pMsg) = 0;

private:
ICommandProcessor *m_pProcessor;
std::mutex m_BufferSwapMutex;
Expand All @@ -73,6 +106,9 @@ class CGraphicsBackend_Threaded : public IGraphicsBackend
void *m_pThread;

static void ThreadFunc(void *pUser);

public:
bool GetWarning(std::vector<std::string> &WarningStrings) override;
};

// takes care of implementation independent operations
Expand Down Expand Up @@ -152,16 +188,27 @@ class CCommandProcessorFragment_SDL
// command processor implementation, uses the fragments to combine into one processor
class CCommandProcessor_SDL_GL : public CGraphicsBackend_Threaded::ICommandProcessor
{
class CCommandProcessorFragment_GLBase *m_pGLBackend;
CCommandProcessorFragment_GLBase *m_pGLBackend;
CCommandProcessorFragment_SDL m_SDL;
CCommandProcessorFragment_General m_General;

EBackendType m_BackendType;

SGFXErrorContainer m_Error;
SGFXWarningContainer m_Warning;

public:
CCommandProcessor_SDL_GL(EBackendType BackendType, int GLMajor, int GLMinor, int GLPatch);
virtual ~CCommandProcessor_SDL_GL();
void RunBuffer(CCommandBuffer *pBuffer) override;

SGFXErrorContainer &GetError() override;
void ErroneousCleanup() override;

SGFXWarningContainer &GetWarning() override;

void HandleError();
void HandleWarning();
};

static constexpr size_t gs_GPUInfoStringSize = 256;
Expand Down Expand Up @@ -196,8 +243,11 @@ class CGraphicsBackend_SDL_GL : public CGraphicsBackend_Threaded
static EBackendType DetectBackend();
static void ClampDriverVersion(EBackendType BackendType);

protected:
bool TryCreateMsgBox(bool AsError, const char *pTitle, const char *pMsg) override;

public:
CGraphicsBackend_SDL_GL();
CGraphicsBackend_SDL_GL(TTranslateFunc &&TranslateFunc);
int Init(const char *pName, int *pScreen, int *pWidth, int *pHeight, int *pRefreshRate, int *pFsaaSamples, int Flags, int *pDesktopWidth, int *pDesktopHeight, int *pCurrentWidth, int *pCurrentHeight, class IStorage *pStorage) override;
int Shutdown() override;

Expand Down
87 changes: 75 additions & 12 deletions src/engine/client/client.cpp
Expand Up @@ -70,11 +70,6 @@

#include "base/hash.h"

// for msvc
#ifndef PRIu64
#define PRIu64 "I64u"
#endif

#include <chrono>
#include <thread>

Expand Down Expand Up @@ -420,7 +415,7 @@ static inline bool RepackMsg(const CMsgPacker *pMsg, CPacker &Packer)
}
else
{
Packer.AddInt((0 << 1) | (pMsg->m_System ? 1 : 0)); // NETMSG_EX, NETMSGTYPE_EX
Packer.AddInt(pMsg->m_System ? 1 : 0); // NETMSG_EX, NETMSGTYPE_EX
g_UuidManager.PackUuid(pMsg->m_MsgID, &Packer);
}
Packer.AddRaw(pMsg->Data(), pMsg->Size());
Expand Down Expand Up @@ -894,7 +889,7 @@ void CClient::Disconnect()
// make sure to remove replay tmp demo
if(g_Config.m_ClReplays)
{
Storage()->RemoveFile(m_aDemoRecorder[RECORDER_REPLAYS].GetCurrentFilename(), IStorage::TYPE_SAVE);
DemoRecorder_Stop(RECORDER_REPLAYS, true);
}
}

Expand Down Expand Up @@ -2884,8 +2879,7 @@ void CClient::Update()
}

// update the server browser
m_ServerBrowser.Update(m_ResortServerBrowser);
m_ResortServerBrowser = false;
m_ServerBrowser.Update();

// update editor/gameclient
if(m_EditorActive)
Expand Down Expand Up @@ -2965,7 +2959,7 @@ void CClient::InitInterfaces()

void CClient::Run()
{
m_LocalStartTime = time_get();
m_LocalStartTime = m_GlobalStartTime = time_get();
#if defined(CONF_VIDEORECORDER)
IVideo::SetLocalStartTime(m_LocalStartTime);
#endif
Expand Down Expand Up @@ -3176,6 +3170,18 @@ void CClient::Run()
else
SetState(IClient::STATE_QUITTING); // SDL_QUIT
}

char aFile[IO_MAX_PATH_LENGTH];
if(Input()->GetDropFile(aFile, sizeof(aFile)))
{
if(str_startswith(aFile, CONNECTLINK_NO_SLASH))
HandleConnectLink(aFile);
else if(str_endswith(aFile, ".demo"))
HandleDemoPath(aFile);
else if(str_endswith(aFile, ".map"))
HandleMapPath(aFile);
}

#if defined(CONF_AUTOUPDATE)
Updater()->Update();
#endif
Expand Down Expand Up @@ -3384,8 +3390,9 @@ void CClient::Run()
g_Config.m_DbgHitch = 0;
}

// update local time
// update local and global time
m_LocalTime = (time_get() - m_LocalStartTime) / (float)time_freq();
m_GlobalTime = (time_get() - m_GlobalStartTime) / (float)time_freq();
}

#if defined(CONF_FAMILY_UNIX)
Expand Down Expand Up @@ -3971,7 +3978,10 @@ void CClient::DemoRecorder_Stop(int Recorder, bool RemoveFile)
{
const char *pFilename = m_aDemoRecorder[Recorder].GetCurrentFilename();
if(pFilename[0] != '\0')
{
Storage()->RemoveFile(pFilename, IStorage::TYPE_SAVE);
m_aDemoRecorder[Recorder].ClearCurrentFilename();
}
}
}

Expand Down Expand Up @@ -4031,7 +4041,7 @@ void CClient::UpdateAndSwap()

void CClient::ServerBrowserUpdate()
{
m_ResortServerBrowser = true;
m_ServerBrowser.RequestResort();
}

void CClient::ConchainServerBrowserUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
Expand Down Expand Up @@ -4694,6 +4704,11 @@ int main(int argc, const char **argv)
}
}

// Register protocol and file extensions
#if defined(CONF_FAMILY_WINDOWS)
pClient->ShellRegister();
#endif

// init SDL
if(SDL_Init(0) < 0)
{
Expand Down Expand Up @@ -4887,3 +4902,51 @@ int CClient::UdpConnectivity(int NetType)
}
return Connectivity;
}

#if defined(CONF_FAMILY_WINDOWS)
void CClient::ShellRegister()
{
char aFullPath[IO_MAX_PATH_LENGTH];
Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath));
if(!aFullPath[0])
{
dbg_msg("client", "Failed to register protocol and file extensions: could not determine absolute path");
return;
}

bool Updated = false;
if(!shell_register_protocol("ddnet", aFullPath, &Updated))
dbg_msg("client", "Failed to register ddnet protocol");
if(!shell_register_extension(".map", "Map File", GAME_NAME, aFullPath, &Updated))
dbg_msg("client", "Failed to register .map file extension");
if(!shell_register_extension(".demo", "Demo File", GAME_NAME, aFullPath, &Updated))
dbg_msg("client", "Failed to register .demo file extension");
if(!shell_register_application(GAME_NAME, aFullPath, &Updated))
dbg_msg("client", "Failed to register application");
if(Updated)
shell_update();
}

void CClient::ShellUnregister()
{
char aFullPath[IO_MAX_PATH_LENGTH];
Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath));
if(!aFullPath[0])
{
dbg_msg("client", "Failed to unregister protocol and file extensions: could not determine absolute path");
return;
}

bool Updated = false;
if(!shell_unregister_class("ddnet", &Updated))
dbg_msg("client", "Failed to unregister ddnet protocol");
if(!shell_unregister_class(GAME_NAME ".map", &Updated))
dbg_msg("client", "Failed to unregister .map file extension");
if(!shell_unregister_class(GAME_NAME ".demo", &Updated))
dbg_msg("client", "Failed to unregister .demo file extension");
if(!shell_unregister_application(aFullPath, &Updated))
dbg_msg("client", "Failed to unregister application");
if(Updated)
shell_update();
}
#endif
7 changes: 6 additions & 1 deletion src/engine/client/client.h
Expand Up @@ -143,6 +143,7 @@ class CClient : public IClient, public CDemoPlayer::IListener

uint64_t m_aSnapshotParts[NUM_DUMMIES];
int64_t m_LocalStartTime;
int64_t m_GlobalStartTime;

IGraphics::CTextureHandle m_DebugFont;
int m_DebugSoundIndex = 0;
Expand All @@ -158,7 +159,6 @@ class CClient : public IClient, public CDemoPlayer::IListener
bool m_AutoCSVRecycle;
bool m_EditorActive;
bool m_SoundInitFailed;
bool m_ResortServerBrowser;

int m_aAckGameTick[NUM_DUMMIES];
int m_aCurrentRecvTick[NUM_DUMMIES];
Expand Down Expand Up @@ -546,6 +546,11 @@ class CClient : public IClient, public CDemoPlayer::IListener
CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; }
bool InfoTaskRunning() override { return m_pDDNetInfoTask != nullptr; }
int UdpConnectivity(int NetType) override;

#if defined(CONF_FAMILY_WINDOWS)
void ShellRegister() override;
void ShellUnregister() override;
#endif
};

#endif
13 changes: 12 additions & 1 deletion src/engine/client/graphics_threaded.cpp
Expand Up @@ -794,6 +794,17 @@ void CGraphics_Threaded::KickCommandBuffer()
{
m_pBackend->RunBuffer(m_pCommandBuffer);

std::vector<std::string> WarningStrings;
if(m_pBackend->GetWarning(WarningStrings))
{
SWarning NewWarning;
std::string WarningStr;
for(const auto &WarnStr : WarningStrings)
WarningStr.append((WarnStr + "\n"));
str_copy(NewWarning.m_aWarningMsg, WarningStr.c_str());
m_vWarnings.emplace_back(NewWarning);
}

// swap buffer
m_CurrentCommandBuffer ^= 1;
m_pCommandBuffer = m_apCommandBuffers[m_CurrentCommandBuffer];
Expand Down Expand Up @@ -2830,7 +2841,7 @@ int CGraphics_Threaded::Init()
m_FirstFreeBufferObjectIndex = -1;
m_FirstFreeQuadContainer = -1;

m_pBackend = CreateGraphicsBackend();
m_pBackend = CreateGraphicsBackend(Localize);
if(InitWindow() != 0)
return -1;

Expand Down
8 changes: 6 additions & 2 deletions src/engine/client/graphics_threaded.h
Expand Up @@ -5,6 +5,7 @@
#include <engine/shared/config.h>

#include <cstddef>
#include <string>
#include <vector>

constexpr int CMD_BUFFER_DATA_BUFFER_SIZE = 1024 * 1024 * 2;
Expand Down Expand Up @@ -714,7 +715,7 @@ class IGraphicsBackend
INITFLAG_DESKTOP_FULLSCREEN = 1 << 5,
};

virtual ~IGraphicsBackend() {}
virtual ~IGraphicsBackend() = default;

virtual int Init(const char *pName, int *pScreen, int *pWidth, int *pHeight, int *pRefreshRate, int *pFsaaSamples, int Flags, int *pDesktopWidth, int *pDesktopHeight, int *pCurrentWidth, int *pCurrentHeight, class IStorage *pStorage) = 0;
virtual int Shutdown() = 0;
Expand Down Expand Up @@ -770,6 +771,8 @@ class IGraphicsBackend

// be aware that this function should only be called from the graphics thread, and even then you should really know what you are doing
virtual TGLBackendReadPresentedImageData &GetReadPresentedImageDataFuncUnsafe() = 0;

virtual bool GetWarning(std::vector<std::string> &WarningStrings) = 0;
};

class CGraphics_Threaded : public IEngineGraphics
Expand Down Expand Up @@ -1313,6 +1316,7 @@ class CGraphics_Threaded : public IEngineGraphics
TGLBackendReadPresentedImageData &GetReadPresentedImageDataFuncUnsafe() override;
};

extern IGraphicsBackend *CreateGraphicsBackend();
typedef std::function<const char *(const char *, const char *)> TTranslateFunc;
extern IGraphicsBackend *CreateGraphicsBackend(TTranslateFunc &&TranslateFunc);

#endif // ENGINE_CLIENT_GRAPHICS_THREADED_H
193 changes: 124 additions & 69 deletions src/engine/client/input.cpp
Expand Up @@ -60,6 +60,8 @@ CInput::CInput()
m_NumTextInputInstances = 0;
m_EditingTextLen = -1;
m_aEditingText[0] = 0;

m_aDropFile[0] = 0;
}

void CInput::Init()
Expand Down Expand Up @@ -91,42 +93,40 @@ void CInput::InitJoysticks()
}
}

int NumJoysticks = SDL_NumJoysticks();
if(NumJoysticks > 0)
const int NumJoysticks = SDL_NumJoysticks();
dbg_msg("joystick", "%d joystick(s) found", NumJoysticks);
for(int i = 0; i < NumJoysticks; i++)
OpenJoystick(i);
UpdateActiveJoystick();

Console()->Chain("joystick_guid", ConchainJoystickGuidChanged, this);
}

bool CInput::OpenJoystick(int JoystickIndex)
{
SDL_Joystick *pJoystick = SDL_JoystickOpen(JoystickIndex);
if(!pJoystick)
{
dbg_msg("joystick", "%d joystick(s) found", NumJoysticks);
int ActualIndex = 0;
for(int i = 0; i < NumJoysticks; i++)
{
SDL_Joystick *pJoystick = SDL_JoystickOpen(i);
if(!pJoystick)
{
dbg_msg("joystick", "Could not open joystick %d: '%s'", i, SDL_GetError());
continue;
}
m_vJoysticks.emplace_back(this, ActualIndex, pJoystick);
const CJoystick &Joystick = m_vJoysticks[m_vJoysticks.size() - 1];
dbg_msg("joystick", "Opened Joystick %d '%s' (%d axes, %d buttons, %d balls, %d hats)", i, Joystick.GetName(),
Joystick.GetNumAxes(), Joystick.GetNumButtons(), Joystick.GetNumBalls(), Joystick.GetNumHats());
ActualIndex++;
}
if(ActualIndex > 0)
{
UpdateActiveJoystick();
Console()->Chain("joystick_guid", ConchainJoystickGuidChanged, this);
}
dbg_msg("joystick", "Could not open joystick %d: '%s'", JoystickIndex, SDL_GetError());
return false;
}
else
if(std::find_if(m_vJoysticks.begin(), m_vJoysticks.end(), [pJoystick](const CJoystick &Joystick) -> bool { return Joystick.m_pDelegate == pJoystick; }) != m_vJoysticks.end())
{
dbg_msg("joystick", "No joysticks found");
// Joystick has already been added
return false;
}
m_vJoysticks.emplace_back(this, m_vJoysticks.size(), pJoystick);
const CJoystick &Joystick = m_vJoysticks[m_vJoysticks.size() - 1];
dbg_msg("joystick", "Opened joystick %d '%s' (%d axes, %d buttons, %d balls, %d hats)", JoystickIndex, Joystick.GetName(),
Joystick.GetNumAxes(), Joystick.GetNumButtons(), Joystick.GetNumBalls(), Joystick.GetNumHats());
return true;
}

void CInput::UpdateActiveJoystick()
{
m_pActiveJoystick = nullptr;
if(m_vJoysticks.empty())
return;
m_pActiveJoystick = nullptr;
for(auto &Joystick : m_vJoysticks)
{
if(str_comp(Joystick.GetGUID(), g_Config.m_InpControllerGUID) == 0)
Expand Down Expand Up @@ -189,25 +189,26 @@ float CInput::CJoystick::GetAxisValue(int Axis)
return (SDL_JoystickGetAxis(m_pDelegate, Axis) - SDL_JOYSTICK_AXIS_MIN) / (float)(SDL_JOYSTICK_AXIS_MAX - SDL_JOYSTICK_AXIS_MIN) * 2.0f - 1.0f;
}

int CInput::CJoystick::GetJoystickHatKey(int Hat, int HatValue)
void CInput::CJoystick::GetJoystickHatKeys(int Hat, int HatValue, int (&HatKeys)[2])
{
switch(HatValue)
{
case SDL_HAT_LEFTUP: return KEY_JOY_HAT0_LEFTUP + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
case SDL_HAT_UP: return KEY_JOY_HAT0_UP + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
case SDL_HAT_RIGHTUP: return KEY_JOY_HAT0_RIGHTUP + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
case SDL_HAT_LEFT: return KEY_JOY_HAT0_LEFT + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
case SDL_HAT_RIGHT: return KEY_JOY_HAT0_RIGHT + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
case SDL_HAT_LEFTDOWN: return KEY_JOY_HAT0_LEFTDOWN + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
case SDL_HAT_DOWN: return KEY_JOY_HAT0_DOWN + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
case SDL_HAT_RIGHTDOWN: return KEY_JOY_HAT0_RIGHTDOWN + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
}
return -1;
if(HatValue & SDL_HAT_UP)
HatKeys[0] = KEY_JOY_HAT0_UP + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
else if(HatValue & SDL_HAT_DOWN)
HatKeys[0] = KEY_JOY_HAT0_DOWN + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
else
HatKeys[0] = KEY_UNKNOWN;

if(HatValue & SDL_HAT_LEFT)
HatKeys[1] = KEY_JOY_HAT0_LEFT + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
else if(HatValue & SDL_HAT_RIGHT)
HatKeys[1] = KEY_JOY_HAT0_RIGHT + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT;
else
HatKeys[1] = KEY_UNKNOWN;
}

int CInput::CJoystick::GetHatValue(int Hat)
void CInput::CJoystick::GetHatValue(int Hat, int (&HatKeys)[2])
{
return GetJoystickHatKey(Hat, SDL_JoystickGetHat(m_pDelegate, Hat));
return GetJoystickHatKeys(Hat, SDL_JoystickGetHat(m_pDelegate, Hat), HatKeys);
}

bool CInput::CJoystick::Relative(float *pX, float *pY)
Expand Down Expand Up @@ -377,62 +378,63 @@ void CInput::UpdateJoystickState()

for(int Hat = 0; Hat < pJoystick->GetNumHats(); Hat++)
{
const int HatState = pJoystick->GetHatValue(Hat);
for(int Key = KEY_JOY_HAT0_LEFTUP + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT; Key <= KEY_JOY_HAT0_RIGHTDOWN + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT; Key++)
m_aInputState[Key] = Key == HatState;
int HatKeys[2];
pJoystick->GetHatValue(Hat, HatKeys);
for(int Key = KEY_JOY_HAT0_UP + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT; Key <= KEY_JOY_HAT0_DOWN + Hat * NUM_JOYSTICK_BUTTONS_PER_HAT; Key++)
m_aInputState[Key] = HatKeys[0] == Key || HatKeys[1] == Key;
}
}

void CInput::HandleJoystickAxisMotionEvent(const SDL_Event &Event)
void CInput::HandleJoystickAxisMotionEvent(const SDL_JoyAxisEvent &Event)
{
if(!g_Config.m_InpControllerEnable)
return;
CJoystick *pJoystick = GetActiveJoystick();
if(!pJoystick || pJoystick->GetInstanceID() != Event.jaxis.which)
if(!pJoystick || pJoystick->GetInstanceID() != Event.which)
return;
if(Event.jaxis.axis >= NUM_JOYSTICK_AXES)
if(Event.axis >= NUM_JOYSTICK_AXES)
return;

const int LeftKey = KEY_JOY_AXIS_0_LEFT + 2 * Event.jaxis.axis;
const int LeftKey = KEY_JOY_AXIS_0_LEFT + 2 * Event.axis;
const int RightKey = LeftKey + 1;
const float DeadZone = GetJoystickDeadzone();

if(Event.jaxis.value <= SDL_JOYSTICK_AXIS_MIN * DeadZone && !m_aInputState[LeftKey])
if(Event.value <= SDL_JOYSTICK_AXIS_MIN * DeadZone && !m_aInputState[LeftKey])
{
m_aInputState[LeftKey] = true;
m_aInputCount[LeftKey] = m_InputCounter;
AddEvent(0, LeftKey, IInput::FLAG_PRESS);
}
else if(Event.jaxis.value > SDL_JOYSTICK_AXIS_MIN * DeadZone && m_aInputState[LeftKey])
else if(Event.value > SDL_JOYSTICK_AXIS_MIN * DeadZone && m_aInputState[LeftKey])
{
m_aInputState[LeftKey] = false;
AddEvent(0, LeftKey, IInput::FLAG_RELEASE);
}

if(Event.jaxis.value >= SDL_JOYSTICK_AXIS_MAX * DeadZone && !m_aInputState[RightKey])
if(Event.value >= SDL_JOYSTICK_AXIS_MAX * DeadZone && !m_aInputState[RightKey])
{
m_aInputState[RightKey] = true;
m_aInputCount[RightKey] = m_InputCounter;
AddEvent(0, RightKey, IInput::FLAG_PRESS);
}
else if(Event.jaxis.value < SDL_JOYSTICK_AXIS_MAX * DeadZone && m_aInputState[RightKey])
else if(Event.value < SDL_JOYSTICK_AXIS_MAX * DeadZone && m_aInputState[RightKey])
{
m_aInputState[RightKey] = false;
AddEvent(0, RightKey, IInput::FLAG_RELEASE);
}
}

void CInput::HandleJoystickButtonEvent(const SDL_Event &Event)
void CInput::HandleJoystickButtonEvent(const SDL_JoyButtonEvent &Event)
{
if(!g_Config.m_InpControllerEnable)
return;
CJoystick *pJoystick = GetActiveJoystick();
if(!pJoystick || pJoystick->GetInstanceID() != Event.jbutton.which)
if(!pJoystick || pJoystick->GetInstanceID() != Event.which)
return;
if(Event.jbutton.button >= NUM_JOYSTICK_BUTTONS)
if(Event.button >= NUM_JOYSTICK_BUTTONS)
return;

const int Key = Event.jbutton.button + KEY_JOYSTICK_BUTTON_0;
const int Key = Event.button + KEY_JOYSTICK_BUTTON_0;

if(Event.type == SDL_JOYBUTTONDOWN)
{
Expand All @@ -447,32 +449,61 @@ void CInput::HandleJoystickButtonEvent(const SDL_Event &Event)
}
}

void CInput::HandleJoystickHatMotionEvent(const SDL_Event &Event)
void CInput::HandleJoystickHatMotionEvent(const SDL_JoyHatEvent &Event)
{
if(!g_Config.m_InpControllerEnable)
return;
CJoystick *pJoystick = GetActiveJoystick();
if(!pJoystick || pJoystick->GetInstanceID() != Event.jhat.which)
if(!pJoystick || pJoystick->GetInstanceID() != Event.which)
return;
if(Event.jhat.hat >= NUM_JOYSTICK_HATS)
if(Event.hat >= NUM_JOYSTICK_HATS)
return;

const int CurrentKey = CJoystick::GetJoystickHatKey(Event.jhat.hat, Event.jhat.value);
int HatKeys[2];
CJoystick::GetJoystickHatKeys(Event.hat, Event.value, HatKeys);

for(int Key = KEY_JOY_HAT0_LEFTUP + Event.jhat.hat * NUM_JOYSTICK_BUTTONS_PER_HAT; Key <= KEY_JOY_HAT0_RIGHTDOWN + Event.jhat.hat * NUM_JOYSTICK_BUTTONS_PER_HAT; Key++)
for(int Key = KEY_JOY_HAT0_UP + Event.hat * NUM_JOYSTICK_BUTTONS_PER_HAT; Key <= KEY_JOY_HAT0_DOWN + Event.hat * NUM_JOYSTICK_BUTTONS_PER_HAT; Key++)
{
if(Key != CurrentKey && m_aInputState[Key])
if(Key != HatKeys[0] && Key != HatKeys[1] && m_aInputState[Key])
{
m_aInputState[Key] = false;
AddEvent(0, Key, IInput::FLAG_RELEASE);
}
}

if(CurrentKey >= 0)
for(int CurrentKey : HatKeys)
{
m_aInputState[CurrentKey] = true;
m_aInputCount[CurrentKey] = m_InputCounter;
AddEvent(0, CurrentKey, IInput::FLAG_PRESS);
if(CurrentKey != KEY_UNKNOWN && !m_aInputState[CurrentKey])
{
m_aInputState[CurrentKey] = true;
m_aInputCount[CurrentKey] = m_InputCounter;
AddEvent(0, CurrentKey, IInput::FLAG_PRESS);
}
}
}

void CInput::HandleJoystickAddedEvent(const SDL_JoyDeviceEvent &Event)
{
if(OpenJoystick(Event.which))
{
UpdateActiveJoystick();
}
}

void CInput::HandleJoystickRemovedEvent(const SDL_JoyDeviceEvent &Event)
{
auto RemovedJoystick = std::find_if(m_vJoysticks.begin(), m_vJoysticks.end(), [Event](const CJoystick &Joystick) -> bool { return Joystick.GetInstanceID() == Event.which; });
if(RemovedJoystick != m_vJoysticks.end())
{
dbg_msg("joystick", "Closed joystick %d '%s'", (*RemovedJoystick).GetIndex(), (*RemovedJoystick).GetName());
auto NextJoystick = m_vJoysticks.erase(RemovedJoystick);
// Adjust indices of following joysticks
while(NextJoystick != m_vJoysticks.end())
{
(*NextJoystick).m_Index--;
++NextJoystick;
}
UpdateActiveJoystick();
}
}

Expand Down Expand Up @@ -604,16 +635,24 @@ int CInput::Update()

// handle the joystick events
case SDL_JOYAXISMOTION:
HandleJoystickAxisMotionEvent(Event);
HandleJoystickAxisMotionEvent(Event.jaxis);
break;

case SDL_JOYBUTTONUP:
case SDL_JOYBUTTONDOWN:
HandleJoystickButtonEvent(Event);
HandleJoystickButtonEvent(Event.jbutton);
break;

case SDL_JOYHATMOTION:
HandleJoystickHatMotionEvent(Event);
HandleJoystickHatMotionEvent(Event.jhat);
break;

case SDL_JOYDEVICEADDED:
HandleJoystickAddedEvent(Event.jdevice);
break;

case SDL_JOYDEVICEREMOVED:
HandleJoystickRemovedEvent(Event.jdevice);
break;

// handle mouse buttons
Expand Down Expand Up @@ -714,6 +753,11 @@ int CInput::Update()
// other messages
case SDL_QUIT:
return 1;

case SDL_DROPFILE:
str_copy(m_aDropFile, Event.drop.file);
SDL_free(Event.drop.file);
break;
}

if(Scancode > KEY_FIRST && Scancode < g_MaxKeys && !IgnoreKeys && (!SDL_IsTextInputActive() || m_EditingTextLen == -1))
Expand All @@ -740,6 +784,17 @@ int CInput::VideoRestartNeeded()
return 0;
}

bool CInput::GetDropFile(char *aBuf, int Len)
{
if(m_aDropFile[0] != '\0')
{
str_copy(aBuf, m_aDropFile, Len);
m_aDropFile[0] = '\0';
return true;
}
return false;
}

IEngineInput *CreateEngineInput()
{
return new CInput;
Expand Down
22 changes: 17 additions & 5 deletions src/engine/client/input.h
Expand Up @@ -3,6 +3,9 @@
#ifndef ENGINE_CLIENT_INPUT_H
#define ENGINE_CLIENT_INPUT_H

#include <SDL_events.h>
#include <SDL_joystick.h>

#include <engine/input.h>
#include <engine/keys.h>

Expand All @@ -13,6 +16,8 @@ class CInput : public IEngineInput
public:
class CJoystick : public IJoystick
{
friend class CInput;

CInput *m_pInput;
int m_Index;
char m_aName[64];
Expand Down Expand Up @@ -40,11 +45,11 @@ class CInput : public IEngineInput
int GetNumBalls() const override { return m_NumBalls; }
int GetNumHats() const override { return m_NumHats; }
float GetAxisValue(int Axis) override;
int GetHatValue(int Hat) override;
void GetHatValue(int Hat, int (&HatKeys)[2]) override;
bool Relative(float *pX, float *pY) override;
bool Absolute(float *pX, float *pY) override;

static int GetJoystickHatKey(int Hat, int HatValue);
static void GetJoystickHatKeys(int Hat, int HatValue, int (&HatKeys)[2]);
};

private:
Expand All @@ -58,6 +63,7 @@ class CInput : public IEngineInput
std::vector<CJoystick> m_vJoysticks;
CJoystick *m_pActiveJoystick = nullptr;
void InitJoysticks();
bool OpenJoystick(int JoystickIndex);
void CloseJoysticks();
void UpdateActiveJoystick();
static void ConchainJoystickGuidChanged(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
Expand All @@ -82,9 +88,13 @@ class CInput : public IEngineInput

void UpdateMouseState();
void UpdateJoystickState();
void HandleJoystickAxisMotionEvent(const SDL_Event &Event);
void HandleJoystickButtonEvent(const SDL_Event &Event);
void HandleJoystickHatMotionEvent(const SDL_Event &Event);
void HandleJoystickAxisMotionEvent(const SDL_JoyAxisEvent &Event);
void HandleJoystickButtonEvent(const SDL_JoyButtonEvent &Event);
void HandleJoystickHatMotionEvent(const SDL_JoyHatEvent &Event);
void HandleJoystickAddedEvent(const SDL_JoyDeviceEvent &Event);
void HandleJoystickRemovedEvent(const SDL_JoyDeviceEvent &Event);

char m_aDropFile[IO_MAX_PATH_LENGTH];

// IME support
int m_NumTextInputInstances;
Expand Down Expand Up @@ -130,6 +140,8 @@ class CInput : public IEngineInput
const char *GetIMEEditingText() override;
int GetEditingCursor() override;
void SetEditingPosition(float X, float Y) override;

bool GetDropFile(char *aBuf, int Len) override;
};

#endif
16 changes: 8 additions & 8 deletions src/engine/client/keynames.h
Expand Up @@ -316,22 +316,14 @@ const char g_aaKeyStrings[512][20] = // NOLINT(misc-definitions-in-headers)
"joystick9",
"joystick10",
"joystick11",
"joy_hat0_leftup",
"joy_hat0_up",
"joy_hat0_rightup",
"joy_hat0_left",
"joy_hat0_right",
"joy_hat0_leftdown",
"joy_hat0_down",
"joy_hat0_rightdown",
"joy_hat1_leftup",
"joy_hat1_up",
"joy_hat1_rightup",
"joy_hat1_left",
"joy_hat1_right",
"joy_hat1_leftdown",
"joy_hat1_down",
"joy_hat1_rightdown",
"joy_axis0_left",
"joy_axis0_right",
"joy_axis1_left",
Expand All @@ -356,6 +348,14 @@ const char g_aaKeyStrings[512][20] = // NOLINT(misc-definitions-in-headers)
"joy_axis10_right",
"joy_axis11_left",
"joy_axis11_right",
"&342",
"&343",
"&344",
"&345",
"&346",
"&347",
"&348",
"&349",
"&350",
"&351",
"&352",
Expand Down