Compare commits

...

90 commits

Author SHA1 Message Date
dobrykafe cbf65b883a
Merge f4b2f49056 into bb974c94ca 2024-11-01 22:55:24 +08:00
Dennis Felsing bb974c94ca
Merge pull request #9188 from Robyt3/Client-Hook-Collision-Width-Fix
Fix upper bound of hook collision line width scrollbar
2024-11-01 12:27:04 +00:00
Robert Müller e965ccdcac Fix upper bound of hook collision line width scrollbar
The maximum value of the config variable is only `20`. Regression from #9138.
2024-11-01 11:00:11 +01:00
Robert Müller 3345bc9da0
Merge pull request #9186 from def-/pr-transl-18.7
Update translations for upcoming 18.7
2024-11-01 09:06:00 +00:00
Dennis Felsing d6e31d35fe Update translations for upcoming 18.7 2024-11-01 09:12:18 +01:00
Dennis Felsing 19aa7fbb77 Version 18.8 2024-11-01 09:11:04 +01:00
Dennis Felsing 0f91b1287d
Merge pull request #9185 from Robyt3/Client-Font-Error
Improve error handling on invalid fonts and font index
2024-11-01 07:50:48 +00:00
Dennis Felsing 6bcf8ead49 Fix clang-tidy 2024-11-01 08:32:26 +01:00
Robert Müller 96eb49ad3f Improve error handling on invalid fonts and font index
Log more detailed error messages when fonts could not be loaded and when the font index is malformed. Show warning popup on client launch if any font failed to load, although this warning won't be readable if all fonts failed to be loaded.
2024-11-01 00:16:14 +01:00
Dennis Felsing f827b6e999
Merge pull request #9183 from KebsCS/pr-fix-command-preview
Fix chat command preview overlap
2024-10-31 21:27:15 +00:00
KebsCS 6dc11bbb89
Fix chat command preview overlap 2024-10-31 21:33:52 +01:00
Dennis Felsing 215c683692
Merge pull request #9182 from Robyt3/Client-clear_chat-Command
Add `clear_chat` command
2024-10-30 22:30:58 +00:00
Robert Müller c978ecec39 Add clear_chat command
Add a command to clear all lines of chat messages, same as the `clear_local_console`/`clear_remote_console` commands for the consoles. This is useful for example when taking screenshots or recording videos with initially empty chat.
2024-10-30 23:06:25 +01:00
Dennis Felsing df3eeffa3d
Merge pull request #9180 from KebsCS/pr-fix-tele-brush
Fix editor tele layer brush number
2024-10-30 08:13:21 +00:00
KebsCS 768384a8da
Fix editor tele layer brush number 2024-10-30 01:12:45 +01:00
Dennis Felsing f74027666a
Merge pull request #9179 from Robyt3/Client-Ghost-Validation
Improve ghost file validation
2024-10-29 21:23:43 +00:00
Robert Müller 9b4a80d8b7 Improve ghost file validation
Fix out-of-bounds reads in ghost loader when current chunk does not contain enough data for expected number of items.

Improve validation of ghost header information. Ensure strings (owner and map name) are null-terminated and valid UTF-8 to avoid crashes. Avoid duplicate code for validating header.

Remove `static` buffers for compression and decompression of ghost data, which would prevent parallelizing ghost recording and loading. Switch between the main buffer and only one temporary buffer instead of using two temporary buffers.

Automatically delete the ghost file when the recording is stopped with the number of ticks or time being invalid, i.e. when ghost recording will be restarted. This should ensure that invalid ghost files are not left behind if recording is not immediately restarted after being stopped, e.g. due to edge cases like standing inside a start-tile.

Use `int32_t` for `DiffItem` and `UndiffItem` data pointers as these functions are expected to operate on 32-bit integers. Ensure chunk and item data is aligned with `int32_t` to avoid potential unaligned accesses. Use `size_t` for size arguments.

Add assertions to ghost loader and recorder functions to ensure their correct usage. Ensure a file is not already open when opening another one and ensure that a file is open when reading/writing. Ensure that the ghost data type is valid, i.e. between `0` and `0xFF` because it is packed into an `unsigned char`. Ensure data size is valid and aligned with `int32_t`, otherwise data would be packed incorrectly by the `DiffItem` function. Improve error messages, consistent with the error messages of the demo player and recorder. Use the `log_*` functions for logging. Use the same color for all ghost-related log messages.

Avoid `system.h` include in `ghost.h` by moving `CGhostHeader` function definitions to `ghost.cpp`.

Closes #7413.
2024-10-29 21:19:07 +01:00
Dennis Felsing a52986f006
Merge pull request #9175 from Robyt3/Console-ExecutionQueue-Cleanup
Use `std::vector` for console execution queue, fix console result client ID sometimes being uninitialized
2024-10-27 10:59:07 +00:00
Robert Müller 2cd8721cde Use std::vector for console execution queue
Simplify the code by replacing the usage of a linked list and heap with an `std::vector`.

The assignment operators are replaced with copy constructors to use `std::vector`.
2024-10-27 11:02:43 +01:00
Robert Müller 3ec2564acc Fix console result client ID sometimes being uninitialized
Add the client ID as a constructor argument to `CResult` to ensure that it is initialized.
2024-10-27 11:02:43 +01:00
Dennis Felsing 12f5410bdb
Merge pull request #9174 from ChillerDragon/pr_fix_vanilla_demos
Fix vanilla demo tuning
2024-10-27 06:12:20 +00:00
ChillerDragon 386d456484 Fix vanilla demo tuning 2024-10-27 10:06:47 +08:00
Dennis Felsing cb508bdbe1
Merge pull request #9171 from heinrich5991/pr_ddnet_mastersrv_ddper
mastersrv: Add DDPer support
2024-10-26 18:22:43 +00:00
heinrich5991 9067dc5a2a
Merge pull request #9164 from Robyt3/Base-Log-Color-Macros
Add `log_*_color` macros, deprecate `IConsole::Print`
2024-10-26 15:43:35 +00:00
heinrich5991 ff87fac384 mastersrv: Add DDPer support
This allows DDPer to continue using our mastersrv while not showing up
in the DDNet client.
2024-10-26 17:19:45 +02:00
Dennis Felsing 1d04f8e641
Merge pull request #9170 from Robyt3/Windows-MoveFile-WriteThrough
Ensure file is moved when `fs_rename` function returns on Windows
2024-10-26 09:46:52 +00:00
Robert Müller 14facd5a4f Ensure file is moved when fs_rename function returns on Windows
Use the flag `MOVEFILE_WRITE_THROUGH` with the `MoveFileExW` function on Windows to ensure that the file is renamed/moved on the disk before the function returns. Otherwise, if the function returns before the file is moved, subsequent loading of the file with this name may fail sporadically.

See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw
2024-10-26 10:20:33 +02:00
Dennis Felsing 4c048f5c7b
Merge pull request #9167 from Robyt3/Client-CLineInput-Event-Condition-Cleanup
Remove redundant check of key for text input events
2024-10-25 20:27:11 +00:00
Robert Müller 565e79fa04 Remove redundant check of key for text input events
Input events with `IInput::FLAG_TEXT` never have a key, so this additional check for modifier keys is redundant.
2024-10-25 22:05:00 +02:00
Robert Müller ce8fa3fba8
Merge pull request #9159 from def-/pr-persian
Update Persian language by ArAsH
2024-10-24 21:43:36 +00:00
Dennis Felsing f121e1ddaa
Merge pull request #9166 from Robyt3/Editor-IsEntitiesLayer-Use
Use `CLayer::IsEntitiesLayer` function to avoid duplicate code
2024-10-24 21:01:50 +00:00
Robert Müller a93cba1a4f Use CLayer::IsEntitiesLayer function to avoid duplicate code 2024-10-24 22:42:39 +02:00
Dennis Felsing a7a07ad402 Update Persian language by ArAsH 2024-10-24 22:26:59 +02:00
Robert Müller e2df30a59e Add log_*_color macros, deprecate IConsole::Print
Add `log_error_color`, `log_warn_color`, `log_info_color`, `log_debug_color` and `log_trace_color` macros to wrap `log_log_color` function with specific log levels.

Add comment to mark the `IConsole::Print` function as deprecated in favor of the `log_*` functions, which should be preferred for the following reasons:

- They support `printf`-formatting without a separate buffer.
- They support all five log levels.
- They do not require a pointer to `IConsole` to be used.
- Consistency of logging code.
2024-10-23 17:52:54 +02:00
Emir Marincic 5a716ae463
Merge pull request #9163 from Robyt3/Console-Format-Removal
Remove obsolete `IConsole::Format` function, fix `show_ips` output
2024-10-21 22:21:16 +00:00
Robert Müller 66c5609ae4 Remove obsolete IConsole::Format function, fix show_ips output
The `IConsole::Format` function was previously used to format the log and rcon output, but it has become obsolete when the logging was refactored.

The function was still being used to format the output of the `show_ips` command in the rcon, which was causing this particular rcon line to have the old log format (e.g. `[12:34:56][server]: Value: 0`). Now, `IConsole::Print` is used directly, as this causes the output to be sent with the correct format and only to the rcon command user.

The `IConsole::FPrintCallback` function pointer typedef is also obsolete and unused.
2024-10-21 23:35:30 +02:00
Dennis Felsing 74fa79b489
Merge pull request #9162 from Robyt3/Client-TextureHandle-Cleanup
Avoid redundant overwriting of texture handles after unloading
2024-10-21 21:07:49 +00:00
Dennis Felsing 78f5013d93
Merge pull request #9161 from KebsCS/pr-regional-top5team
Fix top5team sql
2024-10-21 21:07:46 +00:00
KebsCS 97af0168eb
Fix top5team sql 2024-10-21 22:14:08 +02:00
Robert Müller 4314608dfb Avoid redundant overwriting of texture handles after unloading
It's not necessary to create a new `CTextureHandle` object after unloading the handle and it's also not necessary to check if the texture handle is valid when unloading it.
2024-10-21 22:07:13 +02:00
Robert Müller d923e58452
Merge pull request #9158 from Jupeyy/pr_unblock_less
Don't block drivers before & equal to 2.0.137.
2024-10-20 09:33:43 +00:00
Jupeyy efc1d2d4d1
Don't block drivers before 2.0.137.
Reported by meep on discord. He apparently uses a driver from around 2020 that also worked
without problems.
2024-10-20 11:14:28 +02:00
Dennis Felsing baddafefa0
Merge pull request #9157 from Robyt3/Client-Settings-Skin-Reveal
Scroll to selected skin when entering name and switching tabs
2024-10-19 21:43:53 +00:00
Robert Müller 9e7ba82507 Scroll to selected skin when entering name and switching tabs
Scroll to reveal the selected skin when entering a skin name and when switching between the player and dummy tabs.
2024-10-19 20:50:10 +02:00
Dennis Felsing 1bd0f4330f
Merge pull request #9155 from Robyt3/Client-LastRaceTick-Cleanup
Move last race tick and current race time handling to `CGameClient`
2024-10-18 20:38:55 +00:00
Robert Müller b724ccc6a6 Move last race tick and current race time handling to CGameClient
Store and update the last race tick directly in `CGameClient` instead of in `CGhost`, as the value is also used in the gameclient.

Move current race time calculation from `CClient` to `CGameClient`, as this function is not used in the engine and this allows removing unnecessary virtual functions.

Closes #1720.
2024-10-18 22:10:58 +02:00
Dennis Felsing 74b485b397
Merge pull request #9153 from Robyt3/Editor-Curvetype-Popup
Add popup to select envelope curve type, support shift+left click
2024-10-17 07:15:53 +00:00
Robert Müller 9a1bd192c4 Add popup to select envelope curve type, support shift+left click
Show popup to select the envelope curve type from a list when right clicking the curve type button, as selecting the curve type by pressing the button multiple times is inconvenient.

Switch to previous curve type on shift+left clicking the curve type button.
2024-10-16 22:31:02 +02:00
Dennis Felsing 3e9ca3314f
Merge pull request #9152 from Robyt3/CLayers-Refactoring
Minor refactoring of `CLayers`
2024-10-15 20:56:55 +00:00
Robert Müller 7ac0a8f6c6 Reduce indent, avoid C style casts in CLayers::InitTilemapSkip 2024-10-15 21:11:31 +02:00
Robert Müller e0461f4c21 Combine CLayers::Init and CLayers::InitBackground functions
The `CLayers::InitBackground` function implemented a subset of the `CLayers::Init` function, only loading the game layers and not the other entities layers, so a Boolean parameter can be used to reduce the duplicate code.
2024-10-15 21:11:31 +02:00
Robert Müller b991a44b40 Rename variables l/g to LayerIndex/GroupIndex 2024-10-15 21:11:24 +02:00
heinrich5991 8290db97b8
Merge pull request #9142 from ChillerDragon/pr_07_fav_urls
Store 0.7 favorites in url format
2024-10-14 15:38:54 +00:00
heinrich5991 c829639d65
Merge pull request #9123 from Robyt3/Client-Skin-Download-Refactoring
Improve skin downloading: load from `downloadedskins` if possible
2024-10-14 15:37:35 +00:00
Dennis Felsing 0f074aa773 Fix top5team sql 2024-10-14 17:33:23 +02:00
Dennis Felsing e21915074c
Merge pull request #9149 from dobrykafe/pr-nameplates-preview
Add nameplates preview
2024-10-13 21:44:37 +00:00
Dennis Felsing de7cd8571f
Merge pull request #9148 from Robyt3/Demo-Envelope-Update-Cleanup
Cleanup envelope updating during demo playback
2024-10-13 21:25:16 +00:00
Dennis Felsing c3e627e443 Fix clang-tidy 2024-10-13 23:24:31 +02:00
dobrykafe f3473c7f73 add nameplates preview 2024-10-13 19:33:36 +02:00
Robert Müller e725432a7f Cleanup envelope updating during demo playback
The additional code to evaluate envelopes during demo playback is obsolete, as client ticks are already properly synchronized with the demo playback.

From teeworlds/teeworlds#2750 and teeworlds/teeworlds#2768.
2024-10-13 17:45:38 +02:00
Emir Marincic 2788f12634
Merge pull request #9146 from Robyt3/Http-Wait-Condition-Lock
Ensure lock is held when modifying condition of condition variable
2024-10-13 13:32:23 +00:00
Robert Müller 21e0cdd0bc Ensure lock is held when modifying condition of condition variable
See #9145.
2024-10-13 15:09:51 +02:00
Dennis Felsing a397688c38
Merge pull request #9145 from Robyt3/Http-Request-Wait-Condition
Avoid busy waiting in `CHttpRequest::Wait` function
2024-10-13 11:08:43 +00:00
Robert Müller ad8349b7e9 Avoid busy waiting in CHttpRequest::Wait function
Use a condition variable instead of busy waiting until HTTP requests are done.

Also set the state `EHttpState::RUNNING` which was previously unused.

Closes #7811.
2024-10-13 12:48:24 +02:00
Dennis Felsing 34015ea744
Merge pull request #9143 from KebsCS/pr-command-argument-validation
Fix toggle arg validation
2024-10-12 20:46:09 +00:00
KebsCS cd96eea8ce
Fix toggle arg validation 2024-10-12 21:02:17 +02:00
ChillerDragon 0c797893d5 Store 0.7 favorites in url format 2024-10-12 19:19:43 +09:00
Dennis Felsing 08ec4dd7cf
Merge pull request #9141 from Robyt3/Android-Build-x64-Fix
Fix Android build when first building `x64` architecture
2024-10-12 09:06:00 +00:00
Robert Müller 1828b5b29b Fix Android build when first building x64 architecture
The argument `x64` is an alias for `x86_64` but the name of the build folder always contains `x86_64`, so the `data` folder and certificate were not being found when using the `x64` argument, unless another architecture was previously built.
2024-10-12 10:07:14 +02:00
ChillerDragon bb53b9eae0 Revert "Fix 0.7 server favorites"
This reverts commit 4c57f2d9f8.
2024-10-12 15:13:14 +09:00
Dennis Felsing 0d76b482ee
Merge pull request #9140 from Robyt3/Server-Demos-Filename-Fix
Fix server-side demos with maps in folders, make `CServer::GetMapName` function more efficient by caching
2024-10-11 20:23:53 +00:00
Robert Müller ab60d0bf70 Fix server-side demos with maps in folders
When maps are loaded from folders on the server, the same folders were used for demos but recording would usually fail due to the folders not existing in the demos folder.

Furthermore, the map name being written in the demo header also included the folder names, which causes the client to not find the map unless it also exists at that location.

Closes #9033.
2024-10-11 21:36:32 +02:00
Robert Müller e24b87adbd Make CServer::GetMapName function more efficient by caching
Store the current map filename (without path) separately when loading a new map instead of determining it again each time that the `CServer::GetMapName` function is called.

Use the `fs_filename` function for this.

Avoid the usage of the `sv_map` config variable for this, which may have caused the returned map filename to be out-of-sync with the real map on the server due to the map specified by the config variable not being reloaded immediately.
2024-10-11 21:35:36 +02:00
Dennis Felsing 37761b6d1b
Merge pull request #9137 from KebsCS/pr-flags
Add Antarctica flag, update some existing flags
2024-10-11 12:49:42 +00:00
KebsCS 3c9d92a527
Add Antarctica flag, update some existing flags 2024-10-11 14:29:59 +02:00
Dennis Felsing 52d860a823
Merge pull request #9135 from furo321/no-delay-rescue
Don't check `sv_rescue_delay` in practice
2024-10-11 06:31:41 +00:00
Dennis Felsing e47bf55051
Merge pull request #9052 from ChillerDragon/pr_fav7
Fix 0.7 server favorites
2024-10-11 06:31:28 +00:00
Dennis Felsing 8cee3a6275
Merge pull request #8889 from KebsCS/pr-regional-top5team
Add regional rankings to /top5team
2024-10-11 06:26:38 +00:00
Dennis Felsing e4a3631bf4
Merge pull request #9138 from furo321/tiles-hookcoll-preview
Render hookable and unhookable tiles for hook collision preview
2024-10-11 06:26:16 +00:00
Dennis Felsing 5abd96c1d8
Merge pull request #9139 from Robyt3/Client-CTeeRenderInfo-Usage-Cleanup
Refactor `CTeeRenderInfo` usage
2024-10-11 06:25:39 +00:00
furo 9fdb246724 Render hookable and unhookable tiles for hook collision preview 2024-10-11 01:09:38 +02:00
KebsCS 279b14cc44
Add regional rankings to /top5team 2024-10-11 00:09:32 +02:00
Robert Müller 3f829b4ac2 Refactor CTeeRenderInfo usage
- Add `CTeeRenderInfo::ApplyColors` function to reduce duplicate code.
- Use `CTeeRenderInfo::Apply` function in more cases.
- Use `CSkins::Find` function instead of implementing default skin handling manually with the `FindOrNullptr` function for chat settings preview.
- Remove redundant initialization of `CTeeRenderInfo::m_CustomColoredSkin` member.
- Replace empty client skin with `default` instead of checking for empty skin names later.
- Remove unnecessary check for empty skin name for ghost rendering. The `CSkins::Find` function will return the default skin for an empty skin name, which is more correct than invalidating the ghost skin render info.
2024-10-10 23:29:38 +02:00
Robert Müller 85b836723a
Merge pull request #9136 from KebsCS/pr-hook-coll-preview
Add hook collisions preview
2024-10-10 20:43:26 +00:00
KebsCS bfe2e4dc80
Add hook collisions preview 2024-10-10 22:23:03 +02:00
furo a4bb1ec0dc Don't check sv_rescue_delay in practice 2024-10-10 15:39:47 +02:00
Robert Müller 181b6d8b9c Improve skin downloading: load from downloadedskins if possible
Previously, skins not found in the `skins` folder were downloaded from the configured URL and saved to the `downloadedskins` folder, but the saved files were never used, leading to wasted downloads and write operations.

Now, if a skin to be downloaded already exists in the `downloadedskins` folder, the modified time of the skin file is determined and included in the HTTP GET request as `If-Modified-Since` header. If the file was not modified on the server since that time, the server will answer with status 304 Not Modified and an empty response body, in which case the existing skin file is loaded. If the skin file was modified, then it will be downloaded like usual. The download will also be retried without including the `If-Modified-Since` header, if the local file is more recent but it failed to be loaded. If a skin could not be downloaded when it should but a file already exists in the `downloadedskins` folder then that file will be loaded instead.

Instead of checking whether the number of skins is different to determine whether to reload the list of skins in the settings menus, the last time that the list of skins was refreshed is now saved and compared, which is more robust. The additional check to refresh the skin list while skins are being downloaded is unnecessary, as any updates to the skin list are now covered by checking the last refresh time.
2024-10-09 22:56:44 +02:00
Robert Müller 0bb829b7b8 Add IStorage::RetrieveTimes function as fs_file_time wrapper 2024-10-06 22:11:13 +02:00
ChillerDragon 4c57f2d9f8 Fix 0.7 server favorites 2024-09-27 18:53:08 +08:00
dobrykafe f4b2f49056 remove unnecessary dilate config 2024-09-24 05:06:04 +02:00
111 changed files with 2052 additions and 1399 deletions

View file

@ -1146,6 +1146,7 @@ set(EXPECTED_DATA
countryflags/AL.png
countryflags/AM.png
countryflags/AO.png
countryflags/AQ.png
countryflags/AR.png
countryflags/AS.png
countryflags/AT.png

BIN
data/countryflags/AQ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -57,8 +57,8 @@ AO
AI
== 660
#AQ
#== 10
AQ
== 10
AG
== 28

View file

@ -1125,6 +1125,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -1939,3 +1939,6 @@ Feet
[skins]
Eyes
== Göz
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -1876,6 +1876,9 @@ Connect address error
Could not connect dummy
==
Some fonts could not be loaded. Check the local console for details.
==
Save skin
==

View file

@ -987,6 +987,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -1970,3 +1970,6 @@ Feet
[skins]
Eyes
== Olhos
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -591,6 +591,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Loading DDNet Client
==

View file

@ -1227,6 +1227,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -594,6 +594,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Loading DDNet Client
==

View file

@ -1943,3 +1943,6 @@ Feet
[skins]
Eyes
== Oči
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -1136,6 +1136,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -1273,6 +1273,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -610,6 +610,9 @@ Preparing demo playback
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -1872,6 +1872,9 @@ Connect address error
Could not connect dummy
==
Some fonts could not be loaded. Check the local console for details.
==
Save skin
==

View file

@ -1832,6 +1832,9 @@ Connect address error
Could not connect dummy
==
Some fonts could not be loaded. Check the local console for details.
==
[Spectating]
Following %s
==

View file

@ -1874,6 +1874,9 @@ Connect address error
Could not connect dummy
==
Some fonts could not be loaded. Check the local console for details.
==
[Spectating]
Following %s
==

View file

@ -1586,6 +1586,9 @@ Error saving settings
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Quitting. Please wait…
==

View file

@ -1956,3 +1956,6 @@ Eyes
https://wiki.ddnet.org/wiki/Mapping
== https://wiki.ddnet.org/wiki/Mapping
Some fonts could not be loaded. Check the local console for details.
== Einige Schriften konnten nicht geladen werden. Prüf die lokale Konsole für Details.

View file

@ -600,6 +600,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Loading DDNet Client
==

View file

@ -1566,6 +1566,9 @@ The width of texture %s is not divisible by %d, or the height is not divisible b
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Quitting. Please wait…
==

View file

@ -1435,6 +1435,9 @@ The format of texture %s is not RGBA which will cause visual bugs.
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Why are you slowmo replaying to read this?
==

View file

@ -1168,6 +1168,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -1578,6 +1578,9 @@ The width of texture %s is not divisible by %d, or the height is not divisible b
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Quitting. Please wait…
==

View file

@ -591,6 +591,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Loading DDNet Client
==

View file

@ -1137,6 +1137,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -1,8 +1,9 @@
##### authors #####
##### authors #####
# OneShadow 2016-03-27
# Quick 2020-07-12
# younesdevil 2021-12-07
# Player 2023-01-26
# ArAsH 2024-10-21
##### /authors #####
##### translated strings #####
@ -329,7 +330,7 @@ Browser
== ﻮﺠﺘﺴﺟ
Ghost
== ﺡﻭﺭ ﺖﻟﺎﺣ
== ﺡﻭﺭ ﺖﻟﺎﺣ
Loading DDNet Client
== ﺖﻨﯾﺪﯾﺩ ﺖﻨﯾﻼﮐ ﻥﺩﺮﮐ ﻩﺩﺎﻣﺁ ﻝﺎﺣﺭﺩ
@ -960,7 +961,7 @@ Speed up the demo
== ﻮﻣﺩ ﺖﻋﺮﺳ ﻥﺩﺮﮐ ﺩﺎﯾﺯ
Export cut as a separate demo
== ﻪﻧﺎﮔﺍﺪﺟ ﻮﻣﺩ ﻥﺍﻮﻨﻋ ﻪﺑ ﺵﺮﺑ ﻥﺩﺮﮐ ﺝﺭﺎﺧ
== ﻪﻧﺎﮔﺍﺪﺟ ﻮﻣﺩ ﻥﺍﻮﻨﻋ ﻪﺑ ﺵﺮﺑ ﻥﺩﺮﮐ ﺝﺭﺎﺧ
Go back one marker
== ﺩﺮﮔﺮﺑ ﺐﻘﻋ ﻪﺑ ﺮﮕﻧﺎﺸﻧ ﮏﯾ
@ -1082,7 +1083,7 @@ Absolute
== ﻖﻠﻄﻣ [ﯼﺯﺎﺑ ﺮﻟﺮﺘﻨﮐ ﺖﻟﺎﺣ]
Ingame controller mode
== [ﯼﺯﺎﺑ ﺮﻟﺮﺘﻨﮐ ﺖﻟﺎﺣ]
== ﯼﺯﺎﺑ ﺮﻟﺮﺘﻨﮐ ﺖﻟﺎﺣ
Ingame controller sens.
== ﯼﺯﺎﺑ ﺮﻟﺮﺘﻨﮐ ﺮﮕﺴﺣ
@ -1115,7 +1116,7 @@ Controller
== ﺮﻟﺮﺘﻨﮐ
Reset controls
== ﻝﺮﺘﻨﮐ ﻥﺩﺮﮐ ﺖﺴﯾﺭ
== ﻝﺮﺘﻨﮐ ﻥﺩﺮﮐ ﺖﺴﯾﺭ
Are you sure that you want to reset the controls to their defaults?
== ؟ﺪﯿﻧﺍﺩﺮﮔﺯﺎﺑ ﺽﺮﻓ ﺶﯿﭘ ﺖﻟﺎﺣ ﻪﺑ ﺍﺭ ﻝﺮﺘﻨﮐ ﺪﯿﻫﺍﻮﺧ ﯽﻣ ﻪﮐ ﺪﯿﺘﺴﻫ ﻦﺌﻤﻄﻣ ﺎﯾﺁ
@ -1503,436 +1504,440 @@ Best
[Graphics error]
Could not initialize the given graphics backend, reverting to the default backend now.
==
== .ﺩﻮﺷﯽﻣ ﻩﺩﺍﺩ ﺖﺸﮔﺯﺎﺑ ﺽﺮﻓﺶﯿﭘ ﻂﺑﺍﺭ ﻪﺑ ﻥﻮﻨﻛﺍ ،ﺩﺮﻛ ﻩﺩﺎﻔﺘﺳﺍ ﻩﺪﺷ ﻩﺩﺍﺩ ﯽﻜﯿﻓﺍﺮﮔ ﻂﺑﺍﺭ ﺯﺍ ﻥﺍﻮﺗﯽﻤﻧ
[Graphics error]
Could not initialize the given graphics backend, this is probably because you didn't install the driver of the integrated graphics card.
==
== .ﺪﯾﺍ ﻩﺩﺮﻜﻧ ﺐﺼﻧ ﺍﺭ ﻚﯿﻓﺍﺮﮔ ﺕﺭﺎﻛ ﺭﻮﯾﺍﺭﺩ ﻪﻛ ﺖﺳﺍ ﻞﯿﻟﺩ ﻦﯾﺍ ﻪﺑ ًﻻﺎﻤﺘﺣﺍ ،ﺩﺮﻛ ﻪﯿﻟﻭﺍ ﯽﻫﺩﺭﺍﺪﻘﻣ ﺍﺭ ﻩﺪﺷ ﻩﺩﺍﺩ ﻚﯿﻓﺍﺮﮔ ﺪﻧﺍ ﻚﺑ ﻥﺍﻮﺗ ﯽﻤﻧ
Could not resolve connect address '%s'. See local console for details.
==
== .ﺪﯿﻨﻛ ﻪﻌﺟﺍﺮﻣ ﯽﻠﺤﻣ ﻝﻮﺴﻨﻛ ﻪﺑ ﺮﺘﺸﯿﺑ ﺕﺎﯿﺋﺰﺟ ىﺍﺮﺑ .ﺩﻮﺒﻧ ﻦﻜﻤﻣ '%s' ﺱﺭﺩﺁ ﻪﺑ ﻝﺎﺼﺗﺍ
Connect address error
==
== ﻝﺎﺼﺗﺍ ﺱﺭﺩﺁ ىﺎﻄﺧ
Could not save downloaded map. Try manually deleting this file: %s
==
== %s :ﺪﯿﻨﻛ ﻑﺬﺣ ﺍﺭ ﻞﯾﺎﻓ ﻦﯾﺍ ﯽﺘﺳﺩ ﺕﺭﻮﺻ ﻪﺑ ﺪﯿﻨﻛ ﯽﻌﺳ .ﺪﺸﻧ ﻩﺮﯿﺧﺫ ﻩﺪﺷ ﺩﻮﻠﻧﺍﺩ ﭗﻣ
Could not connect dummy
==
== ﺪﺸﻧ ﻞﺼﺘﻣ ﯽﻣﺍﺩ
Error playing demo
==
== ﻮﻣﺩ ﻥﺩﺮﻛ ﺍﺮﺟﺍ ﺭﺩ ﺎﻄﺧ
Failed saving the replay!
==
== !ﺩﻮﺑ ﻖﻓﻮﻣﺎﻧ ﺩﺪﺠﻣ ﺶﺨﭘ ﻥﺩﺮﻛ ﻩﺮﯿﺧﺫ
Saving settings to '%s' failed
==
== ﺪﺸﻧ ﻡﺎﺠﻧﺍ "%s" ﺭﺩ ﺕﺎﻤﯿﻈﻨﺗ ﻥﺩﺮﻛ ﻩﺮﯿﺧﺫ
Error saving settings
==
== ﺕﺎﻤﯿﻈﻨﺗ ﻥﺩﺮﻛ ﻩﺮﯿﺧﺫ ﺭﺩ ﺎﻄﺧ
The format of texture %s is not RGBA which will cause visual bugs.
== .ﺩﻮﺷ ﯽﻣ ىﺮﻫﺎﻇ ﻝﺎﻜﺷﺍ ﺩﺎﺠﯾﺍ ﺚﻋﺎﺑ ﻪﻛ ﺖﺴﯿﻧ %s RGBA ﯽﻜﯿﻓﺍﺮﮔ ﺖﻟﺎﺣ ﺖﻣﺮﻓ
Loading demo file from storage
== ﻪﻈﻓﺎﺣ ﺯﺍ ﻮﻣﺩ ﻞﯾﺎﻓ ىﺮﯿﮔﺭﺎﺑ ﻝﺎﺣ ﺭﺩ
Initializing map logic
== ﭗﻣ ﻖﻄﻨﻣ ىﺯﺍﺪﻧﺍ ﻩﺍﺭ
Quitting. Please wait…
== …ﺪﯿﻨﻛﺮﺒﺻ ﺎﻔﻄﻟ .ﻥﺪﺷ ﺝﺭﺎﺧ ﻝﺎﺣ ﺭﺩ
Restarting. Please wait…
== ﺪﯿﻨﻛ ﺮﺒﺻ ﺎﻔﻄﻟ .ﺩﺪﺠﻣ ىﺯﺍﺪﻧﺍ ﻩﺍﺭ ﻝﺎﺣ ﺭﺩ
Searching
== ﻮﺟ ﻭ ﺖﺴﺟ ﻝﺎﺣ ﺭﺩ
Enter Username
== ﺪﯿﻨﻛ ﺩﺭﺍﻭ ﺍﺭ ىﺮﺑﺭﺎﻛ ﻡﺎﻧ
Enter Password
== ﺪﯿﻨﻛ ﺩﺭﺍﻭ ﺍﺭ ﺭﻮﺒﻋﺰﻣﺭ
NOT CONNECTED
== ﺪﺸﻧ ﻞﺼﺘﻣ
Match %d of %d
== %d ﺯﺍ %d ﻪﻘﺑﺎﺴﻣ
No results
== ﻪﺠﯿﺘﻧ ﻥﻭﺪﺑ
Lines %d - %d (%s)
== %d - %d (%s) ﻁﻮﻄﺧ
Locked
== ﺪﺷ ﻞﻔﻗ
Following
== ﻥﺩﺮﻛ ﻝﺎﺒﻧﺩ
Loading commands…
== …ﺕﺍﺭﻮﺘﺳﺩ ىﺮﯿﮔﺭﺎﺑ ﻝﺎﺣ ﺭﺩ
Multi-View
== ﯽﻧﺎﮕﻤﻫ-ﺪﯾﺩ
[Spectating]
Following %s
== %s ﻥﺩﺮﻛ ﻝﺎﺒﻧﺩ
Some map images could not be loaded. Check the local console for details.
== .ﺪﯿﻨﻛ ﯽﺳﺭﺮﺑ ﺍﺭ ﯽﻠﺤﻣ ﻝﻮﺴﻨﻛ ،ﺕﺎﯿﺋﺰﺟ ىﺍﺮﺑ .ﺪﻧﺪﺸﻧ ىﺮﯿﮔﺭﺎﺑ ﭗﻣ ﺮﯾﻭﺎﺼﺗ ﺯﺍ ﯽﺧﺮ
Some map sounds could not be loaded. Check the local console for details.
== .ﺪﯿﻨﻛ ﯽﺳﺭﺮﺑ ﺍﺭ ﯽﻠﺤﻣ ﻝﻮﺴﻨﻛ ،ﺕﺎﯿﺋﺰﺟ ىﺍﺮﺑ .ﺪﻧﺪﺸﻧ ىﺮﯿﮔﺭﺎﺑ ﭗﻣ ىﺎﻫﺍﺪﺻ ﺯﺍ ﯽﺧﺮﺑ
Loading menu themes
== ﻮﻨﻣ ىﺎﻫ ﻢﺗ ىﺮﯿﮔﺭﺎﺑ ﻝﺎﺣ ﺭﺩ
Press a key…
== …ﺪﯿﻫﺩ ﺭﺎﺸﻓ ﺍﺭ ﺪﯿﻠﻛ ﻚﯾ
Main menu
== ﯽﻠﺻﺍ ىﻮﻨﻣ
Rename folder
== ﻪﺷﻮﭘ ﻡﺎﻧ ﺮﯿﯿﻐﺗ
Render complete
== ﺪﺷ ﻞﻣﺎﻛ ﺭﺪﻧﺭ
Are you sure that you want to restart?
== ؟ﺪﯿﻨﻛ ىﺯﺍﺪﻧﺍ ﻩﺍﺭ ﻩﺭﺎﺑﻭﺩ ﺪﯿﻫﺍﻮﺧ ﯽﻣ ﻪﻛ ﺪﯿﺘﺴﻫ ﻦﺌﻤﻄﻣ ﺎﯾﺁ
Save skin
== ﻦﯿﻜﺳﺍ ﻩﺮﯿﺧﺫ
Are you sure you want to save your skin? If a skin with this name already exists, it will be replaced.
== ﺯﺍ ﻡﺎﻧ ﻦﯾﺍ ﺎﺑ ﯽﻨﯿﻜﺳﺍ ﺮﮔﺍ ؟ﺪﯿﻨﻛ ﻩﺮﯿﺧﺫ ﺍﺭ ﺩﻮﺧ ﻦﯿﻜﺳﺍ ﺪﯿﻫﺍﻮﺧ ﯽﻣ ﻪﻛ ﺪﯿﺘﺴﻫ ﻦﺌﻤﻄﻣ ﺎﯾﺁ .ﺪﺷ ﺪﻫﺍﻮﺧ ﻦﯾﺰﮕﯾﺎﺟ ،ﺪﺷﺎﺑ ﻪﺘﺷﺍﺩ ﺩﻮﺟﻭ ﻞﺒﻗ
There's an unsaved map in the editor, you might want to save it.
== .ﺪﯿﻨﻛ ﻩﺮﯿﺧﺫ ﺍﺭ ﻥﺁ ﺪﯿﻫﺍﻮﺨﺑ ﺖﺳﺍ ﻦﻜﻤﻣ ،ﺩﺭﺍﺩ ﺩﻮﺟﻭ ﺭﻮﺘﯾﺩﺍ ﺭﺩ ﻩﺪﺸﻧ ﻩﺮﯿﺧﺫ ﭗﻣ ﻚﯾ
Continue anyway?
== ؟ﺪﯿﻫﺩ ﻪﻣﺍﺩﺍ ﻝﺎﺣ ﺮﻫ ﻪﺑ
A demo with this name already exists
== ﺩﺭﺍﺩ ﺩﻮﺟﻭ ﻞﺒﻗ ﺯﺍ ﻡﺎﻧ ﻦﯾﺍ ﺎﺑ ﻮﻣﺩ ﻚﯾ
A folder with this name already exists
== ﺩﺭﺍﺩ ﺩﻮﺟﻭ ﻞﺒﻗ ﺯﺍ ﻡﺎﻧ ﻦﯾﺍ ﺎﺑ ﻪﺷﻮﭘ ﻚﯾ
Unable to rename the folder
== ﺖﺴﯿﻧ ﻦﻜﻤﻣ ﻪﺷﻮﭘ ﻡﺎﻧ ﺮﯿﯿﻐﺗ
(paused)
== (ﻩﺪﺷ ﺚﻜﻣ)
Videos directory
== ﻮﯾﺪﯾﻭ ﺖﺳﺮﻬﻓ
Video was saved to '%s'
== ﺪﺷ ﻩﺮﯿﺧﺫ "%s" ﺭﺩ ﻮﺋﺪﯾﻭ
Unable to save the skin
== ﺩﺭﺍﺪﻧ ﺩﻮﺟﻭ ﻦﯿﻜﺳﺍ ﻩﺮﯿﺧﺫ ﻥﺎﻜﻣﺍ
Unable to save the skin with a reserved name
== ﺩﺭﺍﺪﻧ ﺩﻮﺟﻭ ﻩﺪﺷ ﻭﺭﺯﺭ ﻡﺎﻧ ﺎﺑ ﻦﯿﻜﺳﺍ ﻩﺮﯿﺧﺫ ﻥﺎﻜﻣﺍ
%d/%d KiB (%.1f KiB/s)
== %d/%d KiB (%.1f KiB/s)
No local servers found (ports %d-%d)
== (%d-%d ﺕﺭﻮﭘ) ﺪﺸﻧ ﺖﻓﺎﯾ ﯽﻠﺤﻣ ﺭﻭﺮﺳ ﭻﯿﻫ
Example of usage
== ﺩﺮﺑﺭﺎﻛ ﻪﻧﻮﻤﻧ
No login required
== ﺪﺷﺎﺑ ﯽﻤﻧ ﺩﻭﺭﻭ ﻪﺑ ﺯﺎﯿﻧ
Communities
== ﻊﻣﺍﻮﺟ
Copy info
== ﺕﺎﻋﻼﻃﺍ ﯽﭙﻛ
No server selected
== ﺖﺳﺍ ﻩﺪﺸﻧ ﺏﺎﺨﺘﻧﺍ ىﺭﻭﺮﺳ ﭻﯿﻫ
Online players (%d)
== (%d) ﻦﯾﻼﻧﺁ ﻥﺎﻨﻜﯾﺯﺎﺑ
Online clanmates (%d)
== (%d) ﻦﯾﻼﻧﺁ ىﺎﻫ ﯽﻨﻠﻛ ﻢﻫ
[friends (server browser)]
Offline (%d)
== ﻥﺎﺘﺳﻭﺩ] (%d) ﻦﯾﻼﻓﺁ
Click to select server. Double click to join your friend.
== .ﺪﯿﻨﻛ ﻚﯿﻠﻛ ﺭﺎﺑﻭﺩ ﺩﻮﺧ ﺖﺳﻭﺩ ﻪﺑ ﻦﺘﺳﻮﯿﭘ ىﺍﺮﺑ .ﺪﯿﻨﻛ ﻚﯿﻠﻛ ﺭﻭﺮﺳ ﺏﺎﺨﺘﻧﺍ ىﺍﺮﺑ
Click to remove this player from your friends list.
== .ﺪﯿﻨﻛ ﻚﯿﻠﻛ ﺩﻮﺧ ﻥﺎﺘﺳﻭﺩ ﺖﺴﯿﻟ ﺯﺍ ﻦﻜﯾﺯﺎﺑ ﻦﯾﺍ ﻑﺬﺣ ىﺍﺮﺑ
Click to remove this clan from your friends list.
== .ﺪﯿﻨﻛ ﻚﯿﻠﻛ ﺩﻮﺧ ﻥﺎﺘﺳﻭﺩ ﺖﺴﯿﻟ ﺯﺍ ﻦﻠﻛ ﻦﯾﺍ ﻑﺬﺣ ىﺍﺮﺑ
None
== ﻡﺍﺪﻛ ﭻﯿﻫ
Add Clan
== ﻦﻠﻛ ﻥﺩﺮﻛ ﻪﻓﺎﺿﺍ
Server filter
== ﺭﻭﺮﺳ ﺮﺘﻠﯿﻓ
Friends
== ﻥﺎﺘﺳﻭﺩ
Go back the specified duration
== ﺪﯿﻧﺍﺩﺮﮔﺮﺑ ﺐﻘﻋ ﻪﺑ ﺍﺭ ﻩﺪﺷ ﺺﺨﺸﻣ ﻥﺎﻣﺯ ﺕﺪﻣ
[Demo player duration]
%d min.
== ﻪﻘﯿﻗﺩ %d
[Demo player duration]
%d sec.
== ﻪﯿﻧﺎﺛ %d
Change the skip duration
== ﻩﺪﺷ ﭗﯿﻜﺳﺍ ﻥﺎﻣﺯ ﺕﺪﻣ ﺮﯿﯿﻐﺗ
Go forward the specified duration
== ﻩﺪﺷ ﺺﺨﺸﻣ ﻥﺎﻣﺯ ﺕﺪﻣ ﻥﺩﺮﺑ ﻮﻠﺟ
Mark the beginning of a cut (right click to reset)
== (ﺪﯿﻨﻛ ﺖﺳﺍﺭ ﻚﯿﻠﻛ ﺩﺪﺠﻣ ﻢﯿﻈﻨﺗ ىﺍﺮﺑ) ﺪﯿﻧﺰﺑ ﺖﻣﻼﻋ ﺍﺭ ﺵﺮﺑ ﻚﯾ ﻉﻭﺮﺷ
Mark the end of a cut (right click to reset)
== (ﺪﯿﻨﻛ ﺖﺳﺍﺭ ﻚﯿﻠﻛ ﺩﺪﺠﻣ ﻢﯿﻈﻨﺗ ىﺍﺮﺑ) ﺪﯿﻧﺰﺑ ﺖﻣﻼﻋ ﺍﺭ ﺵﺮﺑ ﻚﯾ ىﺎﻬﺘﻧﺍ
Close the demo player
== ﺪﯾﺪﻨﺒﺑ ﺍﺭ ﻮﻣﺩ ﻩﺪﻨﻨﻛ ﺶﺨﭘ
Export demo cut
== ﻩﺪﺷ ﺵﺮﺑ ﻮﻣﺩ ﻥﺩﺮﻛ ﺭﺩﺎﺻ
Cut interval
== ﺵﺮﺑ ﻪﻠﺻﺎﻓ
Cut length
== ﺵﺮﺑ ﻝﻮﻃ
Render cut to video
== ﻮﯾﺪﯾﻭ ﻪﺑ ﺵﺮﺑ ﺭﺪﻧﺭ
Please use a different filename
== ﺪﯿﻨﻛ ﻩﺩﺎﻔﺘﺳﺍ ىﺮﮕﯾﺩ ﻞﯾﺎﻓ ﻡﺎﻧ ﺯﺍ ﺎﻔﻄﻟ
All combined
== ﺪﯿﻨﻛ ﻩﺩﺎﻔﺘﺳﺍ ىﺮﮕﯾﺩ ﻞﯾﺎﻓ ﻡﺎﻧ ﺯﺍ ﺎﻔﻄﻟ
No demo selected
== ﺖﺳﺍ ﻩﺪﺸﻧ ﺏﺎﺨﺘﻧﺍ ىﺍ ﻮﻣﺩ ﭻﯿﻫ
Folder Link
== ﻪﺷﻮﭘ ﻚﻨﯿﻟ
Created
== ﺪﺷ ﺩﺎﺠﯾﺍ
Netversion
== ﺖﻧ ﻪﺨﺴﻧ
[Demo details]
map not included
== ﺖﺳﺍ ﻩﺪﺸﻧ ﻩﺪﻧﺎﺠﻨﮔ ﭗﻣ
Are you sure that you want to delete the folder '%s'?
== ؟ﺪﯿﻨﻛ ﻑﺬﺣ ﺍﺭ '%s' ﻪﺷﻮﭘ ﺪﯿﻫﺍﻮﺧ ﯽﻣ ﻪﻛ ﺪﯿﺘﺴﻫ ﻦﺌﻤﻄﻣ ﺎﯾﺁ
Delete folder
== ﻪﺷﻮﭘ ﻑﺬﺣ
Unable to delete the folder '%s'. Make sure it's empty first.
== .ﺖﺳﺍ ﯽﻟﺎﺧ ﻪﻛ ﺪﯾﻮﺷ ﻦﺌﻤﻄﻣ ﺍﺪﺘﺑﺍ .ﺖﺴﯿﻧ ﻦﻜﻤﻣ "%s" ﻪﺷﻮﭘ ﻑﺬﺣ
Dummy is not allowed on this server
== ﺖﺴﯿﻧ ﺯﺎﺠﻣ ﺭﻭﺮﺳ ﻦﯾﺍ ﺭﺩ ﯽﻣﺍﺩ
Please wait…
== ﺪﯿﻨﻛ ﺮﺒﺻ ﺎﻔﻄﻟ…
Loading…
== ىﺮﯿﮔﺭﺎﺑ ﻝﺎﺣ ﺭﺩ…
Ghosts directory
== ﯽﻠﺒﻗ ىﺎﻫ ﻥﺍﺭ ﺖﺴﯿﻟ ىﺭﻮﺘﻛﺮﯾﺍﺩﯽﻠﺒﻗ ىﺎﻫ ﻥﺍﺭ ﺖﺴﯿﻟ ىﺭﻮﺘﻛﺮﯾﺍﺩ
Activate all
== ﻪﻤﻫ ىﺯﺎﺳ ﻝﺎﻌﻓ
Deactivate all
== ﻪﻤﻫ ىﺯﺎﺳ ﻝﺎﻌﻓﺮﯿﻏ
[Hertz]
Hz
== Hz
Player info change cooldown
== اطلاعات بازیکن تغییر زمان جریمه شده
Create a random skin
== ﯽﻓﺩﺎﺼﺗ ﻦﯿﻜﺳﺍ ﺖﺧﺎﺳ
Axis
== ﺭﻮﺤﻣ
Graphics card
== ﻚﯿﻓﺍﺮﮔ ﺕﺭﺎﻛ
Tee
== ﯽﺗ
Info Messages
== ﻡﺎﯿﭘ ﺕﺎﻋﻼﻃﺍ
Show local time always
== ﺖﭼ ﯽﮕﺸﯿﻤﻫ ﻥﺩﺍﺩ ﻥﺎﺸﻧ
Show client IDs (scoreboard, chat, spectator)
== (ﺮﮔﺎﺷﺎﻤﺗ ،ﺖﭼ ،ﺯﺎﯿﺘﻣﺍ) ﺖﻨﯾﻼﻛ ىﺎﻫ ﻪﺳﺎﻨﺷ ﺶﯾﺎﻤﻧ
Always show chat
== ﺖﭼ ﯽﮕﺸﯿﻤﻫ ﻥﺩﺍﺩ ﻥﺎﺸﻧ
Show only chat messages from team members
== ﺩﻮﺸﯿﻣ ﺖﻓﺎﯾﺭﺩ ﻢﯿﺗ ىﺎﻀﻋﺍ ﺯﺍ ﻪﻛ ﯽﯾﺎﻫ ﻡﺎﯿﭘ ﺶﯾﺎﻤﻧ
Chat font size
== ﺖﭼ ﺖﻧﻮﻓ ﻩﺯﺍﺪﻧﺍ
Chat width
== ﺖﭼ ﺽﺮﻋ
Show friend mark (♥) in name plates
== ﻥﺎﻨﻜﯾﺯﺎﺑ ﻢﺳﺍ ﺭﺎﻨﻛ ﺭﺩ (♥) ﺖﺳﻭﺩ ﺖﻣﻼﻋ ﺶﯾﺎﻤﻧ
Show hook strength icon indicator
== کﻮﻫ ﺕﺭﺪﻗ ﺩﺪﻋ ﺮﮕﻧﺎﺸﻧ ﺶﯾﺎﻤﻧ
Show hook strength number indicator
== کﻮﻫ ﺕﺭﺪﻗ ﺩﺪﻋ ﺮﮕﻧﺎﺸﻧ ﺶﯾﺎﻤﻧ
Authed name color in scoreboard
== ﺯﺎﯿﺘﻣﺍ ىﻮﻠﺑﺎﺗ ﺭﺩ ﻩﺪﺷ ﺪﯿﯾﺎﺗ ﻡﺎﻧ ﮓﻧﺭ
Same clan color in scoreboard
== ﺯﺎﯿﺘﻣﺍ ىﻮﻠﺑﺎﺗ ﺭﺩ ﯽﻨﻠﻛ ﻢﻫ ﮓﻧﺭ
Show own player's hook collision line
== ﻦﻜﯾﺯﺎﺑ کﻮﻫ ﺭﺰﯿﻟ ﺶﯾﺎﻤﻧ
Always show own player's hook collision line
== ﻦﻜﯾﺯﺎﺑ کﻮﻫ ﺭﺰﯿﻟ ﯽﮕﺸﯿﻤﻫ ﺶﯾﺎﻤﻧ
Always show other players' hook collision lines
== ﺮﮕﯾﺩ ﻥﺎﻨﻜﯾﺯﺎﺑ کﻮﻫ ﺭﺰﯿﻟ ﯽﮕﺸﯿﻤﻫ ﺶﯾﺎﻤﻧ
Show finish messages
== ﭗﻣ ﻥﺎﯾﺎﭘ ىﺎﻫ ﻡﺎﯿﭘ ﺶﯾﺎﻤﻧ
Enable ghost
== ﺡﻭﺭ ﺖﻟﺎﺣ ىﺯﺎﺳ ﻝﺎﻌﻓ
Only save improvements
== ﺩﻮﺷ ﻩﺮﯿﺧﺫ ﺩﻮﺒﻬﺑ ﻂﻘﻓ
Regular background color
== ﻢﻈﻨﻣ ﻪﻨﯿﻣﺯ ﺲﭘ ﮓﻧﺭ
Entities background color
== ﺖﯾﺩﻮﺟﻮﻣ ﻪﻨﯿﻣﺯ ﺲﭘ ﮓﻧﺭ
Unregister protocol and file extensions
== ﺖﺳﺍ ﻩﺪﺸﻧ ﺖﺒﺛ ﻞﯾﺎﻓ ﺪﻧﻮﺴﭘ ﻭ ﻞﻜﺗﻭﺮﭘ
Basic
== ﻪﯾﺎﭘ
Custom
== ﯽﺷﺭﺎﻔﺳ
Are you sure that you want to delete '%s'?
== ؟ﺪﯿﻨﻛ ﻑﺬﺣ ﺍﺭ '%s' ﺪﯿﻫﺍﻮﺧ ﯽﻣ ﻪﻛ ﺪﯿﺘﺴﻫ ﻦﺌﻤﻄﻣ ﺎﯾﺁ
Delete skin
== ﻦﯿﻜﺳﺍ ﻑﺬﺣ
Unable to delete skin
== ﺖﺴﯿﻧ ﻦﻜﻤﻣ ﻦﯿﻜﺳﺍ ﻑﺬﺣ
Round %d/%d
== %d/%d ﺪﻧﺍﺭ
[Spectators]
%d others…
== ﺮﮕﯾﺩ %d
[Team and size]
%d\n(%d/%d)
== %d\n(%d/%d)
Team %d (%d/%d)
== %d (%d/%d) ﻢﯿﺗ
[skins]
Body
== ﻥﺪﺑ
[skins]
Marking
== ىﺭﺍﺬﮔ ﺖﻣﻼﻋ
[skins]
Decoration
== ﻥﻮﯿﺳﺍﺭﻮﻛﺩ
[skins]
Hands
== ﺖﺳﺩ
[skins]
Feet
== ﺎﭘ
[skins]
Eyes
== ﻢﺸﭼ
FPM
== FPM
Spree
== ﯽﻨﻜﺷ ﻥﻮﻧﺎﻗ
Grabs
== ﻥﺩﺯ ﮓﻨﭼ
Moved ingame
== ﺪﺷ ﻞﻘﺘﻨﻣ ىﺯﺎﺑ
https://wiki.ddnet.org/wiki/Mapping
== https://wiki.ddnet.org/wiki/Mapping
The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs.
==
The format of texture %s is not RGBA which will cause visual bugs.
==
Loading demo file from storage
==
Initializing map logic
==
Quitting. Please wait…
==
Restarting. Please wait…
==
Searching
==
Enter Username
==
Enter Password
==
NOT CONNECTED
==
Match %d of %d
==
No results
==
Lines %d - %d (%s)
==
Locked
==
Following
==
Loading commands…
==
Multi-View
==
[Spectating]
Following %s
==
Some map images could not be loaded. Check the local console for details.
==
Some map sounds could not be loaded. Check the local console for details.
==
Loading menu themes
==
Press a key…
==
Main menu
==
Rename folder
==
Render complete
==
Are you sure that you want to restart?
==
Save skin
==
Are you sure you want to save your skin? If a skin with this name already exists, it will be replaced.
==
There's an unsaved map in the editor, you might want to save it.
==
Continue anyway?
==
A demo with this name already exists
==
A folder with this name already exists
==
Unable to rename the folder
==
(paused)
==
Videos directory
==
Video was saved to '%s'
==
Unable to save the skin
==
Unable to save the skin with a reserved name
==
%d/%d KiB (%.1f KiB/s)
==
No local servers found (ports %d-%d)
==
Example of usage
==
No login required
==
Communities
==
Copy info
==
No server selected
==
Online players (%d)
==
Online clanmates (%d)
==
[friends (server browser)]
Offline (%d)
==
Click to select server. Double click to join your friend.
==
Click to remove this player from your friends list.
==
Click to remove this clan from your friends list.
==
None
==
Add Clan
==
Server filter
==
Friends
==
Go back the specified duration
==
[Demo player duration]
%d min.
==
[Demo player duration]
%d sec.
==
Change the skip duration
==
Go forward the specified duration
==
Mark the beginning of a cut (right click to reset)
==
Mark the end of a cut (right click to reset)
==
Close the demo player
==
Export demo cut
==
Cut interval
==
Cut length
==
Render cut to video
==
Please use a different filename
==
All combined
==
No demo selected
==
Folder Link
==
Created
==
Netversion
==
[Demo details]
map not included
==
Are you sure that you want to delete the folder '%s'?
==
Delete folder
==
Unable to delete the folder '%s'. Make sure it's empty first.
==
Dummy is not allowed on this server
==
Please wait…
==
Loading…
==
Ghosts directory
==
Activate all
==
Deactivate all
==
[Hertz]
Hz
==
Player info change cooldown
==
Create a random skin
==
Axis
==
Graphics card
==
Tee
==
Info Messages
==
Show local time always
==
Show client IDs (scoreboard, chat, spectator)
==
Always show chat
==
Show only chat messages from team members
==
Chat font size
==
Chat width
==
Show friend mark (♥) in name plates
==
Show hook strength icon indicator
==
Show hook strength number indicator
==
Authed name color in scoreboard
==
Same clan color in scoreboard
==
Show own player's hook collision line
==
Always show own player's hook collision line
==
Always show other players' hook collision lines
==
Show finish messages
==
Enable ghost
==
Only save improvements
==
Regular background color
==
Entities background color
==
Unregister protocol and file extensions
==
Basic
==
Custom
==
Are you sure that you want to delete '%s'?
==
Delete skin
==
Unable to delete skin
==
Round %d/%d
==
[Spectators]
%d others…
==
[Team and size]
%d\n(%d/%d)
==
Team %d (%d/%d)
==
[skins]
Body
==
[skins]
Marking
==
[skins]
Decoration
==
[skins]
Hands
==
[skins]
Feet
==
[skins]
Eyes
==
FPM
==
Spree
==
Grabs
==
Moved ingame
==
https://wiki.ddnet.org/wiki/Mapping
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -1945,3 +1945,6 @@ Moved ingame
https://wiki.ddnet.org/wiki/Mapping
== https://wiki.ddnet.org/wiki/Mapping
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -932,6 +932,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -606,6 +606,9 @@ Loading map file from storage
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Loading DDNet Client
==

View file

@ -1912,6 +1912,9 @@ Custom
Unable to delete skin
== Невозможно удалить скин
Some fonts could not be loaded. Check the local console for details.
==
Save skin
==

View file

@ -1664,6 +1664,9 @@ Error saving settings
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Searching
==

View file

@ -1440,6 +1440,9 @@ Error saving settings
Loading demo file from storage
==
Some fonts could not be loaded. Check the local console for details.
==
Initializing components
==

View file

@ -1983,3 +1983,6 @@ Feet
[skins]
Eyes
== 眼睛
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -1940,3 +1940,6 @@ Feet
[skins]
Eyes
== Oči
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -1958,3 +1958,6 @@ Feet
[skins]
Eyes
== Ojos
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -1942,3 +1942,6 @@ Feet
[skins]
Eyes
== Ögon
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -1972,3 +1972,6 @@ Feet
[skins]
Eyes
== 眼睛
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -1952,3 +1952,6 @@ Feet
[skins]
Eyes
== Göz
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -1939,3 +1939,6 @@ Zoom in
Zoom out
== Віддалити
Some fonts could not be loaded. Check the local console for details.
==

View file

@ -26,6 +26,9 @@ if [ -z ${1+x} ]; then
printf "${COLOR_RED}%s${COLOR_RESET}\n" "Did not pass Android build type"
else
ANDROID_BUILD=$1
if [[ "${ANDROID_BUILD}" == "x64" ]]; then
ANDROID_BUILD="x86_64"
fi
printf "${COLOR_YELLOW}%s${COLOR_RESET}\n" "Android build type: ${ANDROID_BUILD}"
fi
@ -168,7 +171,7 @@ if [[ "${ANDROID_BUILD}" == "x86" || "${ANDROID_BUILD}" == "all" ]]; then
PID_BUILD_X86=$!
fi
if [[ "${ANDROID_BUILD}" == "x86_64" || "${ANDROID_BUILD}" == "x64" || "${ANDROID_BUILD}" == "all" ]]; then
if [[ "${ANDROID_BUILD}" == "x86_64" || "${ANDROID_BUILD}" == "all" ]]; then
build_for_type x86_64 x86_64 x86_64-linux-android &
PID_BUILD_X86_64=$!
fi
@ -236,7 +239,7 @@ if [[ "${ANDROID_BUILD}" == "x86" || "${ANDROID_BUILD}" == "all" ]]; then
copy_libs x86 x86
fi
if [[ "${ANDROID_BUILD}" == "x86_64" || "${ANDROID_BUILD}" == "x64" || "${ANDROID_BUILD}" == "all" ]]; then
if [[ "${ANDROID_BUILD}" == "x86_64" || "${ANDROID_BUILD}" == "all" ]]; then
copy_libs x86_64 x86_64
fi

View file

@ -32,6 +32,12 @@ struct LOG_COLOR
#define log_debug(sys, ...) log_log(LEVEL_DEBUG, sys, __VA_ARGS__)
#define log_trace(sys, ...) log_log(LEVEL_TRACE, sys, __VA_ARGS__)
#define log_error_color(color, sys, ...) log_log_color(LEVEL_ERROR, color, sys, __VA_ARGS__)
#define log_warn_color(color, sys, ...) log_log_color(LEVEL_WARN, color, sys, __VA_ARGS__)
#define log_info_color(color, sys, ...) log_log_color(LEVEL_INFO, color, sys, __VA_ARGS__)
#define log_debug_color(color, sys, ...) log_log_color(LEVEL_DEBUG, color, sys, __VA_ARGS__)
#define log_trace_color(color, sys, ...) log_log_color(LEVEL_TRACE, color, sys, __VA_ARGS__)
/**
* @defgroup Log
*

View file

@ -2484,7 +2484,7 @@ int fs_rename(const char *oldname, const char *newname)
#if defined(CONF_FAMILY_WINDOWS)
const std::wstring wide_oldname = windows_utf8_to_wide(oldname);
const std::wstring wide_newname = windows_utf8_to_wide(newname);
if(MoveFileExW(wide_oldname.c_str(), wide_newname.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) == 0)
if(MoveFileExW(wide_oldname.c_str(), wide_newname.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH) == 0)
return 1;
#else
if(rename(oldname, newname) != 0)

View file

@ -282,8 +282,6 @@ public:
virtual bool IsSixup() const = 0;
virtual int GetCurrentRaceTime() = 0;
virtual void RaceRecord_Start(const char *pFilename) = 0;
virtual void RaceRecord_Stop() = 0;
virtual bool RaceRecord_IsRecording() = 0;
@ -369,7 +367,6 @@ public:
virtual int OnSnapInput(int *pData, bool Dummy, bool Force) = 0;
virtual void OnDummySwap() = 0;
virtual void SendDummyInfo(bool Start) = 0;
virtual int GetLastRaceTick() const = 0;
virtual const char *GetItemName(int Type) const = 0;
virtual const char *Version() const = 0;

View file

@ -3584,7 +3584,7 @@ public:
auto Minor = (DriverVersion >> 12) & 0x3ff;
auto Patch = DriverVersion & 0xfff;
return Major == 2 && Minor == 0 && Patch > 116 && Patch < 220 && ((ApiMajor <= 1 && ApiMinor < 3) || (ApiMajor <= 1 && ApiMinor == 3 && ApiPatch < 206));
return Major == 2 && Minor == 0 && Patch > 137 && Patch < 220 && ((ApiMajor <= 1 && ApiMinor < 3) || (ApiMajor <= 1 && ApiMinor == 3 && ApiPatch < 206));
}
#endif
return false;

View file

@ -772,13 +772,6 @@ bool CClient::DummyAllowed() const
return m_ServerCapabilities.m_AllowDummy;
}
int CClient::GetCurrentRaceTime()
{
if(GameClient()->GetLastRaceTick() < 0)
return 0;
return (GameTick(g_Config.m_ClDummy) - GameClient()->GetLastRaceTick()) / GameTickSpeed();
}
void CClient::GetServerInfo(CServerInfo *pServerInfo) const
{
mem_copy(pServerInfo, &m_CurrentServerInfo, sizeof(m_CurrentServerInfo));
@ -3630,7 +3623,8 @@ void CClient::Con_AddFavorite(IConsole::IResult *pResult, void *pUserData)
{
CClient *pSelf = (CClient *)pUserData;
NETADDR Addr;
if(net_addr_from_str(&Addr, pResult->GetString(0)) != 0)
if(net_addr_from_url(&Addr, pResult->GetString(0), nullptr, 0) != 0 && net_addr_from_str(&Addr, pResult->GetString(0)) != 0)
{
char aBuf[128];
str_format(aBuf, sizeof(aBuf), "invalid address '%s'", pResult->GetString(0));

View file

@ -487,8 +487,6 @@ public:
void GenerateTimeoutSeed() override;
void GenerateTimeoutCodes(const NETADDR *pAddrs, int NumAddrs);
int GetCurrentRaceTime() override;
const char *GetCurrentMap() const override;
const char *GetCurrentMapPath() const override;
SHA256_DIGEST GetCurrentMapSha256() const override;

View file

@ -42,7 +42,21 @@ void CFavorites::OnConfigSave(IConfigManager *pConfigManager)
{
char aAddr[NETADDR_MAXSTRSIZE];
char aBuffer[128];
net_addr_str(&Entry.m_aAddrs[i], aAddr, sizeof(aAddr), true);
net_addr_str(&Entry.m_aAddrs[i], aBuffer, sizeof(aBuffer), true);
if(Entry.m_aAddrs[i].type & NETTYPE_TW7)
{
str_format(
aAddr,
sizeof(aAddr),
"tw-0.7+udp://%s",
aBuffer);
}
else
{
str_copy(aAddr, aBuffer);
}
if(!Entry.m_AllowPing)
{
str_format(aBuffer, sizeof(aBuffer), "add_favorite %s", aAddr);

View file

@ -1,8 +1,8 @@
#include "ghost.h"
#include <base/log.h>
#include <base/system.h>
#include <engine/console.h>
#include <engine/shared/compression.h>
#include <engine/shared/config.h>
#include <engine/shared/network.h>
@ -10,33 +10,52 @@
static const unsigned char gs_aHeaderMarker[8] = {'T', 'W', 'G', 'H', 'O', 'S', 'T', 0};
static const unsigned char gs_CurVersion = 6;
static const int gs_NumTicksOffset = 93;
static const ColorRGBA gs_GhostPrintColor{0.65f, 0.6f, 0.6f, 1.0f};
static const LOG_COLOR LOG_COLOR_GHOST{165, 153, 153};
int CGhostHeader::GetTicks() const
{
return bytes_be_to_uint(m_aNumTicks);
}
int CGhostHeader::GetTime() const
{
return bytes_be_to_uint(m_aTime);
}
CGhostInfo CGhostHeader::ToGhostInfo() const
{
CGhostInfo Result;
str_copy(Result.m_aOwner, m_aOwner);
str_copy(Result.m_aMap, m_aMap);
Result.m_NumTicks = GetTicks();
Result.m_Time = GetTime();
return Result;
}
CGhostRecorder::CGhostRecorder()
{
m_File = 0;
m_aFilename[0] = '\0';
ResetBuffer();
}
void CGhostRecorder::Init()
{
m_pConsole = Kernel()->RequestInterface<IConsole>();
m_pStorage = Kernel()->RequestInterface<IStorage>();
}
// Record
int CGhostRecorder::Start(const char *pFilename, const char *pMap, SHA256_DIGEST MapSha256, const char *pName)
int CGhostRecorder::Start(const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, const char *pName)
{
dbg_assert(!m_File, "File already open");
m_File = m_pStorage->OpenFile(pFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE);
if(!m_File)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "Unable to open '%s' for ghost recording", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_recorder", aBuf, gs_GhostPrintColor);
log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Unable to open '%s' for recording", pFilename);
return -1;
}
str_copy(m_aFilename, pFilename);
// write header
CGhostHeader Header;
@ -51,19 +70,18 @@ int CGhostRecorder::Start(const char *pFilename, const char *pMap, SHA256_DIGEST
m_LastItem.Reset();
ResetBuffer();
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "ghost recording to '%s'", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_recorder", aBuf, gs_GhostPrintColor);
log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Recording to '%s'", pFilename);
return 0;
}
void CGhostRecorder::ResetBuffer()
{
m_pBufferPos = m_aBuffer;
m_pBufferEnd = m_aBuffer;
m_BufferNumItems = 0;
}
static void DiffItem(int *pPast, int *pCurrent, int *pOut, int Size)
static void DiffItem(const int32_t *pPast, const int32_t *pCurrent, int32_t *pOut, size_t Size)
{
while(Size)
{
@ -75,16 +93,23 @@ static void DiffItem(int *pPast, int *pCurrent, int *pOut, int Size)
}
}
void CGhostRecorder::WriteData(int Type, const void *pData, int Size)
void CGhostRecorder::WriteData(int Type, const void *pData, size_t Size)
{
if(!m_File || (unsigned)Size > MAX_ITEM_SIZE || Size <= 0 || Type == -1)
return;
dbg_assert((bool)m_File, "File not open");
dbg_assert(Type >= 0 && Type <= (int)std::numeric_limits<unsigned char>::max(), "Type invalid");
dbg_assert(Size > 0 && Size <= MAX_ITEM_SIZE && Size % sizeof(int32_t) == 0, "Size invalid");
if((size_t)(m_pBufferEnd - m_pBufferPos) < Size)
{
FlushChunk();
}
CGhostItem Data(Type);
mem_copy(Data.m_aData, pData, Size);
if(m_LastItem.m_Type == Data.m_Type)
DiffItem((int *)m_LastItem.m_aData, (int *)Data.m_aData, (int *)m_pBufferPos, Size / sizeof(int32_t));
{
DiffItem((const int32_t *)m_LastItem.m_aData, (const int32_t *)Data.m_aData, (int32_t *)m_pBufferPos, Size / sizeof(int32_t));
}
else
{
FlushChunk();
@ -95,235 +120,313 @@ void CGhostRecorder::WriteData(int Type, const void *pData, int Size)
m_pBufferPos += Size;
m_BufferNumItems++;
if(m_BufferNumItems >= NUM_ITEMS_PER_CHUNK)
{
FlushChunk();
}
}
void CGhostRecorder::FlushChunk()
{
static char s_aBuffer[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
static char s_aBuffer2[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
unsigned char aChunk[4];
dbg_assert((bool)m_File, "File not open");
int Size = m_pBufferPos - m_aBuffer;
int Type = m_LastItem.m_Type;
if(!m_File || Size == 0)
if(Size == 0 || m_BufferNumItems == 0)
{
return;
}
dbg_assert(Size % sizeof(int32_t) == 0, "Chunk size invalid");
while(Size & 3)
m_aBuffer[Size++] = 0;
Size = CVariableInt::Compress(m_aBuffer, Size, s_aBuffer, sizeof(s_aBuffer));
Size = CVariableInt::Compress(m_aBuffer, Size, m_aBufferTemp, sizeof(m_aBufferTemp));
if(Size < 0)
{
log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Failed to write chunk to '%s': error during intpack compression", m_aFilename);
m_LastItem.Reset();
ResetBuffer();
return;
}
Size = CNetBase::Compress(s_aBuffer, Size, s_aBuffer2, sizeof(s_aBuffer2));
Size = CNetBase::Compress(m_aBufferTemp, Size, m_aBuffer, sizeof(m_aBuffer));
if(Size < 0)
{
log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Failed to write chunk to '%s': error during network compression", m_aFilename);
m_LastItem.Reset();
ResetBuffer();
return;
}
aChunk[0] = Type & 0xff;
aChunk[1] = m_BufferNumItems & 0xff;
aChunk[2] = (Size >> 8) & 0xff;
aChunk[3] = (Size)&0xff;
unsigned char aChunkHeader[4];
aChunkHeader[0] = m_LastItem.m_Type & 0xff;
aChunkHeader[1] = m_BufferNumItems & 0xff;
aChunkHeader[2] = (Size >> 8) & 0xff;
aChunkHeader[3] = Size & 0xff;
io_write(m_File, aChunk, sizeof(aChunk));
io_write(m_File, s_aBuffer2, Size);
io_write(m_File, aChunkHeader, sizeof(aChunkHeader));
io_write(m_File, m_aBuffer, Size);
m_LastItem.Reset();
ResetBuffer();
}
int CGhostRecorder::Stop(int Ticks, int Time)
void CGhostRecorder::Stop(int Ticks, int Time)
{
if(!m_File)
return -1;
{
return;
}
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_recorder", "Stopped ghost recording", gs_GhostPrintColor);
const bool DiscardFile = Ticks <= 0 || Time <= 0;
FlushChunk();
if(!DiscardFile)
{
FlushChunk();
// write down num shots and time
io_seek(m_File, gs_NumTicksOffset, IOSEEK_START);
// write number of ticks and time
io_seek(m_File, offsetof(CGhostHeader, m_aNumTicks), IOSEEK_START);
unsigned char aNumTicks[sizeof(int32_t)];
uint_to_bytes_be(aNumTicks, Ticks);
io_write(m_File, aNumTicks, sizeof(aNumTicks));
unsigned char aNumTicks[sizeof(int32_t)];
uint_to_bytes_be(aNumTicks, Ticks);
io_write(m_File, aNumTicks, sizeof(aNumTicks));
unsigned char aTime[sizeof(int32_t)];
uint_to_bytes_be(aTime, Time);
io_write(m_File, aTime, sizeof(aTime));
unsigned char aTime[sizeof(int32_t)];
uint_to_bytes_be(aTime, Time);
io_write(m_File, aTime, sizeof(aTime));
}
io_close(m_File);
m_File = 0;
return 0;
if(DiscardFile)
{
m_pStorage->RemoveFile(m_aFilename, IStorage::TYPE_SAVE);
}
log_info_color(LOG_COLOR_GHOST, "ghost_recorder", "Stopped recording to '%s'", m_aFilename);
m_aFilename[0] = '\0';
}
CGhostLoader::CGhostLoader()
{
m_File = 0;
m_aFilename[0] = '\0';
ResetBuffer();
}
void CGhostLoader::Init()
{
m_pConsole = Kernel()->RequestInterface<IConsole>();
m_pStorage = Kernel()->RequestInterface<IStorage>();
}
void CGhostLoader::ResetBuffer()
{
m_pBufferPos = m_aBuffer;
m_pBufferEnd = m_aBuffer;
m_BufferNumItems = 0;
m_BufferCurItem = 0;
m_BufferPrevItem = -1;
}
int CGhostLoader::Load(const char *pFilename, const char *pMap, SHA256_DIGEST MapSha256, unsigned MapCrc)
IOHANDLE CGhostLoader::ReadHeader(CGhostHeader &Header, const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc, bool LogMapMismatch) const
{
m_File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, IStorage::TYPE_SAVE);
if(!m_File)
IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, IStorage::TYPE_SAVE);
if(!File)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "could not open '%s'", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_loader", aBuf);
return -1;
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to open ghost file '%s' for reading", pFilename);
return nullptr;
}
// read the header
mem_zero(&m_Header, sizeof(m_Header));
io_read(m_File, &m_Header, sizeof(CGhostHeader));
if(mem_comp(m_Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) != 0)
if(io_read(File, &Header, sizeof(Header)) != sizeof(Header))
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "'%s' is not a ghost file", pFilename);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_loader", aBuf);
io_close(m_File);
m_File = 0;
return -1;
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': failed to read header", pFilename);
io_close(File);
return nullptr;
}
if(!(4 <= m_Header.m_Version && m_Header.m_Version <= gs_CurVersion))
if(!ValidateHeader(Header, pFilename) ||
!CheckHeaderMap(Header, pFilename, pMap, MapSha256, MapCrc, LogMapMismatch))
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "ghost version %d is not supported", m_Header.m_Version);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_loader", aBuf);
io_close(m_File);
m_File = 0;
return -1;
io_close(File);
return nullptr;
}
if(str_comp(m_Header.m_aMap, pMap) != 0)
return File;
}
bool CGhostLoader::ValidateHeader(const CGhostHeader &Header, const char *pFilename) const
{
if(mem_comp(Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) != 0)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "ghost map name '%s' does not match current map '%s'", m_Header.m_aMap, pMap);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_loader", aBuf);
io_close(m_File);
m_File = 0;
return -1;
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': invalid header marker", pFilename);
return false;
}
if(m_Header.m_Version >= 6)
if(Header.m_Version < 4 || Header.m_Version > gs_CurVersion)
{
if(m_Header.m_MapSha256 != MapSha256 && g_Config.m_ClRaceGhostStrictMap)
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': ghost version '%d' is not supported", pFilename, Header.m_Version);
return false;
}
if(!mem_has_null(Header.m_aOwner, sizeof(Header.m_aOwner)) || !str_utf8_check(Header.m_aOwner))
{
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': owner name is invalid", pFilename);
return false;
}
if(!mem_has_null(Header.m_aMap, sizeof(Header.m_aMap)) || !str_utf8_check(Header.m_aMap))
{
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': map name is invalid", pFilename);
return false;
}
const int NumTicks = Header.GetTicks();
if(NumTicks <= 0)
{
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': number of ticks '%d' is invalid", pFilename, NumTicks);
return false;
}
const int Time = Header.GetTime();
if(Time <= 0)
{
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': time '%d' is invalid", pFilename, Time);
return false;
}
return true;
}
bool CGhostLoader::CheckHeaderMap(const CGhostHeader &Header, const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc, bool LogMapMismatch) const
{
if(str_comp(Header.m_aMap, pMap) != 0)
{
if(LogMapMismatch)
{
char aGhostSha256[SHA256_MAXSTRSIZE];
sha256_str(m_Header.m_MapSha256, aGhostSha256, sizeof(aGhostSha256));
char aMapSha256[SHA256_MAXSTRSIZE];
sha256_str(MapSha256, aMapSha256, sizeof(aMapSha256));
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "ghost map '%s' sha256 mismatch, wanted=%s ghost=%s", pMap, aMapSha256, aGhostSha256);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_loader", aBuf);
io_close(m_File);
m_File = 0;
return -1;
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': ghost map name '%s' does not match current map '%s'", pFilename, Header.m_aMap, pMap);
}
return false;
}
if(Header.m_Version >= 6)
{
if(Header.m_MapSha256 != MapSha256 && g_Config.m_ClRaceGhostStrictMap)
{
if(LogMapMismatch)
{
char aGhostSha256[SHA256_MAXSTRSIZE];
sha256_str(Header.m_MapSha256, aGhostSha256, sizeof(aGhostSha256));
char aMapSha256[SHA256_MAXSTRSIZE];
sha256_str(MapSha256, aMapSha256, sizeof(aMapSha256));
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': ghost map SHA256 mismatch (wanted='%s', ghost='%s')", pFilename, aMapSha256, aGhostSha256);
}
return false;
}
}
else
{
io_skip(m_File, -(int)sizeof(SHA256_DIGEST));
unsigned GhostMapCrc = bytes_be_to_uint(m_Header.m_aZeroes);
const unsigned GhostMapCrc = bytes_be_to_uint(Header.m_aZeroes);
if(GhostMapCrc != MapCrc && g_Config.m_ClRaceGhostStrictMap)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "ghost map '%s' crc mismatch, wanted=%08x ghost=%08x", pMap, MapCrc, GhostMapCrc);
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost_loader", aBuf);
io_close(m_File);
m_File = 0;
return -1;
if(LogMapMismatch)
{
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': ghost map CRC mismatch (wanted='%08x', ghost='%08x')", pFilename, MapCrc, GhostMapCrc);
}
return false;
}
}
return true;
}
bool CGhostLoader::Load(const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc)
{
dbg_assert(!m_File, "File already open");
CGhostHeader Header;
IOHANDLE File = ReadHeader(Header, pFilename, pMap, MapSha256, MapCrc, true);
if(!File)
{
return false;
}
if(Header.m_Version < 6)
{
io_skip(File, -(int)sizeof(SHA256_DIGEST));
}
m_File = File;
str_copy(m_aFilename, pFilename);
m_Header = Header;
m_Info = m_Header.ToGhostInfo();
m_LastItem.Reset();
ResetBuffer();
return 0;
return true;
}
int CGhostLoader::ReadChunk(int *pType)
bool CGhostLoader::ReadChunk(int *pType)
{
static char s_aCompresseddata[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
static char s_aDecompressed[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
unsigned char aChunk[4];
if(m_Header.m_Version != 4)
{
m_LastItem.Reset();
}
ResetBuffer();
if(io_read(m_File, aChunk, sizeof(aChunk)) != sizeof(aChunk))
return -1;
*pType = aChunk[0];
int Size = (aChunk[2] << 8) | aChunk[3];
m_BufferNumItems = aChunk[1];
if(Size > MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK || Size <= 0)
return -1;
if(io_read(m_File, s_aCompresseddata, Size) != (unsigned)Size)
unsigned char aChunkHeader[4];
if(io_read(m_File, aChunkHeader, sizeof(aChunkHeader)) != sizeof(aChunkHeader))
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error reading chunk");
return -1;
return false; // EOF
}
Size = CNetBase::Decompress(s_aCompresseddata, Size, s_aDecompressed, sizeof(s_aDecompressed));
*pType = aChunkHeader[0];
int Size = (aChunkHeader[2] << 8) | aChunkHeader[3];
m_BufferNumItems = aChunkHeader[1];
if(Size <= 0 || Size > MAX_CHUNK_SIZE)
{
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': invalid chunk header size", m_aFilename);
return false;
}
if(io_read(m_File, m_aBuffer, Size) != (unsigned)Size)
{
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': error reading chunk data", m_aFilename);
return false;
}
Size = CNetBase::Decompress(m_aBuffer, Size, m_aBufferTemp, sizeof(m_aBufferTemp));
if(Size < 0)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during network decompression");
return -1;
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': error during network decompression", m_aFilename);
return false;
}
Size = CVariableInt::Decompress(s_aDecompressed, Size, m_aBuffer, sizeof(m_aBuffer));
Size = CVariableInt::Decompress(m_aBufferTemp, Size, m_aBuffer, sizeof(m_aBuffer));
if(Size < 0)
{
m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during intpack decompression");
return -1;
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': error during intpack decompression", m_aFilename);
return false;
}
return 0;
m_pBufferEnd = m_aBuffer + Size;
return true;
}
bool CGhostLoader::ReadNextType(int *pType)
{
if(!m_File)
return false;
dbg_assert((bool)m_File, "File not open");
if(m_BufferCurItem != m_BufferPrevItem && m_BufferCurItem < m_BufferNumItems)
{
*pType = m_LastItem.m_Type;
}
else
else if(!ReadChunk(pType))
{
if(ReadChunk(pType))
return false; // error or eof
return false; // error or EOF
}
m_BufferPrevItem = m_BufferCurItem;
return true;
}
static void UndiffItem(int *pPast, int *pDiff, int *pOut, int Size)
static void UndiffItem(const int32_t *pPast, const int32_t *pDiff, int32_t *pOut, size_t Size)
{
while(Size)
{
@ -335,17 +438,27 @@ static void UndiffItem(int *pPast, int *pDiff, int *pOut, int Size)
}
}
bool CGhostLoader::ReadData(int Type, void *pData, int Size)
bool CGhostLoader::ReadData(int Type, void *pData, size_t Size)
{
if(!m_File || Size > MAX_ITEM_SIZE || Size <= 0 || Type == -1)
dbg_assert((bool)m_File, "File not open");
dbg_assert(Type >= 0 && Type <= (int)std::numeric_limits<unsigned char>::max(), "Type invalid");
dbg_assert(Size > 0 && Size <= MAX_ITEM_SIZE && Size % sizeof(int32_t) == 0, "Size invalid");
if((size_t)(m_pBufferEnd - m_pBufferPos) < Size)
{
log_error_color(LOG_COLOR_GHOST, "ghost_loader", "Failed to read ghost file '%s': not enough data (type='%d', got='%" PRIzu "', wanted='%" PRIzu "')", m_aFilename, Type, (size_t)(m_pBufferEnd - m_pBufferPos), Size);
return false;
}
CGhostItem Data(Type);
if(m_LastItem.m_Type == Data.m_Type)
UndiffItem((int *)m_LastItem.m_aData, (int *)m_pBufferPos, (int *)Data.m_aData, Size / sizeof(int32_t));
{
UndiffItem((const int32_t *)m_LastItem.m_aData, (const int32_t *)m_pBufferPos, (int32_t *)Data.m_aData, Size / sizeof(int32_t));
}
else
{
mem_copy(Data.m_aData, m_pBufferPos, Size);
}
mem_copy(pData, Data.m_aData, Size);
@ -358,47 +471,24 @@ bool CGhostLoader::ReadData(int Type, void *pData, int Size)
void CGhostLoader::Close()
{
if(!m_File)
{
return;
}
io_close(m_File);
m_File = 0;
m_aFilename[0] = '\0';
}
bool CGhostLoader::GetGhostInfo(const char *pFilename, CGhostInfo *pGhostInfo, const char *pMap, SHA256_DIGEST MapSha256, unsigned MapCrc)
bool CGhostLoader::GetGhostInfo(const char *pFilename, CGhostInfo *pGhostInfo, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc)
{
CGhostHeader Header;
mem_zero(&Header, sizeof(Header));
IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, IStorage::TYPE_SAVE);
IOHANDLE File = ReadHeader(Header, pFilename, pMap, MapSha256, MapCrc, false);
if(!File)
{
return false;
io_read(File, &Header, sizeof(Header));
}
io_close(File);
if(mem_comp(Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) || !(4 <= Header.m_Version && Header.m_Version <= gs_CurVersion))
return false;
if(str_comp(Header.m_aMap, pMap) != 0)
{
return false;
}
if(Header.m_Version >= 6 && g_Config.m_ClRaceGhostStrictMap)
{
if(Header.m_MapSha256 != MapSha256)
{
return false;
}
}
else if(g_Config.m_ClRaceGhostStrictMap)
{
unsigned GhostMapCrc = bytes_be_to_uint(Header.m_aZeroes);
if(GhostMapCrc != MapCrc)
{
return false;
}
}
*pGhostInfo = Header.ToGhostInfo();
return true;
}

View file

@ -3,13 +3,15 @@
#include <engine/ghost.h>
#include <base/system.h>
#include <cstdint>
enum
{
MAX_ITEM_SIZE = 128,
NUM_ITEMS_PER_CHUNK = 50,
MAX_CHUNK_SIZE = MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK,
};
static_assert(MAX_CHUNK_SIZE % sizeof(int32_t) == 0, "Chunk size must be aligned with int32_t");
// version 4-6
struct CGhostHeader
@ -23,32 +25,15 @@ struct CGhostHeader
unsigned char m_aTime[sizeof(int32_t)];
SHA256_DIGEST m_MapSha256;
int GetTicks() const
{
return bytes_be_to_uint(m_aNumTicks);
}
int GetTime() const
{
return bytes_be_to_uint(m_aTime);
}
CGhostInfo ToGhostInfo() const
{
CGhostInfo Result;
mem_zero(&Result, sizeof(Result));
str_copy(Result.m_aOwner, m_aOwner);
str_copy(Result.m_aMap, m_aMap);
Result.m_NumTicks = GetTicks();
Result.m_Time = GetTime();
return Result;
}
int GetTicks() const;
int GetTime() const;
CGhostInfo ToGhostInfo() const;
};
class CGhostItem
{
public:
unsigned char m_aData[MAX_ITEM_SIZE];
alignas(int32_t) unsigned char m_aData[MAX_ITEM_SIZE];
int m_Type;
CGhostItem() :
@ -61,14 +46,15 @@ public:
class CGhostRecorder : public IGhostRecorder
{
IOHANDLE m_File;
class IConsole *m_pConsole;
char m_aFilename[IO_MAX_PATH_LENGTH];
class IStorage *m_pStorage;
CGhostItem m_LastItem;
char m_aBuffer[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
alignas(int32_t) char m_aBuffer[MAX_CHUNK_SIZE];
alignas(int32_t) char m_aBufferTemp[MAX_CHUNK_SIZE];
char *m_pBufferPos;
const char *m_pBufferEnd;
int m_BufferNumItems;
CGhostItem m_LastItem;
void ResetBuffer();
void FlushChunk();
@ -78,45 +64,49 @@ public:
void Init();
int Start(const char *pFilename, const char *pMap, SHA256_DIGEST MapSha256, const char *pName) override;
int Stop(int Ticks, int Time) override;
int Start(const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, const char *pName) override;
void Stop(int Ticks, int Time) override;
void WriteData(int Type, const void *pData, int Size) override;
void WriteData(int Type, const void *pData, size_t Size) override;
bool IsRecording() const override { return m_File != nullptr; }
};
class CGhostLoader : public IGhostLoader
{
IOHANDLE m_File;
class IConsole *m_pConsole;
char m_aFilename[IO_MAX_PATH_LENGTH];
class IStorage *m_pStorage;
CGhostHeader m_Header;
CGhostInfo m_Info;
CGhostItem m_LastItem;
char m_aBuffer[MAX_ITEM_SIZE * NUM_ITEMS_PER_CHUNK];
alignas(int32_t) char m_aBuffer[MAX_CHUNK_SIZE];
alignas(int32_t) char m_aBufferTemp[MAX_CHUNK_SIZE];
char *m_pBufferPos;
const char *m_pBufferEnd;
int m_BufferNumItems;
int m_BufferCurItem;
int m_BufferPrevItem;
CGhostItem m_LastItem;
void ResetBuffer();
int ReadChunk(int *pType);
IOHANDLE ReadHeader(CGhostHeader &Header, const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc, bool LogMapMismatch) const;
bool ValidateHeader(const CGhostHeader &Header, const char *pFilename) const;
bool CheckHeaderMap(const CGhostHeader &Header, const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc, bool LogMapMismatch) const;
bool ReadChunk(int *pType);
public:
CGhostLoader();
void Init();
int Load(const char *pFilename, const char *pMap, SHA256_DIGEST MapSha256, unsigned MapCrc) override;
bool Load(const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc) override;
void Close() override;
const CGhostInfo *GetInfo() const override { return &m_Info; }
bool ReadNextType(int *pType) override;
bool ReadData(int Type, void *pData, int Size) override;
bool ReadData(int Type, void *pData, size_t Size) override;
bool GetGhostInfo(const char *pFilename, CGhostInfo *pGhostInfo, const char *pMap, SHA256_DIGEST MapSha256, unsigned MapCrc) override;
bool GetGhostInfo(const char *pFilename, CGhostInfo *pGhostInfo, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc) override;
};
#endif

View file

@ -615,37 +615,65 @@ public:
void AddFace(FT_Face Face)
{
m_vFtFaces.push_back(Face);
if(!m_DefaultFace)
m_DefaultFace = Face;
}
void SetDefaultFaceByName(const char *pFamilyName)
bool SetDefaultFaceByName(const char *pFamilyName)
{
m_DefaultFace = GetFaceByName(pFamilyName);
if(!m_DefaultFace)
{
if(!m_vFtFaces.empty())
{
m_DefaultFace = m_vFtFaces.front();
}
log_error("textrender", "The default font face '%s' could not be found", pFamilyName);
return false;
}
return true;
}
void SetIconFaceByName(const char *pFamilyName)
bool SetIconFaceByName(const char *pFamilyName)
{
m_IconFace = GetFaceByName(pFamilyName);
if(!m_IconFace)
{
log_error("textrender", "The icon font face '%s' could not be found", pFamilyName);
return false;
}
return true;
}
void AddFallbackFaceByName(const char *pFamilyName)
bool AddFallbackFaceByName(const char *pFamilyName)
{
FT_Face Face = GetFaceByName(pFamilyName);
if(Face != nullptr && std::find(m_vFallbackFaces.begin(), m_vFallbackFaces.end(), Face) == m_vFallbackFaces.end())
if(!Face)
{
m_vFallbackFaces.push_back(Face);
log_error("textrender", "The fallback font face '%s' could not be found", pFamilyName);
return false;
}
if(std::find(m_vFallbackFaces.begin(), m_vFallbackFaces.end(), Face) != m_vFallbackFaces.end())
{
log_warn("textrender", "The fallback font face '%s' was specified multiple times", pFamilyName);
return true;
}
m_vFallbackFaces.push_back(Face);
return true;
}
void SetVariantFaceByName(const char *pFamilyName)
bool SetVariantFaceByName(const char *pFamilyName)
{
FT_Face Face = GetFaceByName(pFamilyName);
if(m_VariantFace != Face)
{
m_VariantFace = Face;
Clear(); // rebuild atlas after changing variant font
if(!Face && pFamilyName != nullptr)
{
log_error("textrender", "The variant font face '%s' could not be found", pFamilyName);
return false;
}
}
return true;
}
void SetFontPreset(EFontPreset FontPreset)
@ -1002,9 +1030,7 @@ class CTextRender : public IEngineTextRender
FT_Error CollectionLoadError = FT_New_Memory_Face(m_FTLibrary, pFontData, FontDataSize, -1, &FtFace);
if(CollectionLoadError)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "Failed to load font file '%s': %s", pFontName, FT_Error_String(CollectionLoadError));
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf);
log_error("textrender", "Failed to load font file '%s': %s", pFontName, FT_Error_String(CollectionLoadError));
return false;
}
@ -1017,26 +1043,20 @@ class CTextRender : public IEngineTextRender
FT_Error FaceLoadError = FT_New_Memory_Face(m_FTLibrary, pFontData, FontDataSize, FaceIndex, &FtFace);
if(FaceLoadError)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "Failed to load font face %ld from font file '%s': %s", FaceIndex, pFontName, FT_Error_String(FaceLoadError));
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf);
log_error("textrender", "Failed to load font face %ld from font file '%s': %s", FaceIndex, pFontName, FT_Error_String(FaceLoadError));
FT_Done_Face(FtFace);
continue;
}
m_pGlyphMap->AddFace(FtFace);
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "Loaded font face %ld '%s %s' from font file '%s'", FaceIndex, FtFace->family_name, FtFace->style_name, pFontName);
Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "textrender", aBuf);
log_debug("textrender", "Loaded font face %ld '%s %s' from font file '%s'", FaceIndex, FtFace->family_name, FtFace->style_name, pFontName);
LoadedAny = true;
}
if(!LoadedAny)
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "Failed to load font file '%s': no font faces could be loaded", pFontName);
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf);
log_error("textrender", "Failed to load font file '%s': no font faces could be loaded", pFontName);
return false;
}
@ -1083,9 +1103,7 @@ public:
{
int LMajor, LMinor, LPatch;
FT_Library_Version(m_FTLibrary, &LMajor, &LMinor, &LPatch);
char aFreetypeVersion[128];
str_format(aFreetypeVersion, sizeof(aFreetypeVersion), "Freetype version %d.%d.%d (compiled = %d.%d.%d)", LMajor, LMinor, LPatch, FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH);
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aFreetypeVersion);
log_info("textrender", "Freetype version %d.%d.%d (compiled = %d.%d.%d)", LMajor, LMinor, LPatch, FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH);
}
m_FirstFreeTextContainerIndex = -1;
@ -1142,7 +1160,7 @@ public:
m_pStorage = nullptr;
}
void LoadFonts() override
bool LoadFonts() override
{
// read file data into buffer
const char *pFilename = "fonts/index.json";
@ -1150,10 +1168,8 @@ public:
unsigned JsonFileSize;
if(!Storage()->ReadFile(pFilename, IStorage::TYPE_ALL, &pFileData, &JsonFileSize))
{
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "Failed to open/read font index file '%s'", pFilename);
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf);
return;
log_error("textrender", "Failed to open/read font index file '%s'", pFilename);
return false;
}
// parse json data
@ -1163,11 +1179,16 @@ public:
free(pFileData);
if(pJsonData == nullptr)
{
char aBuf[512];
str_format(aBuf, sizeof(aBuf), "Failed to parse font index file '%s': %s", pFilename, aError);
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf);
return;
log_error("textrender", "Failed to parse font index file '%s': %s", pFilename, aError);
return false;
}
if(pJsonData->type != json_object)
{
log_error("textrender", "Font index malformed: root must be an object", pFilename, aError);
return false;
}
bool Success = true;
// extract font file definitions
const json_value &FontFiles = (*pJsonData)["font files"];
@ -1176,7 +1197,11 @@ public:
for(unsigned FontFileIndex = 0; FontFileIndex < FontFiles.u.array.length; ++FontFileIndex)
{
if(FontFiles[FontFileIndex].type != json_string)
{
log_error("textrender", "Font index malformed: 'font files' must be an array of strings (error at index %d)", FontFileIndex);
Success = false;
continue;
}
char aFontName[IO_MAX_PATH_LENGTH];
str_format(aFontName, sizeof(aFontName), "fonts/%s", FontFiles[FontFileIndex].u.string.ptr);
@ -1193,32 +1218,60 @@ public:
free(pFontData);
}
}
else
{
log_error("textrender", "Failed to open/read font file '%s'", aFontName);
Success = false;
}
}
}
else
{
log_error("textrender", "Font index malformed: 'font files' must be an array");
Success = false;
}
// extract default family name
const json_value &DefaultFace = (*pJsonData)["default"];
if(DefaultFace.type == json_string)
{
m_pGlyphMap->SetDefaultFaceByName(DefaultFace.u.string.ptr);
if(!m_pGlyphMap->SetDefaultFaceByName(DefaultFace.u.string.ptr))
{
Success = false;
}
}
else
{
log_error("textrender", "Font index malformed: 'default' must be a string");
Success = false;
}
// extract language variant family names
const json_value &Variants = (*pJsonData)["language variants"];
if(Variants.type == json_object)
{
m_vVariants.resize(Variants.u.object.length);
m_vVariants.reserve(Variants.u.object.length);
for(size_t i = 0; i < Variants.u.object.length; ++i)
{
str_format(m_vVariants[i].m_aLanguageFile, sizeof(m_vVariants[i].m_aLanguageFile), "languages/%s.txt", Variants.u.object.values[i].name);
const json_value *pFamilyName = Variants.u.object.values[i].value;
if(pFamilyName->type == json_string)
str_copy(m_vVariants[i].m_aFamilyName, pFamilyName->u.string.ptr);
else
m_vVariants[i].m_aFamilyName[0] = '\0';
if(pFamilyName->type != json_string)
{
log_error("textrender", "Font index malformed: 'language variants' entries must have string values (error on entry '%s')", Variants.u.object.values[i].name);
Success = false;
continue;
}
SFontLanguageVariant Variant;
str_format(Variant.m_aLanguageFile, sizeof(Variant.m_aLanguageFile), "languages/%s.txt", Variants.u.object.values[i].name);
str_copy(Variant.m_aFamilyName, pFamilyName->u.string.ptr);
m_vVariants.emplace_back(Variant);
}
}
else
{
log_error("textrender", "Font index malformed: 'language variants' must be an array");
Success = false;
}
// extract fallback family names
const json_value &FallbackFaces = (*pJsonData)["fallbacks"];
@ -1226,21 +1279,41 @@ public:
{
for(unsigned i = 0; i < FallbackFaces.u.array.length; ++i)
{
if(FallbackFaces[i].type == json_string)
if(FallbackFaces[i].type != json_string)
{
m_pGlyphMap->AddFallbackFaceByName(FallbackFaces[i].u.string.ptr);
log_error("textrender", "Font index malformed: 'fallbacks' must be an array of strings (error at index %d)", i);
Success = false;
continue;
}
if(!m_pGlyphMap->AddFallbackFaceByName(FallbackFaces[i].u.string.ptr))
{
Success = false;
}
}
}
else
{
log_error("textrender", "Font index malformed: 'fallbacks' must be an array");
Success = false;
}
// extract icon font family name
const json_value &IconFace = (*pJsonData)["icon"];
if(IconFace.type == json_string)
{
m_pGlyphMap->SetIconFaceByName(IconFace.u.string.ptr);
if(!m_pGlyphMap->SetIconFaceByName(IconFace.u.string.ptr))
{
Success = false;
}
}
else
{
log_error("textrender", "Font index malformed: 'icon' must be a string");
Success = false;
}
json_value_free(pJsonData);
return Success;
}
void SetFontPreset(EFontPreset FontPreset) override

View file

@ -47,7 +47,12 @@ public:
unsigned m_NumArgs;
public:
IResult() { m_NumArgs = 0; }
IResult(int ClientId) :
m_NumArgs(0),
m_ClientId(ClientId) {}
IResult(const IResult &Other) :
m_NumArgs(Other.m_NumArgs),
m_ClientId(Other.m_ClientId) {}
virtual ~IResult() {}
virtual int GetInteger(unsigned Index) const = 0;
@ -83,7 +88,6 @@ public:
};
typedef void (*FTeeHistorianCommandCallback)(int ClientId, int FlagMask, const char *pCmd, IResult *pResult, void *pUser);
typedef void (*FPrintCallback)(const char *pStr, void *pUser, ColorRGBA PrintColor);
typedef void (*FPossibleCallback)(int Index, const char *pCmd, void *pUser);
typedef void (*FCommandCallback)(IResult *pResult, void *pUserData);
typedef void (*FChainCommandCallback)(IResult *pResult, void *pUserData, FCommandCallback pfnCallback, void *pCallbackUserData);
@ -111,7 +115,12 @@ public:
virtual void ExecuteLineStroked(int Stroke, const char *pStr, int ClientId = -1, bool InterpretSemicolons = true) = 0;
virtual bool ExecuteFile(const char *pFilename, int ClientId = -1, bool LogFailure = false, int StorageType = IStorage::TYPE_ALL) = 0;
virtual char *Format(char *pBuf, int Size, const char *pFrom, const char *pStr) = 0;
/**
* @deprecated Prefer using the `log_*` functions from base/log.h instead of this function for the following reasons:
* - They support `printf`-formatting without a separate buffer.
* - They support all five log levels.
* - They do not require a pointer to `IConsole` to be used.
*/
virtual void Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor = gs_ConsoleDefaultColor) const = 0;
virtual void SetTeeHistorianCommandCallback(FTeeHistorianCommandCallback pfnCallback, void *pUser) = 0;
virtual void SetUnknownCommandCallback(FUnknownCommandCallback pfnCallback, void *pUser) = 0;

View file

@ -21,10 +21,10 @@ class IGhostRecorder : public IInterface
public:
virtual ~IGhostRecorder() {}
virtual int Start(const char *pFilename, const char *pMap, SHA256_DIGEST MapSha256, const char *pName) = 0;
virtual int Stop(int Ticks, int Time) = 0;
virtual int Start(const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, const char *pName) = 0;
virtual void Stop(int Ticks, int Time) = 0;
virtual void WriteData(int Type, const void *pData, int Size) = 0;
virtual void WriteData(int Type, const void *pData, size_t Size) = 0;
virtual bool IsRecording() const = 0;
};
@ -34,15 +34,15 @@ class IGhostLoader : public IInterface
public:
virtual ~IGhostLoader() {}
virtual int Load(const char *pFilename, const char *pMap, SHA256_DIGEST MapSha256, unsigned MapCrc) = 0;
virtual bool Load(const char *pFilename, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc) = 0;
virtual void Close() = 0;
virtual const CGhostInfo *GetInfo() const = 0;
virtual bool ReadNextType(int *pType) = 0;
virtual bool ReadData(int Type, void *pData, int Size) = 0;
virtual bool ReadData(int Type, void *pData, size_t Size) = 0;
virtual bool GetGhostInfo(const char *pFilename, CGhostInfo *pInfo, const char *pMap, SHA256_DIGEST MapSha256, unsigned MapCrc) = 0;
virtual bool GetGhostInfo(const char *pFilename, CGhostInfo *pInfo, const char *pMap, const SHA256_DIGEST &MapSha256, unsigned MapCrc) = 0;
};
#endif

View file

@ -246,6 +246,7 @@ CServer::CServer()
m_SameMapReload = false;
m_ReloadedWhenEmpty = false;
m_aCurrentMap[0] = '\0';
m_pCurrentMapName = m_aCurrentMap;
m_RconClientId = IServer::RCON_CID_SERV;
m_RconAuthLevel = AUTHED_ADMIN;
@ -2541,14 +2542,7 @@ void CServer::PumpNetwork(bool PacketWaiting)
const char *CServer::GetMapName() const
{
// get the name of the map without his path
const char *pMapShortName = &Config()->m_SvMap[0];
for(int i = 0; i < str_length(Config()->m_SvMap) - 1; i++)
{
if(Config()->m_SvMap[i] == '/' || Config()->m_SvMap[i] == '\\')
pMapShortName = &Config()->m_SvMap[i + 1];
}
return pMapShortName;
return m_pCurrentMapName;
}
void CServer::ChangeMap(const char *pMap)
@ -2587,6 +2581,7 @@ int CServer::LoadMap(const char *pMapName)
Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "server", aBufMsg);
str_copy(m_aCurrentMap, pMapName);
m_pCurrentMapName = fs_filename(m_aCurrentMap);
// load complete map into memory for download
{
@ -3398,13 +3393,13 @@ void CServer::DemoRecorder_HandleAutoStart()
char aTimestamp[20];
str_timestamp(aTimestamp, sizeof(aTimestamp));
char aFilename[IO_MAX_PATH_LENGTH];
str_format(aFilename, sizeof(aFilename), "demos/auto/server/%s_%s.demo", m_aCurrentMap, aTimestamp);
str_format(aFilename, sizeof(aFilename), "demos/auto/server/%s_%s.demo", GetMapName(), aTimestamp);
m_aDemoRecorder[RECORDER_AUTO].Start(
Storage(),
m_pConsole,
aFilename,
GameServer()->NetVersion(),
m_aCurrentMap,
GetMapName(),
m_aCurrentMapSha256[MAP_TYPE_SIX],
m_aCurrentMapCrc[MAP_TYPE_SIX],
"server",
@ -3428,7 +3423,7 @@ void CServer::SaveDemo(int ClientId, float Time)
if(IsRecording(ClientId))
{
char aNewFilename[IO_MAX_PATH_LENGTH];
str_format(aNewFilename, sizeof(aNewFilename), "demos/%s_%s_%05.2f.demo", m_aCurrentMap, m_aClients[ClientId].m_aName, Time);
str_format(aNewFilename, sizeof(aNewFilename), "demos/%s_%s_%05.2f.demo", GetMapName(), m_aClients[ClientId].m_aName, Time);
m_aDemoRecorder[ClientId].Stop(IDemoRecorder::EStopMode::KEEP_FILE, aNewFilename);
}
}
@ -3438,13 +3433,13 @@ void CServer::StartRecord(int ClientId)
if(Config()->m_SvPlayerDemoRecord)
{
char aFilename[IO_MAX_PATH_LENGTH];
str_format(aFilename, sizeof(aFilename), "demos/%s_%d_%d_tmp.demo", m_aCurrentMap, m_NetServer.Address().port, ClientId);
str_format(aFilename, sizeof(aFilename), "demos/%s_%d_%d_tmp.demo", GetMapName(), m_NetServer.Address().port, ClientId);
m_aDemoRecorder[ClientId].Start(
Storage(),
Console(),
aFilename,
GameServer()->NetVersion(),
m_aCurrentMap,
GetMapName(),
m_aCurrentMapSha256[MAP_TYPE_SIX],
m_aCurrentMapCrc[MAP_TYPE_SIX],
"server",
@ -3506,7 +3501,7 @@ void CServer::ConRecord(IConsole::IResult *pResult, void *pUser)
pServer->Console(),
aFilename,
pServer->GameServer()->NetVersion(),
pServer->m_aCurrentMap,
pServer->GetMapName(),
pServer->m_aCurrentMapSha256[MAP_TYPE_SIX],
pServer->m_aCurrentMapCrc[MAP_TYPE_SIX],
"server",
@ -3553,8 +3548,7 @@ void CServer::ConShowIps(IConsole::IResult *pResult, void *pUser)
{
char aStr[9];
str_format(aStr, sizeof(aStr), "Value: %d", pServer->m_aClients[pServer->m_RconClientId].m_ShowIps);
char aBuf[32];
pServer->SendRconLine(pServer->m_RconClientId, pServer->Console()->Format(aBuf, sizeof(aBuf), "server", aStr));
pServer->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aStr);
}
}
}

View file

@ -242,6 +242,7 @@ public:
};
char m_aCurrentMap[IO_MAX_PATH_LENGTH];
const char *m_pCurrentMapName;
SHA256_DIGEST m_aCurrentMapSha256[NUM_MAP_TYPES];
unsigned m_aCurrentMapCrc[NUM_MAP_TYPES];
unsigned char *m_apCurrentMapData[NUM_MAP_TYPES];

View file

@ -323,8 +323,8 @@ void CConfigManager::Init()
#undef MACRO_CONFIG_STR
m_pConsole->Register("reset", "s[config-name]", CFGFLAG_SERVER | CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Reset, this, "Reset a config to its default value");
m_pConsole->Register("toggle", "s[config-option] i[value 1] i[value 2]", CFGFLAG_SERVER | CFGFLAG_CLIENT, Con_Toggle, this, "Toggle config value");
m_pConsole->Register("+toggle", "s[config-option] i[value 1] i[value 2]", CFGFLAG_CLIENT, Con_ToggleStroke, this, "Toggle config value via keypress");
m_pConsole->Register("toggle", "s[config-option] s[value 1] s[value 2]", CFGFLAG_SERVER | CFGFLAG_CLIENT, Con_Toggle, this, "Toggle config value");
m_pConsole->Register("+toggle", "s[config-option] s[value 1] s[value 2]", CFGFLAG_CLIENT, Con_ToggleStroke, this, "Toggle config value via keypress");
}
void CConfigManager::Reset(const char *pScriptName)

View file

@ -298,7 +298,6 @@ MACRO_CONFIG_INT(ClSaveSettings, cl_save_settings, 1, 0, 1, CFGFLAG_CLIENT, "Wri
MACRO_CONFIG_INT(ClRefreshRate, cl_refresh_rate, 0, 0, 10000, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Refresh rate for updating the game (in Hz)")
MACRO_CONFIG_INT(ClRefreshRateInactive, cl_refresh_rate_inactive, 120, 0, 10000, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Refresh rate for updating the game when the window is inactive (in Hz)")
MACRO_CONFIG_INT(ClEditor, cl_editor, 0, 0, 1, CFGFLAG_CLIENT, "Open the map editor")
MACRO_CONFIG_INT(ClEditorDilate, cl_editor_dilate, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Automatically dilates embedded images")
MACRO_CONFIG_STR(ClSkinFilterString, cl_skin_filter_string, 25, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Skin filtering string")
MACRO_CONFIG_INT(ClEditorMaxHistory, cl_editor_max_history, 50, 1, 500, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Maximum number of undo actions in the editor history (not shared between editor, envelope editor and server settings editor)")
@ -494,7 +493,7 @@ MACRO_CONFIG_INT(SvSpecFrequency, sv_pause_frequency, 1, 0, 9999, CFGFLAG_SERVER
MACRO_CONFIG_INT(SvInvite, sv_invite, 1, 0, 1, CFGFLAG_SERVER, "Whether players can invite other players to teams")
MACRO_CONFIG_INT(SvInviteFrequency, sv_invite_frequency, 1, 0, 9999, CFGFLAG_SERVER, "The minimum allowed delay between invites")
MACRO_CONFIG_INT(SvTeleOthersAuthLevel, sv_tele_others_auth_level, 1, 1, 3, CFGFLAG_SERVER, "The auth level you need to tele others")
MACRO_CONFIG_INT(SvRegionalRankings, sv_regional_rankings, 1, 0, 1, CFGFLAG_SERVER, "Display regional rankings in /rank and /top5")
MACRO_CONFIG_INT(SvRegionalRankings, sv_regional_rankings, 1, 0, 1, CFGFLAG_SERVER, "Display regional rankings in /rank, /top5 and /top5team")
MACRO_CONFIG_INT(SvEmotionalTees, sv_emotional_tees, 1, -1, 1, CFGFLAG_SERVER, "Whether eye change of tees is enabled with emoticons = 1, not = 0, -1 not at all")
MACRO_CONFIG_INT(SvEmoticonMsDelay, sv_emoticon_ms_delay, 3000, 20, 999999999, CFGFLAG_SERVER, "The time in ms a player has to wait before allowing the next over-head emoticons")

View file

@ -293,15 +293,6 @@ char CConsole::NextParam(const char *&pFormat)
return *pFormat;
}
char *CConsole::Format(char *pBuf, int Size, const char *pFrom, const char *pStr)
{
char aTimeBuf[80];
str_timestamp_format(aTimeBuf, sizeof(aTimeBuf), FORMAT_TIME);
str_format(pBuf, Size, "[%s][%s]: %s", aTimeBuf, pFrom, pStr);
return pBuf;
}
LEVEL IConsole::ToLogLevel(int Level)
{
switch(Level)
@ -385,7 +376,7 @@ bool CConsole::LineIsValid(const char *pStr)
do
{
CResult Result;
CResult Result(-1);
const char *pEnd = pStr;
const char *pNextPart = 0;
int InString = 0;
@ -436,8 +427,7 @@ void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientId, bo
}
while(pStr && *pStr)
{
CResult Result;
Result.m_ClientId = ClientId;
CResult Result(ClientId);
const char *pEnd = pStr;
const char *pNextPart = 0;
int InString = 0;
@ -532,9 +522,7 @@ void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientId, bo
}
else if(m_StoreCommands && pCommand->m_Flags & CFGFLAG_STORE)
{
m_ExecutionQueue.AddEntry();
m_ExecutionQueue.m_pLast->m_pCommand = pCommand;
m_ExecutionQueue.m_pLast->m_Result = Result;
m_vExecutionQueue.emplace_back(pCommand, Result);
}
else
{
@ -763,7 +751,7 @@ void CConsole::ConCommandStatus(IResult *pResult, void *pUser)
void CConsole::ConUserCommandStatus(IResult *pResult, void *pUser)
{
CConsole *pConsole = static_cast<CConsole *>(pUser);
CResult Result;
CResult Result(pResult->m_ClientId);
Result.m_pCommand = "access_status";
char aBuf[4];
str_format(aBuf, sizeof(aBuf), "%d", (int)IConsole::ACCESS_LEVEL_USER);
@ -791,7 +779,6 @@ CConsole::CConsole(int FlagMask)
m_StoreCommands = true;
m_apStrokeStr[0] = "0";
m_apStrokeStr[1] = "1";
m_ExecutionQueue.Reset();
m_pFirstCommand = 0;
m_pFirstExec = 0;
m_pfnTeeHistorianCommandCallback = 0;
@ -1040,9 +1027,11 @@ void CConsole::StoreCommands(bool Store)
{
if(!Store)
{
for(CExecutionQueue::CQueueEntry *pEntry = m_ExecutionQueue.m_pFirst; pEntry; pEntry = pEntry->m_pNext)
pEntry->m_pCommand->m_pfnCallback(&pEntry->m_Result, pEntry->m_pCommand->m_pUserData);
m_ExecutionQueue.Reset();
for(CExecutionQueueEntry &Entry : m_vExecutionQueue)
{
Entry.m_pCommand->m_pfnCallback(&Entry.m_Result, Entry.m_pCommand->m_pUserData);
}
m_vExecutionQueue.clear();
}
m_StoreCommands = Store;
}

View file

@ -84,8 +84,8 @@ class CConsole : public IConsole
const char *m_pCommand;
const char *m_apArgs[MAX_PARTS];
CResult()
CResult(int ClientId) :
IResult(ClientId)
{
mem_zero(m_aStringStorage, sizeof(m_aStringStorage));
m_pArgsStart = 0;
@ -93,18 +93,14 @@ class CConsole : public IConsole
mem_zero(m_apArgs, sizeof(m_apArgs));
}
CResult &operator=(const CResult &Other)
CResult(const CResult &Other) :
IResult(Other)
{
if(this != &Other)
{
IResult::operator=(Other);
mem_copy(m_aStringStorage, Other.m_aStringStorage, sizeof(m_aStringStorage));
m_pArgsStart = m_aStringStorage + (Other.m_pArgsStart - Other.m_aStringStorage);
m_pCommand = m_aStringStorage + (Other.m_pCommand - Other.m_aStringStorage);
for(unsigned i = 0; i < Other.m_NumArgs; ++i)
m_apArgs[i] = m_aStringStorage + (Other.m_apArgs[i] - Other.m_aStringStorage);
}
return *this;
mem_copy(m_aStringStorage, Other.m_aStringStorage, sizeof(m_aStringStorage));
m_pArgsStart = m_aStringStorage + (Other.m_pArgsStart - Other.m_aStringStorage);
m_pCommand = m_aStringStorage + (Other.m_pCommand - Other.m_aStringStorage);
for(unsigned i = 0; i < Other.m_NumArgs; ++i)
m_apArgs[i] = m_aStringStorage + (Other.m_apArgs[i] - Other.m_aStringStorage);
}
void AddArgument(const char *pArg)
@ -163,35 +159,16 @@ class CConsole : public IConsole
*/
char NextParam(const char *&pFormat);
class CExecutionQueue
class CExecutionQueueEntry
{
CHeap m_Queue;
public:
struct CQueueEntry
{
CQueueEntry *m_pNext;
CCommand *m_pCommand;
CResult m_Result;
} * m_pFirst, *m_pLast;
void AddEntry()
{
CQueueEntry *pEntry = m_Queue.Allocate<CQueueEntry>();
pEntry->m_pNext = 0;
if(!m_pFirst)
m_pFirst = pEntry;
if(m_pLast)
m_pLast->m_pNext = pEntry;
m_pLast = pEntry;
(void)new(&(pEntry->m_Result)) CResult;
}
void Reset()
{
m_Queue.Reset();
m_pFirst = m_pLast = 0;
}
} m_ExecutionQueue;
CCommand *m_pCommand;
CResult m_Result;
CExecutionQueueEntry(CCommand *pCommand, CResult Result) :
m_pCommand(pCommand),
m_Result(Result) {}
};
std::vector<CExecutionQueueEntry> m_vExecutionQueue;
void AddCommandSorted(CCommand *pCommand);
CCommand *FindCommand(const char *pName, int FlagMask);
@ -220,7 +197,6 @@ public:
void ExecuteLineFlag(const char *pStr, int FlagMask, int ClientId = -1, bool InterpretSemicolons = true) override;
bool ExecuteFile(const char *pFilename, int ClientId = -1, bool LogFailure = false, int StorageType = IStorage::TYPE_ALL) override;
char *Format(char *pBuf, int Size, const char *pFrom, const char *pStr) override;
void Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor = gs_ConsoleDefaultColor) const override;
void SetTeeHistorianCommandCallback(FTeeHistorianCommandCallback pfnCallback, void *pUser) override;
void SetUnknownCommandCallback(FUnknownCommandCallback pfnCallback, void *pUser) override;

View file

@ -126,6 +126,11 @@ bool CHttpRequest::ConfigureHandle(void *pHandle)
{
curl_easy_setopt(pH, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)m_MaxResponseSize);
}
if(m_IfModifiedSince >= 0)
{
curl_easy_setopt(pH, CURLOPT_TIMEVALUE_LARGE, (curl_off_t)m_IfModifiedSince);
curl_easy_setopt(pH, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE);
}
// CURLOPT_PROTOCOLS is deprecated: since 7.85.0. Use CURLOPT_PROTOCOLS_STR
// Wait until all platforms have 7.85.0
@ -378,7 +383,11 @@ void CHttpRequest::OnCompletionInternal(void *pHandle, unsigned int Result)
// or other threads may try to access the result of a completed HTTP request,
// before the result has been initialized/updated in OnCompletion.
OnCompletion(State);
m_State = State;
{
std::unique_lock WaitLock(m_WaitMutex);
m_State = State;
}
m_WaitCondition.notify_all();
}
void CHttpRequest::WriteToFile(IStorage *pStorage, const char *pDest, int StorageType)
@ -402,18 +411,11 @@ void CHttpRequest::Header(const char *pNameColonValue)
void CHttpRequest::Wait()
{
using namespace std::chrono_literals;
// This is so uncommon that polling just might work
for(;;)
{
std::unique_lock Lock(m_WaitMutex);
m_WaitCondition.wait(Lock, [this]() {
EHttpState State = m_State.load(std::memory_order_seq_cst);
if(State != EHttpState::QUEUED && State != EHttpState::RUNNING)
{
return;
}
std::this_thread::sleep_for(10ms);
}
return State != EHttpState::QUEUED && State != EHttpState::RUNNING;
});
}
void CHttpRequest::Result(unsigned char **ppResult, size_t *pResultLength) const
@ -604,6 +606,10 @@ void CHttp::RunLoop()
goto error_configure;
}
{
std::unique_lock WaitLock(pRequest->m_WaitMutex);
pRequest->m_State = EHttpState::RUNNING;
}
m_RunningRequests.emplace(pEH, std::move(pRequest));
NewRequests.pop_front();
continue;

View file

@ -86,6 +86,7 @@ class CHttpRequest : public IHttpRequest
CTimeout m_Timeout = CTimeout{0, 0, 0, 0};
int64_t m_MaxResponseSize = -1;
int64_t m_IfModifiedSince = -1;
REQUEST m_Type = REQUEST::GET;
SHA256_DIGEST m_ActualSha256 = SHA256_ZEROED;
@ -116,6 +117,8 @@ class CHttpRequest : public IHttpRequest
char m_aErr[256]; // 256 == CURL_ERROR_SIZE
std::atomic<EHttpState> m_State{EHttpState::QUEUED};
std::atomic<bool> m_Abort{false};
std::mutex m_WaitMutex;
std::condition_variable m_WaitCondition;
int m_StatusCode = 0;
bool m_HeadersEnded = false;
@ -150,6 +153,7 @@ public:
void Timeout(CTimeout Timeout) { m_Timeout = Timeout; }
void MaxResponseSize(int64_t MaxResponseSize) { m_MaxResponseSize = MaxResponseSize; }
void IfModifiedSince(int64_t IfModifiedSince) { m_IfModifiedSince = IfModifiedSince; }
void LogProgress(HTTPLOG LogProgress) { m_LogProgress = LogProgress; }
void IpResolve(IPRESOLVE IpResolve) { m_IpResolve = IpResolve; }
void FailOnErrorStatus(bool FailOnErrorStatus) { m_FailOnErrorStatus = FailOnErrorStatus; }

View file

@ -574,6 +574,14 @@ public:
return pResult;
}
bool RetrieveTimes(const char *pFilename, int Type, time_t *pCreated, time_t *pModified) override
{
dbg_assert(Type == TYPE_ABSOLUTE || (Type >= TYPE_SAVE && Type < m_NumPaths), "Type invalid");
char aBuffer[IO_MAX_PATH_LENGTH];
return fs_file_time(GetPath(Type, pFilename, aBuffer, sizeof(aBuffer)), pCreated, pModified) == 0;
}
bool CalculateHashes(const char *pFilename, int Type, SHA256_DIGEST *pSha256, unsigned *pCrc) override
{
dbg_assert(pSha256 != nullptr || pCrc != nullptr, "At least one output argument required");

View file

@ -54,6 +54,7 @@ public:
virtual bool FolderExists(const char *pFilename, int Type) = 0;
virtual bool ReadFile(const char *pFilename, int Type, void **ppResult, unsigned *pResultLen) = 0;
virtual char *ReadFileStr(const char *pFilename, int Type) = 0;
virtual bool RetrieveTimes(const char *pFilename, int Type, time_t *pCreated, time_t *pModified) = 0;
virtual bool CalculateHashes(const char *pFilename, int Type, SHA256_DIGEST *pSha256, unsigned *pCrc = nullptr) = 0;
virtual bool FindFile(const char *pFilename, const char *pPath, int Type, char *pBuffer, int BufferSize) = 0;
virtual size_t FindFiles(const char *pFilename, const char *pPath, int Type, std::set<std::string> *pEntries) = 0;

View file

@ -316,7 +316,7 @@ public:
virtual void MoveCursor(CTextCursor *pCursor, float x, float y) const = 0;
virtual void SetCursorPosition(CTextCursor *pCursor, float x, float y) const = 0;
virtual void LoadFonts() = 0;
virtual bool LoadFonts() = 0;
virtual void SetFontPreset(EFontPreset FontPreset) = 0;
virtual void SetFontLanguageVariant(const char *pLanguageFile) = 0;

View file

@ -73,7 +73,7 @@ void CBackground::LoadBackground()
}
else if(m_pMap->Load(aBuf))
{
m_pLayers->InitBackground(m_pMap);
m_pLayers->Init(m_pMap, true);
NeedImageLoading = true;
m_Loaded = true;
}

View file

@ -95,12 +95,7 @@ void CChat::RebuildChat()
}
}
void CChat::OnWindowResize()
{
RebuildChat();
}
void CChat::Reset()
void CChat::ClearLines()
{
for(auto &Line : m_aLines)
{
@ -115,6 +110,16 @@ void CChat::Reset()
}
m_PrevScoreBoardShowed = false;
m_PrevShowChat = false;
}
void CChat::OnWindowResize()
{
RebuildChat();
}
void CChat::Reset()
{
ClearLines();
m_Show = false;
m_CompletionUsed = false;
@ -183,6 +188,11 @@ void CChat::ConEcho(IConsole::IResult *pResult, void *pUserData)
((CChat *)pUserData)->Echo(pResult->GetString(0));
}
void CChat::ConClearChat(IConsole::IResult *pResult, void *pUserData)
{
((CChat *)pUserData)->ClearLines();
}
void CChat::ConchainChatOld(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
{
pfnCallback(pResult, pCallbackUserData);
@ -217,6 +227,7 @@ void CChat::OnConsoleInit()
Console()->Register("chat", "s['team'|'all'] ?r[message]", CFGFLAG_CLIENT, ConChat, this, "Enable chat with all/team mode");
Console()->Register("+show_chat", "", CFGFLAG_CLIENT, ConShowChat, this, "Show chat");
Console()->Register("echo", "r[message]", CFGFLAG_CLIENT | CFGFLAG_STORE, ConEcho, this, "Echo the text in chat window");
Console()->Register("clear_chat", "", CFGFLAG_CLIENT | CFGFLAG_STORE, ConClearChat, this, "Clear chat messages");
}
void CChat::OnInit()
@ -1194,7 +1205,7 @@ void CChat::OnRender()
{
if(str_startswith_nocase(Command.m_aName, m_Input.GetString() + 1))
{
Cursor.m_X = m_Input.GetCaretPosition().x;
Cursor.m_X = Cursor.m_X + TextRender()->TextWidth(Cursor.m_FontSize, m_Input.GetString(), -1, Cursor.m_LineWidth);
Cursor.m_Y = m_Input.GetCaretPosition().y;
TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.5f);
TextRender()->TextEx(&Cursor, Command.m_aName + str_length(m_Input.GetString() + 1));

View file

@ -135,6 +135,7 @@ class CChat : public CComponent
static void ConChat(IConsole::IResult *pResult, void *pUserData);
static void ConShowChat(IConsole::IResult *pResult, void *pUserData);
static void ConEcho(IConsole::IResult *pResult, void *pUserData);
static void ConClearChat(IConsole::IResult *pResult, void *pUserData);
static void ConchainChatOld(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
static void ConchainChatFontSize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
@ -170,6 +171,7 @@ public:
void OnInit() override;
void RebuildChat();
void ClearLines();
void EnsureCoherentFontSize() const;
void EnsureCoherentWidth() const;

View file

@ -1,22 +1,22 @@
/* (c) Rajh, Redix and Sushi. */
#include "ghost.h"
#include <base/log.h>
#include <engine/ghost.h>
#include <engine/shared/config.h>
#include <engine/storage.h>
#include <game/client/race.h>
#include "ghost.h"
#include "menus.h"
#include "players.h"
#include "skins.h"
#include <game/client/components/menus.h>
#include <game/client/components/players.h>
#include <game/client/components/skins.h>
#include <game/client/gameclient.h>
#include <game/client/race.h>
const char *CGhost::ms_pGhostDir = "ghosts";
CGhost::CGhost() :
m_NewRenderTick(-1), m_StartRenderTick(-1), m_LastDeathTick(-1), m_LastRaceTick(-1), m_Recording(false), m_Rendering(false) {}
static const LOG_COLOR LOG_COLOR_GHOST{165, 153, 153};
void CGhost::GetGhostSkin(CGhostSkin *pSkin, const char *pSkinName, int UseCustomColor, int ColorBody, int ColorFeet)
{
@ -187,13 +187,13 @@ void CGhost::CheckStart()
int RaceTick = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer;
int RenderTick = m_NewRenderTick;
if(m_LastRaceTick != RaceTick && Client()->GameTick(g_Config.m_ClDummy) - RaceTick < Client()->GameTickSpeed())
if(GameClient()->LastRaceTick() != RaceTick && Client()->GameTick(g_Config.m_ClDummy) - RaceTick < Client()->GameTickSpeed())
{
if(m_Rendering && m_RenderingStartedByServer) // race restarted: stop rendering
StopRender();
if(m_Recording && m_LastRaceTick != -1) // race restarted: activate restarting for local start detection so we have a smooth transition
if(m_Recording && GameClient()->LastRaceTick() != -1) // race restarted: activate restarting for local start detection so we have a smooth transition
m_AllowRestart = true;
if(m_LastRaceTick == -1) // no restart: reset rendering preparations
if(GameClient()->LastRaceTick() == -1) // no restart: reset rendering preparations
m_NewRenderTick = -1;
if(GhostRecorder()->IsRecording()) // race restarted: stop recording
GhostRecorder()->Stop(0, -1);
@ -271,28 +271,21 @@ void CGhost::TryRenderStart(int Tick, bool ServerControl)
void CGhost::OnNewSnapshot()
{
if(!GameClient()->m_GameInfo.m_Race || Client()->State() != IClient::STATE_ONLINE)
if(!GameClient()->m_GameInfo.m_Race || !g_Config.m_ClRaceGhost || Client()->State() != IClient::STATE_ONLINE)
return;
if(!m_pClient->m_Snap.m_pGameInfoObj || m_pClient->m_Snap.m_SpecInfo.m_Active || !m_pClient->m_Snap.m_pLocalCharacter || !m_pClient->m_Snap.m_pLocalPrevCharacter)
return;
bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
const bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
const bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
if(g_Config.m_ClRaceGhost)
{
if(!ServerControl)
CheckStartLocal(false);
else
CheckStart();
if(!ServerControl)
CheckStartLocal(false);
else
CheckStart();
if(m_Recording)
AddInfos(m_pClient->m_Snap.m_pLocalCharacter, (m_pClient->m_Snap.m_LocalClientId != -1 && m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_HasExtendedData) ? &m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_ExtendedData : nullptr);
}
// Record m_LastRaceTick for g_Config.m_ClConfirmDisconnect/QuitTime anyway
int RaceTick = -m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer;
m_LastRaceTick = RaceFlag ? RaceTick : -1;
if(m_Recording)
AddInfos(m_pClient->m_Snap.m_pLocalCharacter, (m_pClient->m_Snap.m_LocalClientId != -1 && m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_HasExtendedData) ? &m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_ExtendedData : nullptr);
}
void CGhost::OnNewPredictedSnapshot()
@ -302,8 +295,8 @@ void CGhost::OnNewPredictedSnapshot()
if(!m_pClient->m_Snap.m_pGameInfoObj || m_pClient->m_Snap.m_SpecInfo.m_Active || !m_pClient->m_Snap.m_pLocalCharacter || !m_pClient->m_Snap.m_pLocalPrevCharacter)
return;
bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
const bool RaceFlag = m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
const bool ServerControl = RaceFlag && g_Config.m_ClRaceGhostServerControl;
if(!ServerControl)
CheckStartLocal(true);
@ -388,20 +381,8 @@ void CGhost::InitRenderInfos(CGhostItem *pGhost)
char aSkinName[MAX_SKIN_LENGTH];
IntsToStr(&pGhost->m_Skin.m_Skin0, 6, aSkinName, std::size(aSkinName));
CTeeRenderInfo *pRenderInfo = &pGhost->m_RenderInfo;
pRenderInfo->Apply(m_pClient->m_Skins.Find(aSkinName));
pRenderInfo->m_CustomColoredSkin = pGhost->m_Skin.m_UseCustomColor;
if(pGhost->m_Skin.m_UseCustomColor)
{
pRenderInfo->m_ColorBody = color_cast<ColorRGBA>(ColorHSLA(pGhost->m_Skin.m_ColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT));
pRenderInfo->m_ColorFeet = color_cast<ColorRGBA>(ColorHSLA(pGhost->m_Skin.m_ColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT));
}
else
{
pRenderInfo->m_ColorBody = ColorRGBA(1, 1, 1);
pRenderInfo->m_ColorFeet = ColorRGBA(1, 1, 1);
}
pRenderInfo->ApplyColors(pGhost->m_Skin.m_UseCustomColor, pGhost->m_Skin.m_ColorBody, pGhost->m_Skin.m_ColorFeet);
pRenderInfo->m_Size = 64;
}
@ -422,11 +403,13 @@ void CGhost::StopRecord(int Time)
m_Recording = false;
bool RecordingToFile = GhostRecorder()->IsRecording();
if(RecordingToFile)
GhostRecorder()->Stop(m_CurGhost.m_Path.Size(), Time);
CMenus::CGhostItem *pOwnGhost = m_pClient->m_Menus.GetOwnGhost();
if(Time > 0 && (!pOwnGhost || Time < pOwnGhost->m_Time || !g_Config.m_ClRaceGhostSaveBest))
const bool StoreGhost = Time > 0 && (!pOwnGhost || Time < pOwnGhost->m_Time || !g_Config.m_ClRaceGhostSaveBest);
if(RecordingToFile)
GhostRecorder()->Stop(m_CurGhost.m_Path.Size(), StoreGhost ? Time : -1);
if(StoreGhost)
{
// add to active ghosts
int Slot = GetSlot();
@ -451,11 +434,8 @@ void CGhost::StopRecord(int Time)
// add item to menu list
m_pClient->m_Menus.UpdateOwnGhost(Item);
}
else if(RecordingToFile) // no new record
Storage()->RemoveFile(m_aTmpFilename, IStorage::TYPE_SAVE);
m_aTmpFilename[0] = 0;
m_aTmpFilename[0] = '\0';
m_CurGhost.Reset();
}
@ -479,18 +459,11 @@ int CGhost::Load(const char *pFilename)
if(Slot == -1)
return -1;
if(GhostLoader()->Load(pFilename, Client()->GetCurrentMap(), Client()->GetCurrentMapSha256(), Client()->GetCurrentMapCrc()) != 0)
if(!GhostLoader()->Load(pFilename, Client()->GetCurrentMap(), Client()->GetCurrentMapSha256(), Client()->GetCurrentMapCrc()))
return -1;
const CGhostInfo *pInfo = GhostLoader()->GetInfo();
if(pInfo->m_NumTicks <= 0 || pInfo->m_Time <= 0)
{
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "invalid header info");
GhostLoader()->Close();
return -1;
}
// select ghost
CGhostItem *pGhost = &m_aActiveGhosts[Slot];
pGhost->Reset();
@ -540,7 +513,7 @@ int CGhost::Load(const char *pFilename)
if(Error || Index != pInfo->m_NumTicks)
{
Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "invalid ghost data");
log_error_color(LOG_COLOR_GHOST, "ghost", "Failed to read all ghost data (error='%d', got '%d' ticks, wanted '%d' ticks)", Error, Index, pInfo->m_NumTicks);
pGhost->Reset();
return -1;
}
@ -659,7 +632,6 @@ void CGhost::OnReset()
StopRecord();
StopRender();
m_LastDeathTick = -1;
m_LastRaceTick = -1;
}
void CGhost::OnShutdown()
@ -675,11 +647,6 @@ void CGhost::OnMapLoad()
m_AllowRestart = false;
}
int CGhost::GetLastRaceTick() const
{
return m_LastRaceTick;
}
void CGhost::OnRefreshSkins()
{
const auto &&RefindSkin = [&](auto &Ghost) {
@ -687,16 +654,7 @@ void CGhost::OnRefreshSkins()
return;
char aSkinName[MAX_SKIN_LENGTH];
IntsToStr(&Ghost.m_Skin.m_Skin0, 6, aSkinName, std::size(aSkinName));
CTeeRenderInfo *pRenderInfo = &Ghost.m_RenderInfo;
if(aSkinName[0] != '\0')
{
pRenderInfo->Apply(m_pClient->m_Skins.Find(aSkinName));
}
else
{
pRenderInfo->m_OriginalRenderSkin.Reset();
pRenderInfo->m_ColorableRenderSkin.Reset();
}
Ghost.m_RenderInfo.Apply(m_pClient->m_Skins.Find(aSkinName));
};
for(auto &Ghost : m_aActiveGhosts)

View file

@ -113,16 +113,14 @@ private:
CGhostItem m_aActiveGhosts[MAX_ACTIVE_GHOSTS];
CGhostItem m_CurGhost;
char m_aTmpFilename[128];
char m_aTmpFilename[IO_MAX_PATH_LENGTH];
int m_NewRenderTick;
int m_StartRenderTick;
int m_LastDeathTick;
int m_LastRaceTick;
bool m_Recording;
bool m_Rendering;
bool m_RenderingStartedByServer;
int m_NewRenderTick = -1;
int m_StartRenderTick = -1;
int m_LastDeathTick = -1;
bool m_Recording = false;
bool m_Rendering = false;
bool m_RenderingStartedByServer = false;
static void GetGhostSkin(CGhostSkin *pSkin, const char *pSkinName, int UseCustomColor, int ColorBody, int ColorFeet);
static void GetGhostCharacter(CGhostCharacter *pGhostChar, const CNetObj_Character *pChar, const CNetObj_DDNetCharacter *pDDnetChar);
@ -149,7 +147,6 @@ private:
public:
bool m_AllowRestart;
CGhost();
virtual int Sizeof() const override { return sizeof(*this); }
virtual void OnRender() override;
@ -174,8 +171,6 @@ public:
class IGhostLoader *GhostLoader() const { return m_pGhostLoader; }
class IGhostRecorder *GhostRecorder() const { return m_pGhostRecorder; }
int GetLastRaceTick() const;
};
#endif

View file

@ -357,13 +357,8 @@ void CMapImages::ChangeEntitiesPath(const char *pPath)
{
for(int LayerType = 0; LayerType < MAP_IMAGE_ENTITY_LAYER_TYPE_COUNT; ++LayerType)
{
if(m_aaEntitiesTextures[ModType][LayerType].IsValid())
{
Graphics()->UnloadTexture(&(m_aaEntitiesTextures[ModType][LayerType]));
}
m_aaEntitiesTextures[ModType][LayerType] = IGraphics::CTextureHandle();
Graphics()->UnloadTexture(&m_aaEntitiesTextures[ModType][LayerType]);
}
m_aEntitiesIsLoaded[ModType] = false;
}
}
@ -383,10 +378,6 @@ void CMapImages::SetTextureScale(int Scale)
Graphics()->UnloadTexture(&m_OverlayTopTexture);
Graphics()->UnloadTexture(&m_OverlayCenterTexture);
m_OverlayBottomTexture = IGraphics::CTextureHandle();
m_OverlayTopTexture = IGraphics::CTextureHandle();
m_OverlayCenterTexture = IGraphics::CTextureHandle();
InitOverlayTextures();
}
}

View file

@ -24,13 +24,9 @@
using namespace std::chrono_literals;
CMapLayers::CMapLayers(int t, bool OnlineOnly)
CMapLayers::CMapLayers(int Type, bool OnlineOnly)
{
m_Type = t;
m_pLayers = 0;
m_CurrentLocalTick = 0;
m_LastLocalTick = 0;
m_EnvelopeUpdate = false;
m_Type = Type;
m_OnlineOnly = OnlineOnly;
}
@ -45,17 +41,6 @@ CCamera *CMapLayers::GetCurCamera()
return &m_pClient->m_Camera;
}
void CMapLayers::EnvelopeUpdate()
{
if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
{
const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
m_CurrentLocalTick = pInfo->m_CurrentTick;
m_LastLocalTick = pInfo->m_CurrentTick;
m_EnvelopeUpdate = true;
}
}
void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser)
{
CMapLayers *pThis = (CMapLayers *)pUser;
@ -75,73 +60,31 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result,
if(EnvelopePoints.NumPoints() == 0)
return;
const auto TickToNanoSeconds = std::chrono::nanoseconds(1s) / (int64_t)pThis->Client()->GameTickSpeed();
static std::chrono::nanoseconds s_Time{0};
static auto s_LastLocalTime = time_get_nanoseconds();
if(pThis->Client()->State() == IClient::STATE_DEMOPLAYBACK)
if(pThis->m_OnlineOnly && (pItem->m_Version < 2 || pItem->m_Synchronized))
{
const IDemoPlayer::CInfo *pInfo = pThis->DemoPlayer()->BaseInfo();
if(!pInfo->m_Paused || pThis->m_EnvelopeUpdate)
if(pThis->m_pClient->m_Snap.m_pGameInfoObj)
{
if(pThis->m_CurrentLocalTick != pInfo->m_CurrentTick)
{
pThis->m_LastLocalTick = pThis->m_CurrentLocalTick;
pThis->m_CurrentLocalTick = pInfo->m_CurrentTick;
}
if(pItem->m_Version < 2 || pItem->m_Synchronized)
{
if(pThis->m_pClient->m_Snap.m_pGameInfoObj)
{
// get the lerp of the current tick and prev
int MinTick = pThis->Client()->PrevGameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick;
int CurTick = pThis->Client()->GameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick;
s_Time = std::chrono::nanoseconds((int64_t)(mix<double>(
0,
(CurTick - MinTick),
(double)pThis->Client()->IntraGameTick(g_Config.m_ClDummy)) *
TickToNanoSeconds.count())) +
MinTick * TickToNanoSeconds;
}
}
else
{
int MinTick = pThis->m_LastLocalTick;
s_Time = std::chrono::nanoseconds((int64_t)(mix<double>(0,
pThis->m_CurrentLocalTick - MinTick,
(double)pThis->Client()->IntraGameTick(g_Config.m_ClDummy)) *
TickToNanoSeconds.count())) +
MinTick * TickToNanoSeconds;
}
// get the lerp of the current tick and prev
const auto TickToNanoSeconds = std::chrono::nanoseconds(1s) / (int64_t)pThis->Client()->GameTickSpeed();
const int MinTick = pThis->Client()->PrevGameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick;
const int CurTick = pThis->Client()->GameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick;
s_Time = std::chrono::nanoseconds((int64_t)(mix<double>(
0,
(CurTick - MinTick),
(double)pThis->Client()->IntraGameTick(g_Config.m_ClDummy)) *
TickToNanoSeconds.count())) +
MinTick * TickToNanoSeconds;
}
CRenderTools::RenderEvalEnvelope(&EnvelopePoints, s_Time + (int64_t)TimeOffsetMillis * std::chrono::nanoseconds(1ms), Result, Channels);
}
else
{
if(pThis->m_OnlineOnly && (pItem->m_Version < 2 || pItem->m_Synchronized))
{
if(pThis->m_pClient->m_Snap.m_pGameInfoObj) // && !(pThis->m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags&GAMESTATEFLAG_PAUSED))
{
// get the lerp of the current tick and prev
int MinTick = pThis->Client()->PrevGameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick;
int CurTick = pThis->Client()->GameTick(g_Config.m_ClDummy) - pThis->m_pClient->m_Snap.m_pGameInfoObj->m_RoundStartTick;
s_Time = std::chrono::nanoseconds((int64_t)(mix<double>(
0,
(CurTick - MinTick),
(double)pThis->Client()->IntraGameTick(g_Config.m_ClDummy)) *
TickToNanoSeconds.count())) +
MinTick * TickToNanoSeconds;
}
}
else
{
auto CurTime = time_get_nanoseconds();
s_Time += CurTime - s_LastLocalTime;
s_LastLocalTime = CurTime;
}
CRenderTools::RenderEvalEnvelope(&EnvelopePoints, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Result, Channels);
const auto CurTime = time_get_nanoseconds();
s_Time += CurTime - s_LastLocalTime;
s_LastLocalTime = CurTime;
}
CRenderTools::RenderEvalEnvelope(&EnvelopePoints, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Result, Channels);
}
void FillTmpTile(SGraphicTile *pTmpTile, SGraphicTileTexureCoords *pTmpTex, unsigned char Flags, unsigned char Index, int x, int y, const ivec2 &Offset, int Scale, CMapItemGroup *pGroup)

View file

@ -30,11 +30,8 @@ class CMapLayers : public CComponent
CLayers *m_pLayers;
CMapImages *m_pImages;
int m_Type;
int m_CurrentLocalTick;
int m_LastLocalTick;
bool m_EnvelopeUpdate;
int m_Type;
bool m_OnlineOnly;
struct STileLayerVisuals
@ -157,8 +154,6 @@ public:
void RenderKillTileBorder(int LayerIndex, const ColorRGBA &Color, CMapItemLayerTilemap *pTileLayer, CMapItemGroup *pGroup);
void RenderQuadLayer(int LayerIndex, CMapItemLayerQuads *pQuadLayer, CMapItemGroup *pGroup, bool ForceRender = false);
void EnvelopeUpdate();
static void EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser);
};

View file

@ -254,7 +254,7 @@ void CMenuBackground::LoadMenuBackground(bool HasDayHint, bool HasNightHint)
if(m_Loaded)
{
m_pLayers->InitBackground(m_pMap);
m_pLayers->Init(m_pMap, true);
CMapLayers::OnMapLoad();
m_pImages->LoadBackground(m_pLayers, m_pMap);

View file

@ -548,7 +548,7 @@ void CMenus::RenderMenubar(CUIRect Box, IClient::EClientState ClientState)
ColorRGBA QuitColor(1, 0, 0, 0.5f);
if(DoButton_MenuTab(&s_QuitButton, FONT_ICON_POWER_OFF, 0, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_QUIT], nullptr, nullptr, &QuitColor, 10.0f))
{
if(m_pClient->Editor()->HasUnsavedData() || (Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0))
if(m_pClient->Editor()->HasUnsavedData() || (GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0))
{
m_Popup = POPUP_QUIT;
}
@ -909,6 +909,11 @@ void CMenus::OnInit()
// load community icons
m_vCommunityIcons.clear();
Storage()->ListDirectory(IStorage::TYPE_ALL, "communityicons", CommunityIconScan, this);
// Quad for the direction arrows above the player
m_DirectionQuadContainerIndex = Graphics()->CreateQuadContainer(false);
RenderTools()->QuadContainerAddSprite(m_DirectionQuadContainerIndex, 0.f, 0.f, 22.f);
Graphics()->QuadContainerUpload(m_DirectionQuadContainerIndex);
}
void CMenus::OnConsoleInit()

View file

@ -95,6 +95,9 @@ class CMenus : public CComponent
void DoJoystickBar(const CUIRect *pRect, float Current, float Tolerance, bool Active);
bool m_SkinListNeedsUpdate = false;
bool m_SkinListScrollToSelected = false;
int m_DirectionQuadContainerIndex;
// menus_settings_assets.cpp
public:

View file

@ -637,7 +637,7 @@ void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItem
void CMenus::Connect(const char *pAddress)
{
if(Client()->State() == IClient::STATE_ONLINE && Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
if(Client()->State() == IClient::STATE_ONLINE && GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
{
str_copy(m_aNextServer, pAddress);
PopupConfirm(Localize("Disconnect"), Localize("Are you sure that you want to disconnect and switch to a different server?"), Localize("Yes"), Localize("No"), &CMenus::PopupConfirmSwitchServer);
@ -1846,17 +1846,7 @@ CTeeRenderInfo CMenus::GetTeeRenderInfo(vec2 Size, const char *pSkinName, bool C
{
CTeeRenderInfo TeeInfo;
TeeInfo.Apply(m_pClient->m_Skins.Find(pSkinName));
TeeInfo.m_CustomColoredSkin = CustomSkinColors;
if(CustomSkinColors)
{
TeeInfo.m_ColorBody = color_cast<ColorRGBA>(ColorHSLA(CustomSkinColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT));
TeeInfo.m_ColorFeet = color_cast<ColorRGBA>(ColorHSLA(CustomSkinColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT));
}
else
{
TeeInfo.m_ColorBody = ColorRGBA(1.0f, 1.0f, 1.0f);
TeeInfo.m_ColorFeet = ColorRGBA(1.0f, 1.0f, 1.0f);
}
TeeInfo.ApplyColors(CustomSkinColors, CustomSkinColorBody, CustomSkinColorFeet);
TeeInfo.m_Size = minimum(Size.x, Size.y);
return TeeInfo;
}

View file

@ -90,8 +90,6 @@ void CMenus::HandleDemoSeeking(float PositionToSeek, float TimeToSeek)
else
DemoPlayer()->SeekPercent(PositionToSeek);
m_pClient->m_SuppressEvents = false;
m_pClient->m_MapLayersBackground.EnvelopeUpdate();
m_pClient->m_MapLayersForeground.EnvelopeUpdate();
if(!DemoPlayer()->BaseInfo()->m_Paused && PositionToSeek == 1.0f)
DemoPlayer()->Pause();
}
@ -103,8 +101,6 @@ void CMenus::DemoSeekTick(IDemoPlayer::ETickOffset TickOffset)
DemoPlayer()->SeekTick(TickOffset);
m_pClient->m_SuppressEvents = false;
DemoPlayer()->Pause();
m_pClient->m_MapLayersBackground.EnvelopeUpdate();
m_pClient->m_MapLayersForeground.EnvelopeUpdate();
}
void CMenus::RenderDemoPlayer(CUIRect MainView)

View file

@ -58,7 +58,7 @@ void CMenus::RenderGame(CUIRect MainView)
static CButtonContainer s_DisconnectButton;
if(DoButton_Menu(&s_DisconnectButton, Localize("Disconnect"), 0, &Button))
{
if(Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
if(GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
{
PopupConfirm(Localize("Disconnect"), Localize("Are you sure that you want to disconnect?"), Localize("Yes"), Localize("No"), &CMenus::PopupConfirmDisconnect);
}
@ -95,7 +95,7 @@ void CMenus::RenderGame(CUIRect MainView)
}
else
{
if(Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
if(GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
{
PopupConfirm(Localize("Disconnect Dummy"), Localize("Are you sure that you want to disconnect your dummy?"), Localize("Yes"), Localize("No"), &CMenus::PopupConfirmDisconnectDummy);
}

View file

@ -459,12 +459,14 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
if(DoButton_MenuTab(&s_PlayerTabButton, Localize("Player"), !m_Dummy, &PlayerTab, IGraphics::CORNER_L, nullptr, nullptr, nullptr, nullptr, 4.0f))
{
m_Dummy = false;
m_SkinListScrollToSelected = true;
}
static CButtonContainer s_DummyTabButton;
if(DoButton_MenuTab(&s_DummyTabButton, Localize("Dummy"), m_Dummy, &DummyTab, IGraphics::CORNER_R, nullptr, nullptr, nullptr, nullptr, 4.0f))
{
m_Dummy = true;
m_SkinListScrollToSelected = true;
}
if(Client()->State() == IClient::STATE_ONLINE && m_pClient->m_NextChangeInfo && m_pClient->m_NextChangeInfo > Client()->GameTick(g_Config.m_ClDummy))
@ -593,17 +595,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
// which invalidates the skin.
CTeeRenderInfo OwnSkinInfo;
OwnSkinInfo.Apply(m_pClient->m_Skins.Find(pSkinName));
OwnSkinInfo.m_CustomColoredSkin = *pUseCustomColor;
if(*pUseCustomColor)
{
OwnSkinInfo.m_ColorBody = color_cast<ColorRGBA>(ColorHSLA(*pColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT));
OwnSkinInfo.m_ColorFeet = color_cast<ColorRGBA>(ColorHSLA(*pColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT));
}
else
{
OwnSkinInfo.m_ColorBody = ColorRGBA(1.0f, 1.0f, 1.0f);
OwnSkinInfo.m_ColorFeet = ColorRGBA(1.0f, 1.0f, 1.0f);
}
OwnSkinInfo.ApplyColors(*pUseCustomColor, *pColorBody, *pColorFeet);
OwnSkinInfo.m_Size = 50.0f;
// Tee
@ -621,6 +613,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
if(Ui()->DoClearableEditBox(&s_SkinInput, &Button, 14.0f))
{
SetNeedSendInfo();
m_SkinListScrollToSelected = true;
}
// Random skin button
@ -717,21 +710,16 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
static std::vector<CUISkin> s_vSkinList;
static std::vector<CUISkin> s_vSkinListHelper;
static std::vector<CUISkin> s_vFavoriteSkinListHelper;
static int s_SkinCount = 0;
static CListBox s_ListBox;
// be nice to the CPU
static auto s_SkinLastRebuildTime = time_get_nanoseconds();
const auto CurTime = time_get_nanoseconds();
if(m_SkinListNeedsUpdate || m_pClient->m_Skins.Num() != s_SkinCount || m_SkinFavoritesChanged || (m_pClient->m_Skins.IsDownloadingSkins() && (CurTime - s_SkinLastRebuildTime > 500ms)))
static std::chrono::nanoseconds s_SkinLastRefreshTime = m_pClient->m_Skins.LastRefreshTime();
if(m_SkinListNeedsUpdate || m_SkinFavoritesChanged || s_SkinLastRefreshTime != m_pClient->m_Skins.LastRefreshTime())
{
s_SkinLastRebuildTime = CurTime;
s_SkinLastRefreshTime = m_pClient->m_Skins.LastRefreshTime();
s_vSkinList.clear();
s_vSkinListHelper.clear();
s_vFavoriteSkinListHelper.clear();
// set skin count early, since Find of the skin class might load
// a downloading skin
s_SkinCount = m_pClient->m_Skins.Num();
m_SkinFavoritesChanged = false;
auto &&SkinNotFiltered = [&](const CSkin *pSkinToBeSelected) {
@ -777,7 +765,14 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
{
const CSkin *pSkinToBeDraw = s_vSkinList[i].m_pSkin;
if(str_comp(pSkinToBeDraw->GetName(), pSkinName) == 0)
{
OldSelected = i;
if(m_SkinListScrollToSelected)
{
s_ListBox.ScrollToSelected();
m_SkinListScrollToSelected = false;
}
}
const CListboxItem Item = s_ListBox.DoNextItem(pSkinToBeDraw, OldSelected >= 0 && (size_t)OldSelected == i);
if(!Item.m_Visible)
@ -786,10 +781,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
Item.m_Rect.VSplitLeft(60.0f, &Button, &Label);
CTeeRenderInfo Info = OwnSkinInfo;
Info.m_CustomColoredSkin = *pUseCustomColor;
Info.m_OriginalRenderSkin = pSkinToBeDraw->m_OriginalSkin;
Info.m_ColorableRenderSkin = pSkinToBeDraw->m_ColorableSkin;
Info.m_SkinMetrics = pSkinToBeDraw->m_Metrics;
Info.Apply(pSkinToBeDraw);
vec2 OffsetToMid;
CRenderTools::GetRenderTeeOffsetToRenderedTee(CAnimState::GetIdle(), &Info, OffsetToMid);
@ -2514,7 +2506,6 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView)
static std::vector<SPreviewLine> s_vLines;
const auto *pDefaultSkin = GameClient()->m_Skins.Find("default");
enum ELineFlag
{
FLAG_TEAM = 1 << 0,
@ -2552,15 +2543,11 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView)
str_copy(pLine->m_aName, pName);
str_copy(pLine->m_aText, pText);
};
auto &&SetLineSkin = [RealTeeSize, &pDefaultSkin](int Index, const CSkin *pSkin) {
auto &&SetLineSkin = [RealTeeSize](int Index, const CSkin *pSkin) {
if(Index >= (int)s_vLines.size())
return;
s_vLines[Index].m_RenderInfo.m_Size = RealTeeSize;
s_vLines[Index].m_RenderInfo.m_CustomColoredSkin = false;
if(pSkin != nullptr)
s_vLines[Index].m_RenderInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin;
else if(pDefaultSkin != nullptr)
s_vLines[Index].m_RenderInfo.m_OriginalRenderSkin = pDefaultSkin->m_OriginalSkin;
s_vLines[Index].m_RenderInfo.Apply(pSkin);
};
auto &&RenderPreview = [&](int LineIndex, int x, int y, bool Render = true) {
@ -2666,10 +2653,10 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView)
SetPreviewLine(PREVIEW_CLIENT, -1, "", "Echo command executed", FLAG_CLIENT, 0);
}
SetLineSkin(1, GameClient()->m_Skins.FindOrNullptr("pinky"));
SetLineSkin(2, pDefaultSkin);
SetLineSkin(3, GameClient()->m_Skins.FindOrNullptr("cammostripes"));
SetLineSkin(4, GameClient()->m_Skins.FindOrNullptr("beast"));
SetLineSkin(1, GameClient()->m_Skins.Find("pinky"));
SetLineSkin(2, GameClient()->m_Skins.Find("default"));
SetLineSkin(3, GameClient()->m_Skins.Find("cammostripes"));
SetLineSkin(4, GameClient()->m_Skins.Find("beast"));
// Backgrounds first
if(!g_Config.m_ClChatOld)
@ -2804,6 +2791,107 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView)
static CButtonContainer s_AuthedColor, s_SameClanColor;
DoLine_ColorPicker(&s_AuthedColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Authed name color in scoreboard"), &g_Config.m_ClAuthedPlayerColor, GreenDefault, false);
DoLine_ColorPicker(&s_SameClanColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Same clan color in scoreboard"), &g_Config.m_ClSameClanColor, GreenDefault, false);
// ***** Name Plate Preview ***** //
RightView.HSplitTop(HeadlineHeight, &Label, &RightView);
Ui()->DoLabel(&Label, Localize("Preview"), HeadlineFontSize, TEXTALIGN_ML);
RightView.HSplitTop(2 * MarginSmall, nullptr, &RightView);
CTeeRenderInfo TeeRenderInfo;
TeeRenderInfo.Apply(m_pClient->m_Skins.Find(g_Config.m_ClPlayerSkin));
TeeRenderInfo.ApplyColors(g_Config.m_ClPlayerUseCustomColor, g_Config.m_ClPlayerColorBody, g_Config.m_ClPlayerColorFeet);
TeeRenderInfo.m_Size = 64.0f;
const vec2 TeeRenderPos = vec2(RightView.x + RightView.w / 2, RightView.y + RightView.h / 2);
RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeRenderInfo, 0, vec2(1.0f, 0.0f), TeeRenderPos);
const float FontSize = 18.0f + 20.0f * g_Config.m_ClNameplatesSize / 100.0f;
const float FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNameplatesClanSize / 100.0f;
const ColorRGBA Rgb = g_Config.m_ClNameplatesTeamcolors ? m_pClient->GetDDTeamColor(13, 0.75f) : TextRender()->DefaultTextColor();
float YOffset = TeeRenderPos.y - 38;
TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE);
if(g_Config.m_ClShowDirection)
{
const float ShowDirectionImgSize = 22.0f;
YOffset -= ShowDirectionImgSize;
const vec2 ShowDirectionPos = vec2(TeeRenderPos.x - 11.0f, YOffset);
TextRender()->TextColor(TextRender()->DefaultTextColor());
// Left
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_ARROW].m_Id);
Graphics()->QuadsSetRotation(pi);
Graphics()->RenderQuadContainerAsSprite(m_DirectionQuadContainerIndex, 0, ShowDirectionPos.x - 30.f, ShowDirectionPos.y);
// Right
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_ARROW].m_Id);
Graphics()->QuadsSetRotation(0);
Graphics()->RenderQuadContainerAsSprite(m_DirectionQuadContainerIndex, 0, ShowDirectionPos.x + 30.f, ShowDirectionPos.y);
// Jump
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_ARROW].m_Id);
Graphics()->QuadsSetRotation(pi * 3 / 2);
Graphics()->RenderQuadContainerAsSprite(m_DirectionQuadContainerIndex, 0, ShowDirectionPos.x, ShowDirectionPos.y);
Graphics()->QuadsSetRotation(0);
}
if(g_Config.m_ClNameplates)
{
YOffset -= FontSize;
TextRender()->TextColor(Rgb);
TextRender()->TextOutlineColor(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f));
TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSize, g_Config.m_PlayerName) / 2.0f, YOffset, FontSize, g_Config.m_PlayerName);
if(g_Config.m_ClNameplatesClan)
{
YOffset -= FontSizeClan;
TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSizeClan, g_Config.m_PlayerClan) / 2.0f, YOffset, FontSizeClan, g_Config.m_PlayerClan);
}
TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor());
if(g_Config.m_ClNameplatesFriendMark)
{
YOffset -= FontSize;
TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSize, "") / 2.0f, YOffset, FontSize, "");
}
if(g_Config.m_ClNameplatesIds)
{
YOffset -= FontSize;
TextRender()->TextColor(Rgb);
TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSize, "0") / 2.0f, YOffset, FontSize, "0");
}
if(g_Config.m_ClNameplatesStrong)
{
Graphics()->TextureSet(g_pData->m_aImages[IMAGE_STRONGWEAK].m_Id);
Graphics()->QuadsBegin();
const ColorRGBA StrongStatusColor = color_cast<ColorRGBA>(ColorHSLA(6401973));
const int StrongSpriteId = SPRITE_HOOK_STRONG;
Graphics()->SetColor(StrongStatusColor);
float ScaleX, ScaleY;
RenderTools()->SelectSprite(StrongSpriteId);
RenderTools()->GetSpriteScale(StrongSpriteId, ScaleX, ScaleY);
TextRender()->TextColor(StrongStatusColor);
const float StrongImgSize = 40.0f;
YOffset -= StrongImgSize * ScaleY;
RenderTools()->DrawSprite(TeeRenderPos.x, YOffset + (StrongImgSize / 2.0f) * ScaleY, StrongImgSize);
Graphics()->QuadsEnd();
if(g_Config.m_ClNameplatesStrong == 2)
{
YOffset -= FontSize;
TextRender()->Text(TeeRenderPos.x - TextRender()->TextWidth(FontSize, "0") / 2.0f, YOffset, FontSize, "0");
}
}
}
TextRender()->TextColor(TextRender()->DefaultTextColor());
TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor());
TextRender()->SetRenderFlags(0);
}
else if(s_CurTab == APPEARANCE_TAB_HOOK_COLLISION)
{
@ -2858,6 +2946,93 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView)
DoLine_ColorPicker(&s_HookCollNoCollResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Nothing hookable"), &g_Config.m_ClHookCollColorNoColl, ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), false);
DoLine_ColorPicker(&s_HookCollHookableCollResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Something hookable"), &g_Config.m_ClHookCollColorHookableColl, ColorRGBA(130.0f / 255.0f, 232.0f / 255.0f, 160.0f / 255.0f, 1.0f), false);
DoLine_ColorPicker(&s_HookCollTeeCollResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("A Tee"), &g_Config.m_ClHookCollColorTeeColl, ColorRGBA(1.0f, 1.0f, 0.0f, 1.0f), false);
// ***** Hook collisions preview ***** //
RightView.HSplitTop(HeadlineHeight, &Label, &RightView);
Ui()->DoLabel(&Label, Localize("Preview"), HeadlineFontSize, TEXTALIGN_ML);
RightView.HSplitTop(2 * MarginSmall, nullptr, &RightView);
auto DoHookCollision = [this](const vec2 &Pos, const float &Length, const ColorRGBA &Color) {
Graphics()->TextureClear();
if(g_Config.m_ClHookCollSize > 0)
{
Graphics()->QuadsBegin();
Graphics()->SetColor(Color.WithAlpha((float)g_Config.m_ClHookCollAlpha / 100));
float LineWidth = 0.5f + (float)(g_Config.m_ClHookCollSize - 1) * 0.25f;
IGraphics::CQuadItem QuadItem(Pos.x, Pos.y - LineWidth, Length, LineWidth * 2.f);
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
}
else
{
Graphics()->LinesBegin();
Graphics()->SetColor(Color.WithAlpha((float)g_Config.m_ClHookCollAlpha / 100));
IGraphics::CLineItem LineItem(Pos.x, Pos.y, Pos.x + Length, Pos.y);
Graphics()->LinesDraw(&LineItem, 1);
Graphics()->LinesEnd();
}
};
CTeeRenderInfo OwnSkinInfo;
OwnSkinInfo.Apply(m_pClient->m_Skins.Find(g_Config.m_ClPlayerSkin));
OwnSkinInfo.ApplyColors(g_Config.m_ClPlayerUseCustomColor, g_Config.m_ClPlayerColorBody, g_Config.m_ClPlayerColorFeet);
OwnSkinInfo.m_Size = 50.0f;
CTeeRenderInfo DummySkinInfo;
DummySkinInfo.Apply(m_pClient->m_Skins.Find(g_Config.m_ClDummySkin));
DummySkinInfo.ApplyColors(g_Config.m_ClDummyUseCustomColor, g_Config.m_ClDummyColorBody, g_Config.m_ClDummyColorFeet);
DummySkinInfo.m_Size = 50.0f;
const float LineLength = 150.f;
const float LeftMargin = 30.f;
CUIRect PreviewNoColl;
RightView.HSplitTop(50.0f, &PreviewNoColl, &RightView);
RightView.HSplitTop(4 * MarginSmall, nullptr, &RightView);
vec2 TeeRenderPos = vec2(PreviewNoColl.x + LeftMargin, PreviewNoColl.y + PreviewNoColl.h / 2.0f);
DoHookCollision(TeeRenderPos, PreviewNoColl.w - LineLength, color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorNoColl)));
RenderTools()->RenderTee(CAnimState::GetIdle(), &OwnSkinInfo, 0, vec2(1.0f, 0.0f), TeeRenderPos);
CUIRect NoHookTileRect;
PreviewNoColl.VSplitRight(LineLength, &PreviewNoColl, &NoHookTileRect);
NoHookTileRect.VSplitLeft(50.0f, &NoHookTileRect, nullptr);
NoHookTileRect.Margin(10.0f, &NoHookTileRect);
// Render unhookable tile
int TileScale = 32.0f;
Graphics()->TextureClear();
Graphics()->TextureSet(m_pClient->m_MapImages.GetEntities(MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH));
Graphics()->BlendNormal();
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
RenderTools()->RenderTile(NoHookTileRect.x, NoHookTileRect.y, TILE_NOHOOK, TileScale, ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
CUIRect PreviewColl;
RightView.HSplitTop(50.0f, &PreviewColl, &RightView);
RightView.HSplitTop(4 * MarginSmall, nullptr, &RightView);
TeeRenderPos = vec2(PreviewColl.x + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
DoHookCollision(TeeRenderPos, PreviewColl.w - LineLength, color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorHookableColl)));
RenderTools()->RenderTee(CAnimState::GetIdle(), &OwnSkinInfo, 0, vec2(1.0f, 0.0f), TeeRenderPos);
CUIRect HookTileRect;
PreviewColl.VSplitRight(LineLength, &PreviewColl, &HookTileRect);
HookTileRect.VSplitLeft(50.0f, &HookTileRect, nullptr);
HookTileRect.Margin(10.0f, &HookTileRect);
// Render hookable tile
Graphics()->TextureClear();
Graphics()->TextureSet(m_pClient->m_MapImages.GetEntities(MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH));
Graphics()->BlendNormal();
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
RenderTools()->RenderTile(HookTileRect.x, HookTileRect.y, TILE_SOLID, TileScale, ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
CUIRect PreviewCollTee;
RightView.HSplitTop(50.0f, &PreviewCollTee, &RightView);
RightView.HSplitTop(4 * MarginSmall, nullptr, &RightView);
TeeRenderPos = vec2(PreviewCollTee.x + LeftMargin, PreviewCollTee.y + PreviewCollTee.h / 2.0f);
const vec2 DummyRenderPos = vec2(PreviewCollTee.x + PreviewCollTee.w - LineLength - 5.f + LeftMargin, PreviewCollTee.y + PreviewCollTee.h / 2.0f);
DoHookCollision(TeeRenderPos, PreviewCollTee.w - LineLength - 15.f, color_cast<ColorRGBA>(ColorHSLA(g_Config.m_ClHookCollColorTeeColl)));
RenderTools()->RenderTee(CAnimState::GetIdle(), &DummySkinInfo, 0, vec2(1.0f, 0.0f), DummyRenderPos);
RenderTools()->RenderTee(CAnimState::GetIdle(), &OwnSkinInfo, 0, vec2(1.0f, 0.0f), TeeRenderPos);
}
else if(s_CurTab == APPEARANCE_TAB_INFO_MESSAGES)
{

View file

@ -249,11 +249,9 @@ static const CMenus::SCustomItem *GetCustomItem(int CurTab, size_t Index)
template<typename TName>
void ClearAssetList(std::vector<TName> &vList, IGraphics *pGraphics)
{
for(size_t i = 0; i < vList.size(); ++i)
for(TName &Asset : vList)
{
if(vList[i].m_RenderTexture.IsValid())
pGraphics->UnloadTexture(&(vList[i].m_RenderTexture));
vList[i].m_RenderTexture = IGraphics::CTextureHandle();
pGraphics->UnloadTexture(&Asset.m_RenderTexture);
}
vList.clear();
}
@ -266,9 +264,7 @@ void CMenus::ClearCustomItems(int CurTab)
{
for(auto &Image : Entity.m_aImages)
{
if(Image.m_Texture.IsValid())
Graphics()->UnloadTexture(&Image.m_Texture);
Image.m_Texture = IGraphics::CTextureHandle();
Graphics()->UnloadTexture(&Image.m_Texture);
}
}
m_vEntitiesList.clear();

View file

@ -108,7 +108,7 @@ void CMenus::RenderStartMenu(CUIRect MainView)
bool UsedEscape = false;
if(DoButton_Menu(&s_QuitButton, Localize("Quit"), 0, &Button, 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (UsedEscape = Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) || CheckHotKey(KEY_Q))
{
if(UsedEscape || m_pClient->Editor()->HasUnsavedData() || (Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0))
if(UsedEscape || m_pClient->Editor()->HasUnsavedData() || (GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0))
{
m_Popup = POPUP_QUIT;
}

View file

@ -862,7 +862,6 @@ void CPlayers::OnRender()
}
CTeeRenderInfo RenderInfoSpec;
RenderInfoSpec.Apply(m_pClient->m_Skins.Find("x_spec"));
RenderInfoSpec.m_CustomColoredSkin = false;
RenderInfoSpec.m_Size = 64.0f;
const int LocalClientId = m_pClient->m_Snap.m_LocalClientId;

View file

@ -1,6 +1,8 @@
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include "skins.h"
#include <base/log.h>
#include <base/math.h>
#include <base/system.h>
@ -9,15 +11,13 @@
#include <engine/gfx/image_manipulation.h>
#include <engine/graphics.h>
#include <engine/shared/config.h>
#include <engine/shared/http.h>
#include <engine/storage.h>
#include <game/generated/client_data.h>
#include <game/client/gameclient.h>
#include <game/generated/client_data.h>
#include <game/localization.h>
#include "skins.h"
CSkins::CSkins() :
m_PlaceholderSkin("dummy")
{
@ -43,33 +43,16 @@ bool CSkins::IsVanillaSkin(const char *pName)
return std::any_of(std::begin(VANILLA_SKINS), std::end(VANILLA_SKINS), [pName](const char *pVanillaSkin) { return str_comp(pName, pVanillaSkin) == 0; });
}
void CSkins::CGetPngFile::OnCompletion(EHttpState State)
{
// Maybe this should start another thread to load the png in instead of stalling the curl thread
if(State == EHttpState::DONE)
{
m_pSkins->LoadSkinPng(m_Info, Dest(), Dest(), IStorage::TYPE_SAVE);
}
}
CSkins::CGetPngFile::CGetPngFile(CSkins *pSkins, const char *pUrl, IStorage *pStorage, const char *pDest) :
CHttpRequest(pUrl),
m_pSkins(pSkins)
{
WriteToFile(pStorage, pDest, IStorage::TYPE_SAVE);
Timeout(CTimeout{0, 0, 0, 0});
LogProgress(HTTPLOG::NONE);
}
struct SSkinScanUser
class CSkinScanUser
{
public:
CSkins *m_pThis;
CSkins::TSkinLoadedCBFunc m_SkinLoadedFunc;
CSkins::TSkinLoadedCallback m_SkinLoadedCallback;
};
int CSkins::SkinScan(const char *pName, int IsDir, int DirType, void *pUser)
{
auto *pUserReal = static_cast<SSkinScanUser *>(pUser);
auto *pUserReal = static_cast<CSkinScanUser *>(pUser);
CSkins *pSelf = pUserReal->m_pThis;
if(IsDir)
@ -94,7 +77,7 @@ int CSkins::SkinScan(const char *pName, int IsDir, int DirType, void *pUser)
char aPath[IO_MAX_PATH_LENGTH];
str_format(aPath, sizeof(aPath), "skins/%s", pName);
pSelf->LoadSkin(aSkinName, aPath, DirType);
pUserReal->m_SkinLoadedFunc((int)pSelf->m_Skins.size());
pUserReal->m_SkinLoadedCallback();
return 0;
}
@ -136,19 +119,12 @@ static void CheckMetrics(CSkin::SSkinMetricVariable &Metrics, const uint8_t *pIm
const CSkin *CSkins::LoadSkin(const char *pName, const char *pPath, int DirType)
{
CImageInfo Info;
if(!LoadSkinPng(Info, pName, pPath, DirType))
return nullptr;
return LoadSkin(pName, Info);
}
bool CSkins::LoadSkinPng(CImageInfo &Info, const char *pName, const char *pPath, int DirType)
{
if(!Graphics()->LoadPng(Info, pPath, DirType))
{
log_error("skins", "Failed to load skin PNG: %s", pName);
return false;
return nullptr;
}
return true;
return LoadSkin(pName, Info);
}
const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info)
@ -298,6 +274,8 @@ const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info)
auto &&pSkin = std::make_unique<CSkin>(std::move(Skin));
const auto SkinInsertIt = m_Skins.insert({pSkin->GetName(), std::move(pSkin)});
m_LastRefreshTime = time_get_nanoseconds();
return SkinInsertIt.first->second.get();
}
@ -313,32 +291,34 @@ void CSkins::OnInit()
}
}
// load skins;
Refresh([this](int SkinCounter) {
// load skins
Refresh([this]() {
GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0);
});
}
void CSkins::Refresh(TSkinLoadedCBFunc &&SkinLoadedFunc)
void CSkins::OnShutdown()
{
m_LoadingSkins.clear();
}
void CSkins::Refresh(TSkinLoadedCallback &&SkinLoadedCallback)
{
m_LoadingSkins.clear();
for(const auto &[_, pSkin] : m_Skins)
{
pSkin->m_OriginalSkin.Unload(Graphics());
pSkin->m_ColorableSkin.Unload(Graphics());
}
m_Skins.clear();
m_DownloadSkins.clear();
m_DownloadingSkins = 0;
SSkinScanUser SkinScanUser;
SkinScanUser.m_pThis = this;
SkinScanUser.m_SkinLoadedFunc = SkinLoadedFunc;
Storage()->ListDirectory(IStorage::TYPE_ALL, "skins", SkinScan, &SkinScanUser);
}
int CSkins::Num()
{
return m_Skins.size();
CSkinScanUser SkinScanUser;
SkinScanUser.m_pThis = this;
SkinScanUser.m_SkinLoadedCallback = SkinLoadedCallback;
Storage()->ListDirectory(IStorage::TYPE_ALL, "skins", SkinScan, &SkinScanUser);
m_LastRefreshTime = time_get_nanoseconds();
}
const CSkin *CSkins::Find(const char *pName)
@ -365,7 +345,7 @@ const CSkin *CSkins::FindOrNullptr(const char *pName, bool IgnorePrefix)
const char *pSkinPrefix = m_aEventSkinPrefix[0] != '\0' ? m_aEventSkinPrefix : g_Config.m_ClSkinPrefix;
if(!IgnorePrefix && pSkinPrefix[0] != '\0')
{
char aNameWithPrefix[48]; // Larger than skin name length to allow IsValidName to check if it's too long
char aNameWithPrefix[2 * MAX_SKIN_LENGTH + 2]; // Larger than skin name length to allow IsValidName to check if it's too long
str_format(aNameWithPrefix, sizeof(aNameWithPrefix), "%s_%s", pSkinPrefix, pName);
// If we find something, use it, otherwise fall back to normal skins.
const auto *pResult = FindImpl(aNameWithPrefix);
@ -393,44 +373,26 @@ const CSkin *CSkins::FindImpl(const char *pName)
if(!CSkin::IsValidName(pName))
return nullptr;
const auto SkinDownloadIt = m_DownloadSkins.find(pName);
if(SkinDownloadIt != m_DownloadSkins.end())
auto ExistingLoadingSkin = m_LoadingSkins.find(pName);
if(ExistingLoadingSkin != m_LoadingSkins.end())
{
if(SkinDownloadIt->second->m_pTask && SkinDownloadIt->second->m_pTask->State() == EHttpState::DONE && SkinDownloadIt->second->m_pTask->m_Info.m_pData)
std::unique_ptr<CLoadingSkin> &pLoadingSkin = ExistingLoadingSkin->second;
if(!pLoadingSkin->m_pDownloadJob || !pLoadingSkin->m_pDownloadJob->Done())
return nullptr;
if(pLoadingSkin->m_pDownloadJob->State() == IJob::STATE_DONE && pLoadingSkin->m_pDownloadJob->ImageInfo().m_pData)
{
char aPath[IO_MAX_PATH_LENGTH];
str_format(aPath, sizeof(aPath), "downloadedskins/%s.png", SkinDownloadIt->second->GetName());
Storage()->RenameFile(SkinDownloadIt->second->m_aPath, aPath, IStorage::TYPE_SAVE);
const auto *pSkin = LoadSkin(SkinDownloadIt->second->GetName(), SkinDownloadIt->second->m_pTask->m_Info);
SkinDownloadIt->second->m_pTask = nullptr;
--m_DownloadingSkins;
return pSkin;
}
if(SkinDownloadIt->second->m_pTask && (SkinDownloadIt->second->m_pTask->State() == EHttpState::ERROR || SkinDownloadIt->second->m_pTask->State() == EHttpState::ABORTED))
{
SkinDownloadIt->second->m_pTask = nullptr;
--m_DownloadingSkins;
LoadSkin(pLoadingSkin->Name(), pLoadingSkin->m_pDownloadJob->ImageInfo());
}
pLoadingSkin->m_pDownloadJob = nullptr;
return nullptr;
}
CDownloadSkin Skin{pName};
char aEscapedName[256];
EscapeUrl(aEscapedName, sizeof(aEscapedName), pName);
char aUrl[IO_MAX_PATH_LENGTH];
str_format(aUrl, sizeof(aUrl), "%s%s.png", g_Config.m_ClDownloadCommunitySkins != 0 ? g_Config.m_ClSkinCommunityDownloadUrl : g_Config.m_ClSkinDownloadUrl, aEscapedName);
char aBuf[IO_MAX_PATH_LENGTH];
str_format(Skin.m_aPath, sizeof(Skin.m_aPath), "downloadedskins/%s", IStorage::FormatTmpPath(aBuf, sizeof(aBuf), pName));
Skin.m_pTask = std::make_shared<CGetPngFile>(this, aUrl, Storage(), Skin.m_aPath);
Http()->Run(Skin.m_pTask);
auto &&pDownloadSkin = std::make_unique<CDownloadSkin>(std::move(Skin));
m_DownloadSkins.insert({pDownloadSkin->GetName(), std::move(pDownloadSkin)});
++m_DownloadingSkins;
CLoadingSkin LoadingSkin(pName);
LoadingSkin.m_pDownloadJob = std::make_shared<CSkinDownloadJob>(this, pName);
Engine()->AddJob(LoadingSkin.m_pDownloadJob);
auto &&pLoadingSkin = std::make_unique<CLoadingSkin>(std::move(LoadingSkin));
m_LoadingSkins.insert({pLoadingSkin->Name(), std::move(pLoadingSkin)});
return nullptr;
}
@ -463,15 +425,191 @@ void CSkins::RandomizeSkin(int Dummy)
const size_t SkinNameSize = Dummy ? sizeof(g_Config.m_ClDummySkin) : sizeof(g_Config.m_ClPlayerSkin);
char aRandomSkinName[MAX_SKIN_LENGTH];
str_copy(aRandomSkinName, "default", SkinNameSize);
if(!m_pClient->m_Skins.GetSkinsUnsafe().empty())
if(!m_Skins.empty())
{
do
{
auto it = m_pClient->m_Skins.GetSkinsUnsafe().begin();
std::advance(it, rand() % m_pClient->m_Skins.GetSkinsUnsafe().size());
auto it = m_Skins.begin();
std::advance(it, rand() % m_Skins.size());
str_copy(aRandomSkinName, (*it).second->GetName(), SkinNameSize);
} while(!str_comp(aRandomSkinName, "x_ninja") || !str_comp(aRandomSkinName, "x_spec"));
}
char *pSkinName = Dummy ? g_Config.m_ClDummySkin : g_Config.m_ClPlayerSkin;
str_copy(pSkinName, aRandomSkinName, SkinNameSize);
}
CSkins::CSkinDownloadJob::CSkinDownloadJob(CSkins *pSkins, const char *pName) :
m_pSkins(pSkins)
{
str_copy(m_aName, pName);
Abortable(true);
}
CSkins::CSkinDownloadJob::~CSkinDownloadJob()
{
m_ImageInfo.Free();
}
bool CSkins::CSkinDownloadJob::Abort()
{
if(!IJob::Abort())
{
return false;
}
const CLockScope LockScope(m_Lock);
if(m_pGetRequest)
{
m_pGetRequest->Abort();
m_pGetRequest = nullptr;
}
return true;
}
void CSkins::CSkinDownloadJob::Run()
{
const char *pBaseUrl = g_Config.m_ClDownloadCommunitySkins != 0 ? g_Config.m_ClSkinCommunityDownloadUrl : g_Config.m_ClSkinDownloadUrl;
char aEscapedName[256];
EscapeUrl(aEscapedName, sizeof(aEscapedName), m_aName);
char aUrl[IO_MAX_PATH_LENGTH];
str_format(aUrl, sizeof(aUrl), "%s%s.png", pBaseUrl, aEscapedName);
char aPathReal[IO_MAX_PATH_LENGTH];
str_format(aPathReal, sizeof(aPathReal), "downloadedskins/%s.png", m_aName);
const CTimeout Timeout{10000, 0, 8192, 10};
const size_t MaxResponseSize = 10 * 1024 * 1024; // 10 MiB
// We assume the file does not exist if we could not get the times
time_t FileCreatedTime, FileModifiedTime;
const bool GotFileTimes = m_pSkins->Storage()->RetrieveTimes(aPathReal, IStorage::TYPE_SAVE, &FileCreatedTime, &FileModifiedTime);
std::shared_ptr<CHttpRequest> pGet = HttpGet(aUrl);
pGet->Timeout(Timeout);
pGet->MaxResponseSize(MaxResponseSize);
if(GotFileTimes)
{
pGet->IfModifiedSince(FileModifiedTime);
pGet->FailOnErrorStatus(false);
}
pGet->LogProgress(HTTPLOG::NONE);
{
const CLockScope LockScope(m_Lock);
m_pGetRequest = pGet;
}
m_pSkins->Http()->Run(pGet);
// Load existing file while waiting for the HTTP request
if(GotFileTimes)
{
m_pSkins->Graphics()->LoadPng(m_ImageInfo, aPathReal, IStorage::TYPE_SAVE);
}
pGet->Wait();
{
const CLockScope LockScope(m_Lock);
m_pGetRequest = nullptr;
}
if(pGet->State() != EHttpState::DONE || State() == IJob::STATE_ABORTED || pGet->StatusCode() >= 400)
{
return;
}
if(pGet->StatusCode() == 304) // 304 Not Modified
{
if(m_ImageInfo.m_pData != nullptr)
{
return; // Local skin is up-to-date and was loaded successfully
}
log_error("skins", "Failed to load PNG of existing downloaded skin '%s' from '%s', downloading it again", m_aName, aPathReal);
pGet = HttpGet(aUrl);
pGet->Timeout(Timeout);
pGet->MaxResponseSize(MaxResponseSize);
pGet->LogProgress(HTTPLOG::NONE);
{
const CLockScope LockScope(m_Lock);
m_pGetRequest = pGet;
}
m_pSkins->Http()->Run(pGet);
pGet->Wait();
{
const CLockScope LockScope(m_Lock);
m_pGetRequest = nullptr;
}
if(pGet->State() != EHttpState::DONE || State() == IJob::STATE_ABORTED)
{
return;
}
}
unsigned char *pResult;
size_t ResultSize;
pGet->Result(&pResult, &ResultSize);
if(!m_pSkins->Graphics()->LoadPng(m_ImageInfo, pResult, ResultSize, aUrl))
{
log_error("skins", "Failed to load PNG of skin '%s' downloaded from '%s'", m_aName, aUrl);
return;
}
if(State() == IJob::STATE_ABORTED)
{
return;
}
char aBuf[IO_MAX_PATH_LENGTH];
char aPathTemp[IO_MAX_PATH_LENGTH];
str_format(aPathTemp, sizeof(aPathTemp), "downloadedskins/%s", IStorage::FormatTmpPath(aBuf, sizeof(aBuf), m_aName));
IOHANDLE TempFile = m_pSkins->Storage()->OpenFile(aPathTemp, IOFLAG_WRITE, IStorage::TYPE_SAVE);
if(!TempFile)
{
log_error("skins", "Failed to open temporary skin file '%s' for writing", aPathTemp);
return;
}
if(io_write(TempFile, pResult, ResultSize) != ResultSize)
{
log_error("skins", "Failed to write downloaded skin data to '%s'", aPathTemp);
io_close(TempFile);
m_pSkins->Storage()->RemoveFile(aPathTemp, IStorage::TYPE_SAVE);
return;
}
io_close(TempFile);
if(!m_pSkins->Storage()->RenameFile(aPathTemp, aPathReal, IStorage::TYPE_SAVE))
{
log_error("skins", "Failed to rename temporary skin file '%s' to '%s'", aPathTemp, aPathReal);
m_pSkins->Storage()->RemoveFile(aPathTemp, IStorage::TYPE_SAVE);
return;
}
}
CSkins::CLoadingSkin::CLoadingSkin(const char *pName)
{
str_copy(m_aName, pName);
}
CSkins::CLoadingSkin::~CLoadingSkin()
{
if(m_pDownloadJob)
{
m_pDownloadJob->Abort();
}
}
bool CSkins::CLoadingSkin::operator<(const CLoadingSkin &Other) const
{
return str_comp(m_aName, Other.m_aName) < 0;
}
bool CSkins::CLoadingSkin::operator<(const char *pOther) const
{
return str_comp(m_aName, pOther) < 0;
}
bool CSkins::CLoadingSkin::operator==(const char *pOther) const
{
return !str_comp(m_aName, pOther);
}

View file

@ -3,72 +3,38 @@
#ifndef GAME_CLIENT_COMPONENTS_SKINS_H
#define GAME_CLIENT_COMPONENTS_SKINS_H
#include <base/system.h>
#include <engine/shared/http.h>
#include <base/lock.h>
#include <engine/shared/jobs.h>
#include <game/client/component.h>
#include <game/client/skin.h>
#include <chrono>
#include <string_view>
#include <unordered_map>
class CHttpRequest;
class CSkins : public CComponent
{
public:
CSkins();
class CGetPngFile : public CHttpRequest
{
CSkins *m_pSkins;
typedef std::function<void()> TSkinLoadedCallback;
protected:
virtual void OnCompletion(EHttpState State) override;
public:
CGetPngFile(CSkins *pSkins, const char *pUrl, IStorage *pStorage, const char *pDest);
CImageInfo m_Info;
};
struct CDownloadSkin
{
private:
char m_aName[MAX_SKIN_LENGTH];
public:
std::shared_ptr<CSkins::CGetPngFile> m_pTask;
char m_aPath[IO_MAX_PATH_LENGTH];
CDownloadSkin(CDownloadSkin &&Other) = default;
CDownloadSkin(const char *pName)
{
str_copy(m_aName, pName);
}
~CDownloadSkin()
{
if(m_pTask)
m_pTask->Abort();
}
bool operator<(const CDownloadSkin &Other) const { return str_comp(m_aName, Other.m_aName) < 0; }
bool operator<(const char *pOther) const { return str_comp(m_aName, pOther) < 0; }
bool operator==(const char *pOther) const { return !str_comp(m_aName, pOther); }
CDownloadSkin &operator=(CDownloadSkin &&Other) = default;
const char *GetName() const { return m_aName; }
};
typedef std::function<void(int)> TSkinLoadedCBFunc;
virtual int Sizeof() const override { return sizeof(*this); }
int Sizeof() const override { return sizeof(*this); }
void OnInit() override;
void OnShutdown() override;
void Refresh(TSkinLoadedCBFunc &&SkinLoadedFunc);
int Num();
std::unordered_map<std::string_view, std::unique_ptr<CSkin>> &GetSkinsUnsafe() { return m_Skins; }
void Refresh(TSkinLoadedCallback &&SkinLoadedCallback);
std::chrono::nanoseconds LastRefreshTime() const { return m_LastRefreshTime; }
const std::unordered_map<std::string_view, std::unique_ptr<CSkin>> &GetSkinsUnsafe() const { return m_Skins; }
const CSkin *FindOrNullptr(const char *pName, bool IgnorePrefix = false);
const CSkin *Find(const char *pName);
void RandomizeSkin(int Dummy);
bool IsDownloadingSkins() { return m_DownloadingSkins; }
void RandomizeSkin(int Dummy);
static bool IsVanillaSkin(const char *pName);
@ -78,13 +44,56 @@ public:
"twinbop", "twintri", "warpaint", "x_ninja", "x_spec"};
private:
class CSkinDownloadJob : public IJob
{
public:
CSkinDownloadJob(CSkins *pSkins, const char *pName);
~CSkinDownloadJob();
bool Abort() override REQUIRES(!m_Lock);
CImageInfo &ImageInfo() { return m_ImageInfo; }
protected:
void Run() override REQUIRES(!m_Lock);
private:
CSkins *m_pSkins;
char m_aName[MAX_SKIN_LENGTH];
CLock m_Lock;
std::shared_ptr<CHttpRequest> m_pGetRequest;
CImageInfo m_ImageInfo;
};
class CLoadingSkin
{
private:
char m_aName[MAX_SKIN_LENGTH];
public:
std::shared_ptr<CSkinDownloadJob> m_pDownloadJob = nullptr;
CLoadingSkin(CLoadingSkin &&Other) = default;
CLoadingSkin(const char *pName);
~CLoadingSkin();
bool operator<(const CLoadingSkin &Other) const;
bool operator<(const char *pOther) const;
bool operator==(const char *pOther) const;
CLoadingSkin &operator=(CLoadingSkin &&Other) = default;
const char *Name() const { return m_aName; }
};
std::unordered_map<std::string_view, std::unique_ptr<CSkin>> m_Skins;
std::unordered_map<std::string_view, std::unique_ptr<CDownloadSkin>> m_DownloadSkins;
std::unordered_map<std::string_view, std::unique_ptr<CLoadingSkin>> m_LoadingSkins;
std::chrono::nanoseconds m_LastRefreshTime;
CSkin m_PlaceholderSkin;
size_t m_DownloadingSkins = 0;
char m_aEventSkinPrefix[MAX_SKIN_LENGTH];
bool LoadSkinPng(CImageInfo &Info, const char *pName, const char *pPath, int DirType);
const CSkin *LoadSkin(const char *pName, const char *pPath, int DirType);
const CSkin *LoadSkin(const char *pName, CImageInfo &Info);
const CSkin *FindImpl(const char *pName);

View file

@ -328,7 +328,10 @@ void CGameClient::OnInit()
for(int i = 0; i < OLD_NUM_NETOBJTYPES; i++)
Client()->SnapSetStaticsize7(i, m_NetObjHandler7.GetObjSize(i));
TextRender()->LoadFonts();
if(!TextRender()->LoadFonts())
{
Client()->AddWarning(SWarning(Localize("Some fonts could not be loaded. Check the local console for details.")));
}
TextRender()->SetFontLanguageVariant(g_Config.m_ClLanguagefile);
// update and swap after font loading, they are quite huge
@ -532,7 +535,7 @@ void CGameClient::OnConnected()
const char *pLoadMapContent = Localize("Initializing map logic");
// render loading before skip is calculated
m_Menus.RenderLoading(pConnectCaption, pLoadMapContent, 0, false);
m_Layers.Init(Kernel());
m_Layers.Init(Kernel()->RequestInterface<IMap>(), false);
m_Collision.Init(Layers());
m_GameWorld.m_Core.InitSwitchers(m_Collision.m_HighestSwitchNumber);
m_RaceHelper.Init(this);
@ -572,6 +575,7 @@ void CGameClient::OnReset()
std::fill(std::begin(m_aLastNewPredictedTick), std::end(m_aLastNewPredictedTick), -1);
m_LastRoundStartTick = -1;
m_LastRaceTick = -1;
m_LastFlagCarrierRed = -4;
m_LastFlagCarrierBlue = -4;
@ -844,9 +848,18 @@ void CGameClient::OnDummyDisconnect()
m_PredictedDummyId = -1;
}
int CGameClient::GetLastRaceTick() const
int CGameClient::LastRaceTick() const
{
return m_Ghost.GetLastRaceTick();
return m_LastRaceTick;
}
int CGameClient::CurrentRaceTime() const
{
if(m_LastRaceTick < 0)
{
return 0;
}
return (Client()->GameTick(g_Config.m_ClDummy) - m_LastRaceTick) / Client()->GameTickSpeed();
}
bool CGameClient::Predict() const
@ -940,6 +953,7 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm
m_aReceivedTuning[Conn] = true;
// apply new tuning
m_aTuning[Conn] = NewTuning;
TuningList()[0] = NewTuning;
return;
}
@ -1542,30 +1556,21 @@ void CGameClient::OnNewSnapshot()
}
IntsToStr(&pInfo->m_Clan0, 3, pClient->m_aClan, std::size(pClient->m_aClan));
pClient->m_Country = pInfo->m_Country;
IntsToStr(&pInfo->m_Skin0, 6, pClient->m_aSkinName, std::size(pClient->m_aSkinName));
if(pClient->m_aSkinName[0] == '\0' ||
(!m_GameInfo.m_AllowXSkins && (pClient->m_aSkinName[0] == 'x' && pClient->m_aSkinName[1] == '_')))
{
str_copy(pClient->m_aSkinName, "default");
}
pClient->m_UseCustomColor = pInfo->m_UseCustomColor;
pClient->m_ColorBody = pInfo->m_ColorBody;
pClient->m_ColorFeet = pInfo->m_ColorFeet;
// prepare the info
if(!m_GameInfo.m_AllowXSkins && (pClient->m_aSkinName[0] == 'x' && pClient->m_aSkinName[1] == '_'))
str_copy(pClient->m_aSkinName, "default");
pClient->m_SkinInfo.m_ColorBody = color_cast<ColorRGBA>(ColorHSLA(pClient->m_ColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT));
pClient->m_SkinInfo.m_ColorFeet = color_cast<ColorRGBA>(ColorHSLA(pClient->m_ColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT));
pClient->m_SkinInfo.m_Size = 64;
// find new skin
pClient->m_SkinInfo.Apply(m_Skins.Find(pClient->m_aSkinName));
pClient->m_SkinInfo.m_CustomColoredSkin = pClient->m_UseCustomColor;
if(!pClient->m_UseCustomColor)
{
pClient->m_SkinInfo.m_ColorBody = ColorRGBA(1, 1, 1);
pClient->m_SkinInfo.m_ColorFeet = ColorRGBA(1, 1, 1);
}
pClient->m_SkinInfo.ApplyColors(pClient->m_UseCustomColor, pClient->m_ColorBody, pClient->m_ColorFeet);
pClient->UpdateRenderInfo(IsTeamPlay());
}
}
@ -2097,6 +2102,19 @@ void CGameClient::OnNewSnapshot()
}
}
}
// Record m_LastRaceTick for g_Config.m_ClConfirmDisconnect/QuitTime
if(m_GameInfo.m_Race &&
Client()->State() == IClient::STATE_ONLINE &&
m_Snap.m_pGameInfoObj &&
!m_Snap.m_SpecInfo.m_Active &&
m_Snap.m_pLocalCharacter &&
m_Snap.m_pLocalPrevCharacter)
{
const bool RaceFlag = m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME;
m_LastRaceTick = RaceFlag ? -m_Snap.m_pGameInfoObj->m_WarmupTimer : -1;
}
if(m_Snap.m_LocalClientId != m_PrevLocalId)
m_PredictedDummyId = m_PrevLocalId;
m_PrevLocalId = m_Snap.m_LocalClientId;
@ -3745,7 +3763,7 @@ void CGameClient::LoadExtrasSkin(const char *pPath, bool AsDir)
void CGameClient::RefreshSkins()
{
const auto SkinStartLoadTime = time_get_nanoseconds();
m_Skins.Refresh([&](int) {
m_Skins.Refresh([&]() {
// if skin refreshing takes to long, swap to a loading screen
if(time_get_nanoseconds() - SkinStartLoadTime > 500ms)
{
@ -3755,15 +3773,7 @@ void CGameClient::RefreshSkins()
for(auto &Client : m_aClients)
{
if(Client.m_aSkinName[0] != '\0')
{
Client.m_SkinInfo.Apply(m_Skins.Find(Client.m_aSkinName));
}
else
{
Client.m_SkinInfo.m_OriginalRenderSkin.Reset();
Client.m_SkinInfo.m_ColorableRenderSkin.Reset();
}
Client.m_SkinInfo.Apply(m_Skins.Find(Client.m_aSkinName));
Client.UpdateRenderInfo(IsTeamPlay());
}
@ -3793,6 +3803,13 @@ void CGameClient::LoadMapSettings()
for(int i = 0; i < NUM_TUNEZONES; i++)
{
TuningList()[i] = TuningParams;
// only hardcode ddrace tuning for the tune zones
// and not the base tuning
// that one will be sent by the server if needed
if(!i)
continue;
TuningList()[i].Set("gun_curvature", 0);
TuningList()[i].Set("gun_speed", 1400);
TuningList()[i].Set("shotgun_curvature", 0);

View file

@ -209,6 +209,7 @@ private:
int m_aLastNewPredictedTick[NUM_DUMMIES];
int m_LastRoundStartTick;
int m_LastRaceTick;
int m_LastFlagCarrierRed;
int m_LastFlagCarrierBlue;
@ -579,7 +580,8 @@ public:
int IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, int OwnId);
int GetLastRaceTick() const override;
int LastRaceTick() const;
int CurrentRaceTime() const;
bool IsTeamPlay() { return m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS; }

View file

@ -196,7 +196,7 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event)
const size_t SelectionLength = GetSelectionLength();
bool KeyHandled = false;
if((Event.m_Flags & IInput::FLAG_TEXT) && !(KEY_LCTRL <= Event.m_Key && Event.m_Key <= KEY_RGUI))
if(Event.m_Flags & IInput::FLAG_TEXT)
{
SetRange(Event.m_aText, m_SelectionStart, m_SelectionEnd);
KeyHandled = true;

View file

@ -64,6 +64,21 @@ public:
m_SkinMetrics = pSkin->m_Metrics;
}
void ApplyColors(bool CustomColoredSkin, int ColorBody, int ColorFeet)
{
m_CustomColoredSkin = CustomColoredSkin;
if(CustomColoredSkin)
{
m_ColorBody = color_cast<ColorRGBA>(ColorHSLA(ColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT));
m_ColorFeet = color_cast<ColorRGBA>(ColorHSLA(ColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT));
}
else
{
m_ColorBody = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
m_ColorFeet = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
}
}
CSkin::SSkinTextures m_OriginalRenderSkin;
CSkin::SSkinTextures m_ColorableRenderSkin;
@ -226,6 +241,8 @@ public:
// the rectangle include all tiles in [RectX, RectX+RectW-1] x [RectY, RectY+RectH-1]
void RenderTileRectangle(int RectX, int RectY, int RectW, int RectH, unsigned char IndexIn, unsigned char IndexOut, float Scale, ColorRGBA Color, int RenderFlags) const;
void RenderTile(int x, int y, unsigned char Index, float Scale, ColorRGBA Color) const;
// helpers
void CalcScreenParams(float Aspect, float Zoom, float *pWidth, float *pHeight);
void MapScreenToWorld(float CenterX, float CenterY, float ParallaxX, float ParallaxY,

View file

@ -667,6 +667,73 @@ void CRenderTools::RenderTilemap(CTile *pTiles, int w, int h, float Scale, Color
Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1);
}
void CRenderTools::RenderTile(int x, int y, unsigned char Index, float Scale, ColorRGBA Color) const
{
if(Graphics()->HasTextureArraysSupport())
Graphics()->QuadsTex3DBegin();
else
Graphics()->QuadsBegin();
float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1);
// calculate the final pixelsize for the tiles
float TilePixelSize = 1024 / Scale;
float FinalTileSize = Scale / (ScreenX1 - ScreenX0) * Graphics()->ScreenWidth();
float FinalTilesetScale = FinalTileSize / TilePixelSize;
float TexSize = 1024.0f;
float Frac = (1.25f / TexSize) * (1 / FinalTilesetScale);
float Nudge = (0.5f / TexSize) * (1 / FinalTilesetScale);
int tx = Index % 16;
int ty = Index / 16;
int Px0 = tx * (1024 / 16);
int Py0 = ty * (1024 / 16);
int Px1 = Px0 + (1024 / 16) - 1;
int Py1 = Py0 + (1024 / 16) - 1;
float x0 = Nudge + Px0 / TexSize + Frac;
float y0 = Nudge + Py0 / TexSize + Frac;
float x1 = Nudge + Px1 / TexSize - Frac;
float y1 = Nudge + Py0 / TexSize + Frac;
float x2 = Nudge + Px1 / TexSize - Frac;
float y2 = Nudge + Py1 / TexSize - Frac;
float x3 = Nudge + Px0 / TexSize + Frac;
float y3 = Nudge + Py1 / TexSize - Frac;
if(Graphics()->HasTextureArraysSupport())
{
x0 = 0;
y0 = 0;
x1 = x0 + 1;
y1 = y0;
x2 = x0 + 1;
y2 = y0 + 1;
x3 = x0;
y3 = y0 + 1;
}
if(Graphics()->HasTextureArraysSupport())
{
Graphics()->QuadsSetSubsetFree(x0, y0, x1, y1, x2, y2, x3, y3, Index);
IGraphics::CQuadItem QuadItem(x, y, Scale, Scale);
Graphics()->QuadsTex3DDrawTL(&QuadItem, 1);
}
else
{
Graphics()->QuadsSetSubsetFree(x0, y0, x1, y1, x2, y2, x3, y3);
IGraphics::CQuadItem QuadItem(x, y, Scale, Scale);
Graphics()->QuadsDrawTL(&QuadItem, 1);
}
if(Graphics()->HasTextureArraysSupport())
Graphics()->QuadsTex3DEnd();
else
Graphics()->QuadsEnd();
Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1);
}
void CRenderTools::RenderTeleOverlay(CTeleTile *pTele, int w, int h, float Scale, float Alpha) const
{
if(!g_Config.m_ClTextEntities)

View file

@ -4478,10 +4478,7 @@ bool CEditor::ReplaceImage(const char *pFileName, int StorageType, bool CheckDup
pImg->m_External = IsVanillaImage(pImg->m_aName);
ConvertToRgba(*pImg);
if(g_Config.m_ClEditorDilate == 1)
{
DilateImage(*pImg);
}
DilateImage(*pImg);
pImg->m_AutoMapper.Load(pImg->m_aName);
int TextureLoadFlag = Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
@ -4542,10 +4539,7 @@ bool CEditor::AddImage(const char *pFileName, int StorageType, void *pUser)
pImg->m_External = IsVanillaImage(aBuf);
ConvertToRgba(*pImg);
if(g_Config.m_ClEditorDilate == 1)
{
DilateImage(*pImg);
}
DilateImage(*pImg);
int TextureLoadFlag = pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0)
@ -6781,22 +6775,30 @@ void CEditor::RenderEnvelopeEditor(CUIRect View)
CurveButton.w = CurveBar.h;
CurveButton.x -= CurveButton.w / 2.0f;
const void *pId = &pEnvelope->m_vPoints[i].m_Curvetype;
const char *apTypeName[] = {"N", "L", "S", "F", "M", "B"};
static const char *const TYPE_NAMES[NUM_CURVETYPES] = {"N", "L", "S", "F", "M", "B"};
const char *pTypeName = "!?";
if(0 <= pEnvelope->m_vPoints[i].m_Curvetype && pEnvelope->m_vPoints[i].m_Curvetype < (int)std::size(apTypeName))
pTypeName = apTypeName[pEnvelope->m_vPoints[i].m_Curvetype];
if(0 <= pEnvelope->m_vPoints[i].m_Curvetype && pEnvelope->m_vPoints[i].m_Curvetype < (int)std::size(TYPE_NAMES))
pTypeName = TYPE_NAMES[pEnvelope->m_vPoints[i].m_Curvetype];
if(CurveButton.x >= View.x)
{
if(DoButton_Editor(pId, pTypeName, 0, &CurveButton, 0, "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier)"))
const int ButtonResult = DoButton_Editor(pId, pTypeName, 0, &CurveButton, BUTTON_CONTEXT, "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier).");
if(ButtonResult == 1)
{
int PrevCurve = pEnvelope->m_vPoints[i].m_Curvetype;
pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + 1) % NUM_CURVETYPES;
const int PrevCurve = pEnvelope->m_vPoints[i].m_Curvetype;
const int Direction = Input()->ShiftIsPressed() ? -1 : 1;
pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + Direction + NUM_CURVETYPES) % NUM_CURVETYPES;
m_EnvelopeEditorHistory.RecordAction(std::make_shared<CEditorActionEnvelopeEditPoint>(this,
m_SelectedEnvelope, i, 0, CEditorActionEnvelopeEditPoint::EEditType::CURVE_TYPE, PrevCurve, pEnvelope->m_vPoints[i].m_Curvetype));
m_Map.OnModify();
}
else if(ButtonResult == 2)
{
m_PopupEnvelopeSelectedPoint = i;
static SPopupMenuId s_PopupCurvetypeId;
Ui()->DoPopupMenu(&s_PopupCurvetypeId, Ui()->MouseX(), Ui()->MouseY(), 80, NUM_CURVETYPES * 14.0f + 10.0f, this, PopupEnvelopeCurvetype);
}
}
}
}
@ -9088,9 +9090,7 @@ void CEditor::AdjustBrushSpecialTiles(bool UseNextFree, int Adjust)
if(IsTeleTileNumberUsedAny(pTeleLayer->m_pTiles[i].m_Index) &&
m_TeleNumbers[pTeleLayer->m_pTiles[i].m_Index] != pTeleLayer->m_pTeleTile[i].m_Number)
{
if(UseNextFree || Adjust != 0)
m_TeleNumbers[pTeleLayer->m_pTiles[i].m_Index] = pTeleLayer->m_pTeleTile[i].m_Number;
else if(!UseNextFree && Adjust == 0)
if(!UseNextFree && Adjust == 0)
pTeleLayer->m_pTeleTile[i].m_Number = m_TeleNumbers[pTeleLayer->m_pTiles[i].m_Index];
}
}

View file

@ -917,6 +917,8 @@ public:
static CUi::EPopupMenuFunctionResult PopupEntities(void *pContext, CUIRect View, bool Active);
static CUi::EPopupMenuFunctionResult PopupProofMode(void *pContext, CUIRect View, bool Active);
static CUi::EPopupMenuFunctionResult PopupAnimateSettings(void *pContext, CUIRect View, bool Active);
int m_PopupEnvelopeSelectedPoint = -1;
static CUi::EPopupMenuFunctionResult PopupEnvelopeCurvetype(void *pContext, CUIRect View, bool Active);
static bool CallbackOpenMap(const char *pFileName, int StorageType, void *pUser);
static bool CallbackAppendMap(const char *pFileName, int StorageType, void *pUser);

View file

@ -91,7 +91,7 @@ void CLayerGroup::Render()
for(auto &pLayer : m_vpLayers)
{
if(pLayer->m_Visible && pLayer->m_Type == LAYERTYPE_TILES && pLayer != m_pMap->m_pGameLayer && pLayer != m_pMap->m_pFrontLayer && pLayer != m_pMap->m_pTeleLayer && pLayer != m_pMap->m_pSpeedupLayer && pLayer != m_pMap->m_pSwitchLayer && pLayer != m_pMap->m_pTuneLayer)
if(pLayer->m_Visible && pLayer->m_Type == LAYERTYPE_TILES && !pLayer->IsEntitiesLayer())
{
std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(pLayer);
if(pTiles->m_Game || pTiles->m_Front || pTiles->m_Tele || pTiles->m_Speedup || pTiles->m_Tune || pTiles->m_Switch)

View file

@ -109,10 +109,6 @@ void CLayerTele::BrushDraw(std::shared_ptr<CLayer> pBrush, vec2 WorldPos)
// as tiles with number 0 would be ignored by previous versions.
m_pTeleTile[Index].m_Number = 255;
}
else if(m_pEditor->m_TeleNumbers[TgtIndex] != pTeleLayer->m_TeleNumbers[TgtIndex])
{
m_pTeleTile[Index].m_Number = m_pEditor->m_TeleNumbers[TgtIndex];
}
else if(pTeleLayer->m_pTeleTile[y * pTeleLayer->m_Width + x].m_Number)
{
m_pTeleTile[Index].m_Number = pTeleLayer->m_pTeleTile[y * pTeleLayer->m_Width + x].m_Number;
@ -280,7 +276,7 @@ void CLayerTele::FillSelection(bool Empty, std::shared_ptr<CLayer> pBrush, CUIRe
// as tiles with number 0 would be ignored by previous versions.
m_pTeleTile[TgtIndex].m_Number = 255;
}
else if((pLt->m_pTeleTile[SrcIndex].m_Number == 0 && m_pEditor->m_TeleNumbers[m_pTiles[TgtIndex].m_Index]) || m_pEditor->m_TeleNumbers[m_pTiles[TgtIndex].m_Index] != pLt->m_TeleNumbers[m_pTiles[TgtIndex].m_Index])
else if((pLt->m_pTeleTile[SrcIndex].m_Number == 0 && m_pEditor->m_TeleNumbers[m_pTiles[TgtIndex].m_Index]))
m_pTeleTile[TgtIndex].m_Number = m_pEditor->m_TeleNumbers[m_pTiles[TgtIndex].m_Index];
else
m_pTeleTile[TgtIndex].m_Number = pLt->m_pTeleTile[SrcIndex].m_Number;

View file

@ -299,24 +299,33 @@ int CLayerTiles::BrushGrab(std::shared_ptr<CLayerGroup> pBrush, CUIRect Rect)
pBrush->AddLayer(pGrabbed);
// copy the tiles
for(int y = 0; y < r.h; y++)
{
for(int x = 0; x < r.w; x++)
{
// copy the tiles
pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);
// copy the tele data
if(!m_pEditor->Input()->KeyIsPressed(KEY_SPACE))
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
// copy the tele data
if(!m_pEditor->Input()->KeyIsPressed(KEY_SPACE))
{
pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x] = static_cast<CLayerTele *>(this)->m_pTeleTile[(r.y + y) * m_Width + (r.x + x)];
unsigned char TgtIndex = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type;
if(IsValidTeleTile(TgtIndex))
if(IsValidTeleTile(TgtIndex) && IsTeleTileNumberUsedAny(TgtIndex))
{
if(IsTeleTileNumberUsedAny(TgtIndex))
m_pEditor->m_TeleNumbers[TgtIndex] = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
m_pEditor->m_TeleNumbers[TgtIndex] = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
}
}
else
{
CTile Tile = pGrabbed->m_pTiles[y * pGrabbed->m_Width + x];
if(IsValidTeleTile(Tile.m_Index) && IsTeleTileNumberUsedAny(Tile.m_Index))
{
pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number = m_pEditor->m_TeleNumbers[Tile.m_Index];
}
}
}
}
pGrabbed->m_TeleNumbers = m_pEditor->m_TeleNumbers;

Some files were not shown because too many files have changed in this diff Show more