Compare commits

...

118 commits

Author SHA1 Message Date
Alexei Kotov
55107e0913 Merge branch 'idle_select_to_group_name' into 'master'
Some checks are pending
Build and test / Ubuntu (push) Waiting to run
Build and test / MacOS (push) Waiting to run
Build and test / Read .env file and expose it as output (push) Waiting to run
Build and test / Windows (2019) (push) Blocked by required conditions
Build and test / Windows (2022) (push) Blocked by required conditions
Use string_view for sIdleSelectToGroupName

See merge request OpenMW/openmw!4646
2025-04-28 03:02:17 +03:00
Alexei Kotov
011dfb305c Merge branch 'navmeshtool_lib' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Do not build navmeshtool translation units twice

See merge request OpenMW/openmw!4647
2025-04-25 21:40:57 +03:00
elsid
fc4cc3255d
Do not build navmeshtool translation units twice 2025-04-24 23:03:07 +02:00
elsid
f487a6332b
Use string_view for sIdleSelectToGroupName 2025-04-24 23:01:30 +02:00
psi29a
80d6f020ed Merge branch 'apt_get_retry' into 'master'
Some checks failed
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Ubuntu (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Retry apt-get update and add-apt-repository

See merge request OpenMW/openmw!4639
2025-04-24 09:07:20 +00:00
psi29a
c7c95c5a85 Merge branch 'fix-bug-8462' into 'master'
Set SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH.

Closes #8225 and #8462

See merge request OpenMW/openmw!4641
2025-04-24 09:06:27 +00:00
Sam Kaufman
1948ab21f7 Set SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH.
Some checks failed
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Ubuntu (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
This fixes bugs #8225 and #8462.
2025-04-23 20:33:56 -07:00
Alexei Kotov
ca3a286cc4 Merge branch 'master' into 'master'
Some checks are pending
Build and test / MacOS (push) Waiting to run
Build and test / Read .env file and expose it as output (push) Waiting to run
Build and test / Windows (2019) (push) Blocked by required conditions
Build and test / Windows (2022) (push) Blocked by required conditions
Build and test / Ubuntu (push) Waiting to run
FIX: Clarify that `ignore` field of raycast options types are a list and not a single object

See merge request OpenMW/openmw!4613
2025-04-24 02:09:47 +03:00
Evil Eye
ab5070328d Merge branch 'powershell-multiline-exit' into 'master'
Detect failures in multiline PowerShell commands

See merge request OpenMW/openmw!4645
2025-04-23 18:54:47 +00:00
Evil Eye
23522ed314 Merge branch 'missing_enum' into 'master'
Add missing TargetPolygonNotFound enum value to lua bindings

See merge request OpenMW/openmw!4643
2025-04-23 18:54:46 +00:00
Evil Eye
b4d5013679 Merge branch 'lua_state' into 'master'
Use unique_ptr to handle lua state lifetime

See merge request OpenMW/openmw!4636
2025-04-23 18:54:40 +00:00
Dave Corley
5ef2cf23b3 CLEANUP: Loop param, also, is a bool
Some checks failed
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Ubuntu (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
2025-04-23 11:20:46 -07:00
elsid
8ee0c9e7be
Retry apt-get update and add-apt-repository
Some checks failed
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Ubuntu (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
2025-04-22 23:05:27 +02:00
AnyOldName3
19725473d7 Detect failures in multiline PowerShell commands
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
GitLab inserts a check for failure after each command in our `script`.
This is documented here https://docs.gitlab.com/runner/shells/#powershell

However, it doesn't detect failures if we run commands back to back.

This adds the checks GitLab would have added for us if we were able to make it do that.
2025-04-22 20:58:43 +01:00
psi29a
aed135a7c0 Merge branch 'bump-cache-key' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Increment cache keys missed in !4450

Closes #8463

See merge request OpenMW/openmw!4644
2025-04-22 07:05:45 +00:00
AnyOldName3
928bbed09b Increment cache keys missed in !4450
It changed the filenames for deps, so we've got two copies of the deps in the cache, and now we're running out of disk space.
2025-04-21 22:52:24 +01:00
elsid
626d7b2282
Add missing TargetPolygonNotFound enum value to lua bindings 2025-04-21 16:36:19 +02:00
Evil Eye
e20d52d23c Merge branch 'namespace_typo' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Fix typo in namespace name

See merge request OpenMW/openmw!4635
2025-04-21 08:41:27 +00:00
Evil Eye
05c624bc34 Merge branch 'rm_using_namespace_fallback' into 'master'
Remove redundant using namespace Fallback

See merge request OpenMW/openmw!4638
2025-04-21 08:39:48 +00:00
Evil Eye
5a42db3256 Merge branch 'static_assert' into 'master'
Use static_assert for compile time check

See merge request OpenMW/openmw!4637
2025-04-21 08:35:17 +00:00
elsid
6f89d38b78
Replace includes by forward declaration 2025-04-20 02:28:34 +02:00
elsid
042c4b2b9d
Use static_assert for compile time check 2025-04-18 14:38:23 +02:00
elsid
f80283422f
Use unique_ptr to handle lua state lifetime 2025-04-18 14:01:07 +02:00
elsid
972995d124
Fix typo in namespace name 2025-04-18 12:27:48 +02:00
elsid
0d5e9ef85f
Remove redundant using namespace Fallback
C++ has ADL to find overloads. using namespace does nothing in this
case.
2025-04-18 12:27:37 +02:00
psi29a
87d77a6882 Merge branch 'rippleshaders' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Fix crash if ripple pipeline shaders are unavailable

See merge request OpenMW/openmw!4622
2025-04-17 09:26:12 +00:00
Alexei Kotov
c1c8769742 Merge branch 'clang_tidy_identifier_naming' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Enable identifier naming clang-tidy check (#8424)

See merge request OpenMW/openmw!4631
2025-04-14 13:39:09 +03:00
Alexei Kotov
271ab2e109 Merge branch 'dont-ask-the-disk-for-things-we-already-know' into 'master'
Avoid IO in resolveParentFileIndices

See merge request OpenMW/openmw!4627
2025-04-14 13:11:57 +03:00
Alexei Kotov
583620e607 Merge branch 'patch-1' into 'master'
Crimes.lua wrong type for victim aware

See merge request OpenMW/openmw!4623
2025-04-14 09:44:45 +03:00
psi29a
6dd2cac3ec Merge branch 'better-freeze-catcher' into 'master'
Some checks are pending
Build and test / Ubuntu (push) Waiting to run
Build and test / MacOS (push) Waiting to run
Build and test / Read .env file and expose it as output (push) Waiting to run
Build and test / Windows (2019) (push) Blocked by required conditions
Build and test / Windows (2022) (push) Blocked by required conditions
Improve Windows crash/freeze catcher UX

See merge request OpenMW/openmw!4630
2025-04-13 20:57:12 +00:00
Alexei Kotov
6aed2d8284 Bump Crimes interface version 2025-04-13 14:05:07 +03:00
elsid
84f471ce5c
Enable identifier naming clang-tidy check 2025-04-13 09:59:51 +02:00
AnyOldName3
deb070389f Improve Windows crash/freeze catcher UX
* Change crash log to crash dump in messages.
* Make the freeze catcher popup disappear more quickly when OpenMW thaws - we got a few freeze dumps from after a thaw.
* Improve freeze catcher message - hopefully fewer users think it's a false positive they're expected to put up with and we get future reports sooner.
2025-04-12 18:38:55 +01:00
Alexei Kotov
f7f148a6ca Merge branch 'fix-debug-config' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Fix Windows Debug build

See merge request OpenMW/openmw!4629
2025-04-12 17:35:03 +03:00
AnyOldName3
a5a6f33578 Manual reformatting that wasn't done automatically on my machine 2025-04-11 17:41:40 +01:00
AnyOldName3
d74a0edb82 Format 2025-04-11 17:37:55 +01:00
AnyOldName3
396cd1c727 Fix Windows Debug build
This was a regression from https://gitlab.com/OpenMW/openmw/-/merge_requests/4596

Also move more things into the anonymous namespace because there's not really a reason not to and I had to rearrange things anyway.
2025-04-11 17:33:19 +01:00
AnyOldName3
31fcc5e126 Add test for new ReadersCache functions 2025-04-11 17:30:56 +01:00
Dave Corley
22172f3b0e FIX: useAmbientLighting arg of addVfx options is a bool 2025-04-10 11:48:57 -07:00
Dave Corley
b68935e917 FIX: Model param of addVfx should not be a header 2025-04-10 11:44:34 -07:00
Dave Corley
e512a8e74f FIX: Add a name to options table in castRenderingRay 2025-04-10 11:42:24 -07:00
AnyOldName3
48572e4c96 Even more elses 2025-04-10 18:32:52 +01:00
Dave Corley
1d1ae1c906 CLEANUP: But it can also be a single object 2025-04-10 10:15:31 -07:00
Dave Corley
586467540b FIX: Clarify that ignore field of raycast options types are a list and not a single object 2025-04-10 10:15:31 -07:00
AnyOldName3
37dc1a6a76 Remove redundant elses 2025-04-10 16:51:23 +01:00
AnyOldName3
15162a734d Avoid IO in resolveParentFileIndices
In the olden days, we passed it a vector of open ESMReader instances, as they knew the filenames and sizes, so were a convenient source of this knowledge.

When the ReadersCache was introduced as a pool of readers to limit the maximum number of simultaneously open file handles (to avoid going over the OS' limit) it was a poor substitute.
* We iterate over all the earlier readers in order in a double loop, which is the worst case scenario for an LRU pool as once we're past the size limit, we're guaranteed maximum thrashing - the least recently used item is the most likely to be used next, so the worst to evict.
* We didn't want to read any ESM files, just know whether they'd been read and what their sizes were, so didn't want to open a file handle, which the ReadersCache forced us to do.

Obviously, opening lots of file handles isn't fast, and as this was an operation done for each content file which iterated over the file's masters and within that loop iterated over every loaded file, that's O(n^3) complexity in the worst case, and for things like delta plugin merged plugins, they hit the worst case in long load orders.

This resolves the freeze reported as https://gitlab.com/OpenMW/openmw/-/issues/8425, but there may be other freezes on launch.
2025-04-10 16:16:19 +01:00
psi29a
ea8369eff0 Merge branch 'dont-redraw-the-whole-gui-every-time-we-change-the-tiniest-thing' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Be more careful when we tell Qt that data has changed

Closes #8405

See merge request OpenMW/openmw!4621
2025-04-09 11:19:32 +00:00
AnyOldName3
096759435a Add progress bars where the launcher can be limited by IO
I tested this with a USB3 external hard drive.

These two places were the only ones where we're IO-bound and block the main thread, so they're the only ones that need progress bars.

If trying to replicate this test, then it's important to unplug the hard drive between each repeat.
Apparently Windows is excellent at disk caching these days as it takes a minute and a half to start the launcher with Total Overhaul on this drive when it's just been plugged in, but less time than the first launch after a reboot on an NVME drive once the cache has been warmed up.
2025-04-09 01:36:52 +01:00
AnyOldName3
894ea4ba62 Don't precompute load order errors after every change
It's much slower than doing it on demand as it only takes a microsecond, but for a really big load order, there are hundreds of thousands of intermediate calls before everything's set up and we can draw the GUI.
2025-04-08 01:19:24 +01:00
AnyOldName3
d6b61f1f54 Sprinkle some const&
QStringView required more fighting as loads of call sites take a const&
2025-04-08 00:34:45 +01:00
AnyOldName3
e779f115ef Exclude directories from containsDataFiles
Also include capo's microoptimisation even though it doesn't make things any faster.
2025-04-07 16:11:27 +01:00
psi29a
428044abe2 Merge branch 'moveoutalready' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Open the data directory file picker at the last opened location

Closes #8426

See merge request OpenMW/openmw!4606
2025-04-07 11:04:26 +00:00
psi29a
bd1c2a11d7 Merge branch 'herbalfish' into 'master'
Fix some graphic herbalism issues

See merge request OpenMW/openmw!4624
2025-04-07 11:03:55 +00:00
psi29a
0c4c202998 Merge branch 'ifavideofallsintheforest' into 'master'
Pause menu video playback when OpenMW is minimized

Closes #8441

See merge request OpenMW/openmw!4625
2025-04-07 11:03:37 +00:00
Alexei Kotov
8419116cae Fix crash if ripple pipeline shaders are unavailable 2025-04-07 03:31:32 +03:00
psi29a
8a0f513094 Merge branch 'fix_clang_tidy' into 'master'
Some checks are pending
Build and test / Ubuntu (push) Waiting to run
Build and test / MacOS (push) Waiting to run
Build and test / Read .env file and expose it as output (push) Waiting to run
Build and test / Windows (2019) (push) Blocked by required conditions
Build and test / Windows (2022) (push) Blocked by required conditions
Fix clang tidy checks

See merge request OpenMW/openmw!4619
2025-04-06 17:54:20 +00:00
Evil Eye
1667b11564 Pause menu video playback when OpenMW is minimized 2025-04-06 19:42:04 +02:00
psi29a
b29d89bd6a Merge branch 'enchantment404' into 'master'
Allow referenced enchantments to be missing on equipped items

See merge request OpenMW/openmw!4617
2025-04-06 16:06:10 +00:00
psi29a
211a5e5bda Merge branch 'lightminimumradius' into 'master'
Give point lights a minimum radius of 16

See merge request OpenMW/openmw!4601
2025-04-06 16:03:35 +00:00
psi29a
2ed14de41f Merge branch 'objectrootless' into 'master'
Account for creatures not having a model in more places

Closes #8439

See merge request OpenMW/openmw!4618
2025-04-06 16:01:50 +00:00
Evil Eye
d826962eaa Don't assume unresolved containers contain no visible items 2025-04-06 11:02:31 +02:00
Evil Eye
962ef91e25 Allow skinned plants to be harvested 2025-04-06 11:01:26 +02:00
Chronolegionnaire
ed62f9b12b Lua api demands a boolean for victim aware but crimes.lua looks for a number. Which makes scripts that call the crime interface unable to provide a value other than nil for victim aware. 2025-04-06 06:27:40 +00:00
AnyOldName3
973282e471 Optimise ContentSelectorModel::ContentModel::item
This saves about 5% of remaining launcher startup time

Not using fileProperty avoids loads of QVariant conversions.
2025-04-06 02:46:31 +01:00
AnyOldName3
7bad2864d9 Reuse QIcon
This saves more than 15% of launcher startup time on my machine (after the prior improvements - it's way less without those)
2025-04-06 02:40:42 +01:00
AnyOldName3
1237746549 Be more careful when we tell Qt that data has changed
Unchecking files only changes whether they're checked, and doesn't completely rearrange the table and change the number of elements it has, so we only need to change the check state, not the whole layout.

It's way faster to just query all the data once after setting a content list than it is to query the data for all files between the old and new location of a file when we change any file's location in the load order.
2025-04-06 01:31:05 +01:00
elsid
7254bb74a4
Enable modernize-avoid-bind clang-tidy check 2025-04-05 12:55:18 +02:00
elsid
3af2091b28
Use prefix with dot for clang-analyzer-optin. checks 2025-04-05 12:55:18 +02:00
elsid
621a0a15a3
Disable clang-analyzer-cplusplus.NewDelete clang-tidy check 2025-04-05 12:55:18 +02:00
elsid
c34b0f90d7
Avoid clang-tidy checks duplication 2025-04-05 12:55:17 +02:00
elsid
e098770ba2
Use custom clang-tidy config for extern/ 2025-04-05 12:55:17 +02:00
elsid
7c45a564a1
Fix clang-analyzer-deadcode.DeadStores 2025-04-05 12:55:17 +02:00
elsid
da388c93eb
Remove boost-* clang-tidy checks
There are only:
* https://clang.llvm.org/extra/clang-tidy/checks/boost/use-ranges.html
* https://clang.llvm.org/extra/clang-tidy/checks/boost/use-to-string.html

None of them makes sense in this project.
2025-04-05 12:55:17 +02:00
elsid
d609bd1ab1
Fix clang-tidy header filter 2025-04-05 12:55:17 +02:00
Evil Eye
15f4368fe6 Account for creatures not having a model in more places 2025-04-05 10:19:50 +02:00
Alexei Kotov
3901084cc2 Merge branch 'cmake_minimum_required_macos' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Remove cmake_minimum_required for osx install script

See merge request OpenMW/openmw!4615
2025-04-05 08:54:09 +03:00
Alexei Kotov
065a388632 Allow enchantments to be missing on equipped items 2025-04-05 01:10:41 +03:00
elsid
9a6807f862
Remove cmake_minimum_required for osx install script
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
2025-04-03 23:01:36 +02:00
Alexei Kotov
3523ba564a Merge branch 'non-deprecated-known-folder-api' into 'master'
Some checks failed
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Use non-deprecated known folder API

See merge request OpenMW/openmw!4603
2025-04-02 19:20:21 +03:00
Alexei Kotov
1629ea32f7 Merge branch 'rm_unused' into 'master'
Some checks are pending
Build and test / Ubuntu (push) Waiting to run
Build and test / MacOS (push) Waiting to run
Build and test / Read .env file and expose it as output (push) Waiting to run
Build and test / Windows (2019) (push) Blocked by required conditions
Build and test / Windows (2022) (push) Blocked by required conditions
Remove declaration without definition

See merge request OpenMW/openmw!4607
2025-04-01 23:35:49 +03:00
Evil Eye
86426aa87b Open the data directory file picker at the last opened location 2025-03-31 17:11:09 +02:00
elsid
a61ce111a5
Remove declaration without definition 2025-03-31 00:28:06 +02:00
Alexei Kotov
11c2fd9e3d Merge branch 'master' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
FIX: Remove outdated instructions for ubuntu installation

See merge request OpenMW/openmw!4605
2025-03-29 02:47:16 +03:00
Dave Corley
b6be7cdd56 CLEANUP: Use apt instead of apt-get 2025-03-28 22:27:30 +00:00
Dave Corley
caef91d261 FIX: Remove outdated instructions for ubuntu installation 2025-03-27 14:55:05 -07:00
psi29a
72aefbf191 Merge branch 'lua_save_load_test' into 'master'
Some checks failed
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Ubuntu (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Add Lua integration tests for loading and saving

See merge request OpenMW/openmw!4604
2025-03-27 11:05:12 +00:00
psi29a
747771ac5e Merge branch 'fontexport' into 'master'
Restore --export-fonts option functionality

See merge request OpenMW/openmw!4561
2025-03-27 11:04:35 +00:00
psi29a
5f413e7b4d Merge branch 'navigator_debug_mesh' into 'master'
Make navigator debug meshes generation safer

See merge request OpenMW/openmw!4602
2025-03-27 11:04:18 +00:00
elsid
2ebdc43bbe
Add test for load while teleporting
To reproduce #8311.

Load game while landracer is scheduled to teleport from different cell.
2025-03-25 23:41:04 +01:00
elsid
536325e0ba
Add test for saving and loading the game 2025-03-25 23:40:58 +01:00
elsid
cbcd4f6acd
Move matchers to different module 2025-03-25 23:24:24 +01:00
Evil Eye
9570b29a0a Merge branch 'lua_test_menu' into 'master'
Some checks failed
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Ubuntu (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Run Lua integration tests starting with menu script

See merge request OpenMW/openmw!4556
2025-03-25 16:54:41 +00:00
AnyOldName3
166852254f Use non-deprecated known folder API
SHGetFolderPathW was deprecated in Windows Vista nearly two decades ago. ShGetKnownFolderPath is the replacement.

Also log if there was an error. Someone seemed to be getting an error on Discord, despite other apps being able to get the path just fine with these functions.

Also don't pass the flags to create the folders if they don't exist. We probably don't have the right permissions and if they don't exist, then there are bigger problems. Maybe this will fix the issue the user was having.

Also add a comment about global config on Windows being fundamentally wrong.
2025-03-25 01:32:44 +00:00
Alexei Kotov
f8be5fdd2a Give point lights a minimum radius of 16 2025-03-24 22:14:08 +03:00
elsid
ada48d9021
Reduce a chance to have a deadlock in the AsyncNavMeshUpdater
* Do not fail tile generation if debug mesh writing fails.
* Mark some functions as noexcept to better crash than have a deadlock.
* Unlock tile and remove job if there on exception while processing it.
2025-03-23 23:33:40 +01:00
elsid
7112217adc
Use temporary directory for tests output 2025-03-23 23:33:40 +01:00
elsid
87a2f776b7
Add version to the recast log prefix 2025-03-23 23:33:39 +01:00
psi29a
d13f108779 Merge branch 'nbsp' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Non-breaking space-related fixes (#8378)

Closes #8378

See merge request OpenMW/openmw!4592
2025-03-23 16:27:07 +00:00
Evil Eye
57fb334a6e Merge branch 'recast_log_level' into 'master'
Support max log level for Recast via env variable

See merge request OpenMW/openmw!4596
2025-03-23 14:50:11 +00:00
Alexei Kotov
88c673de51 Merge branch 'luadoc' into 'master'
Some checks are pending
Build and test / Ubuntu (push) Waiting to run
Build and test / MacOS (push) Waiting to run
Build and test / Read .env file and expose it as output (push) Waiting to run
Build and test / Windows (2019) (push) Blocked by required conditions
Build and test / Windows (2022) (push) Blocked by required conditions
Fix minor documentation errors

See merge request OpenMW/openmw!4595
2025-03-22 14:22:51 +03:00
elsid
51258662b5
Support max log level for Recast via env variable
Do not write to log if log message level is greater than one speficied
in the OPENMW_RECAST_MAX_LOG_LEVEL env variable. Use Error by default.
2025-03-21 15:34:47 +01:00
Evil Eye
e5e21eef20 Fix minor documentation errors 2025-03-20 20:08:31 +01:00
psi29a
057c85b710 Merge branch 'esmtool_qust_subrecords' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Skip SLSD, SCVR, SCRV subrecords in QUST record

See merge request OpenMW/openmw!4589
2025-03-18 13:18:15 +00:00
psi29a
241a24564a Merge branch 'doubleexit' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled
Don't assume there is a GUI mode in exitCurrentGuiMode (#8380)

Closes #8380

See merge request OpenMW/openmw!4590
2025-03-18 13:16:23 +00:00
Alexei Kotov
cd3980eca4 Make figure space non-breaking 2025-03-18 10:29:29 +03:00
Alexei Kotov
c2744a1846 Change substitute character in Mystic Cards from question mark to underscore 2025-03-18 00:50:03 +03:00
Alexei Kotov
8d0dcb774f Add no-break space to MysticCards 2025-03-18 00:49:51 +03:00
Alexei Kotov
b5a2a4e52d Render no-break space in books, don't consider narrow NBSP breaking 2025-03-17 22:03:38 +03:00
Alexei Kotov
e4ae0c9a95 Don't assume there is a GUI mode in exitCurrentGuiMode (#8380) 2025-03-16 09:06:01 +03:00
elsid
e5f6b77c29
Skip SLSD, SCVR, SCRV subrecords in QUST record
Present in:

Fallout 3 GOTY English/Data/Anchorage.esm
Fallout 3 GOTY English/Data/BrokenSteel.esm
Fallout 3 GOTY English/Data/PointLookout.esm
Fallout 3 GOTY English/Data/ThePitt.esm
Fallout 3 GOTY English/Data/Zeta.esm
2025-03-15 13:11:26 +01:00
elsid
2892e19c43
Run integration tests with verbose output 2025-03-08 13:14:23 +01:00
elsid
0e19b1dd75
Run Lua integration tests starting with menu script
This allows writing tests for menu scripts.

Keep global script as entry point to morrowind tests.

Fix menu.newGame and menu.loadGame to hide main menu.
2025-03-08 13:14:20 +01:00
elsid
7a9c2d5e88
Split local and global event handlers 2025-03-05 22:18:46 +01:00
elsid
981ca957c1
Register global tests to run them 2025-03-05 22:18:46 +01:00
elsid
8b62f02523
Use world.players to initialize player in global tests 2025-03-05 22:18:46 +01:00
elsid
c298210844
Make integration_tests.py output more verbose
* Make it look more like googletest.
* Print total and failed number of tests.
* Print failed tests names.
* Print duration of each test and total.
* Hide all logs by default.
2025-03-05 22:18:46 +01:00
elsid
f80c7b2355
Expect openmw.cfg to exist 2025-03-05 22:18:47 +01:00
Alexei Kotov
dc3264a3a5 Restore --export-fonts option functionality 2025-03-02 22:33:06 +03:00
106 changed files with 1528 additions and 837 deletions

View file

@ -1,18 +1,15 @@
Checks: >
-*,
boost-*,
portability-*,
clang-analyzer-*,
-clang-analyzer-optin*,
-clang-analyzer-optin.*,
-clang-analyzer-cplusplus.NewDeleteLeaks,
-clang-analyzer-cplusplus.NewDelete,
-clang-analyzer-core.CallAndMessage,
-modernize-avoid-bind
WarningsAsErrors: >
-*,
boost-*,
portability-*,
clang-analyzer-*,
-clang-analyzer-optin*,
-clang-analyzer-cplusplus.NewDeleteLeaks,
-clang-analyzer-core.CallAndMessage
HeaderFilterRegex: '^(apps|components)'
modernize-avoid-bind,
readability-identifier-naming
WarningsAsErrors: '*'
HeaderFilterRegex: '(apps|components)/'
CheckOptions:
- key: readability-identifier-naming.ConceptCase
value: CamelCase

View file

@ -632,12 +632,16 @@ macOS14_Xcode15_arm64:
- |
if (Get-ChildItem -Recurse *.pdb) {
7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt
if(!$?) { Exit $LASTEXITCODE }
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory}
if(!$?) { Exit $LASTEXITCODE }
}
Push-Location ..
..\CI\Store-Symbols.ps1 -SkipCompress
if(!$?) { Exit $LASTEXITCODE }
7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt
if(!$?) { Exit $LASTEXITCODE }
Pop-Location
Get-ChildItem -Recurse *.pdb | Remove-Item
}
@ -645,13 +649,20 @@ macOS14_Xcode15_arm64:
- |
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory}
if(!$?) { Exit $LASTEXITCODE }
}
- |
if ($executables) {
foreach ($exe in $executables.Split(',')) {
& .\$exe
if(!$?) { Exit $LASTEXITCODE }
}
}
- if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } }
after_script:
- Get-Volume
- Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log
cache:
key: ninja-2022-v11
key: ninja-2022-v12
paths:
- ccache
- deps
@ -779,12 +790,16 @@ macOS14_Xcode15_arm64:
- |
if (Get-ChildItem -Recurse *.pdb) {
7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt
if(!$?) { Exit $LASTEXITCODE }
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory}
if(!$?) { Exit $LASTEXITCODE }
}
Push-Location ..
..\CI\Store-Symbols.ps1 -SkipCompress
if(!$?) { Exit $LASTEXITCODE }
7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt
if(!$?) { Exit $LASTEXITCODE }
Pop-Location
Get-ChildItem -Recurse *.pdb | Remove-Item
}
@ -792,13 +807,20 @@ macOS14_Xcode15_arm64:
- |
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory}
if(!$?) { Exit $LASTEXITCODE }
}
- |
if ($executables) {
foreach ($exe in $executables.Split(',')) {
& .\$exe
if(!$?) { Exit $LASTEXITCODE }
}
}
- if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } }
after_script:
- Get-Volume
- Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log
cache:
key: msbuild-2022-v11
key: msbuild-2022-v12
paths:
- deps
- MSVC2022_64/deps/Qt

View file

@ -229,6 +229,9 @@
Bug #8299: Crash while smoothing landscape
Bug #8364: Crash when clicking scrollbar without handle (divide by zero)
Bug #8378: Korean bitmap fonts are unusable
Bug #8439: Creatures without models can crash the game
Bug #8441: Freeze when using video main menu replacers
Bug #8462: Crashes when resizing the window on macOS
Feature #1415: Infinite fall failsafe
Feature #2566: Handle NAM9 records for manual cell references
Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking

View file

@ -38,7 +38,7 @@ fi
if [[ $CI_CLANG_TIDY ]]; then
CMAKE_CONF_OPTS+=(
-DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*"
-DCMAKE_CXX_CLANG_TIDY=clang-tidy
-DBUILD_COMPONENTS_TESTS=ON
-DBUILD_OPENMW_TESTS=ON
-DBUILD_OPENCS_TESTS=ON

View file

@ -126,10 +126,24 @@ export APT_CACHE_DIR="${PWD}/apt-cache"
export DEBIAN_FRONTEND=noninteractive
set -x
mkdir -pv "$APT_CACHE_DIR"
apt-get update -yqq
while true; do
apt-get update -yqq && break
done
apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends software-properties-common gnupg >/dev/null
add-apt-repository -y ppa:openmw/openmw
add-apt-repository -y ppa:openmw/openmw-daily
add-apt-repository -y ppa:openmw/staging
while true; do
add-apt-repository -y ppa:openmw/openmw && break
done
while true; do
add-apt-repository -y ppa:openmw/openmw-daily && break
done
while true; do
add-apt-repository -y ppa:openmw/staging && break
done
apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null
apt list --installed

View file

@ -9,7 +9,7 @@ git checkout FETCH_HEAD
cd ..
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24x60' \
scripts/integration_tests.py --omw build/install/bin/openmw --workdir integration_tests_output example-suite/
scripts/integration_tests.py --verbose --omw build/install/bin/openmw --workdir integration_tests_output example-suite/
ls integration_tests_output/*.osg_stats.log | while read v; do
scripts/osg_stats.py --stats '.*' --regexp_match < "${v}"

View file

@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...")
set(OPENMW_VERSION_MAJOR 0)
set(OPENMW_VERSION_MINOR 49)
set(OPENMW_VERSION_RELEASE 0)
set(OPENMW_LUA_API_REVISION 71)
set(OPENMW_LUA_API_REVISION 72)
set(OPENMW_POSTPROCESSING_API_REVISION 2)
set(OPENMW_VERSION_COMMITHASH "")
@ -860,7 +860,6 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE)
set(BU_CHMOD_BUNDLE_ITEMS ON)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH})
include(BundleUtilities)
cmake_minimum_required(VERSION 3.1)
" COMPONENT Runtime)
set(ABSOLUTE_PLUGINS "")

View file

@ -179,7 +179,7 @@ namespace
generateKeys(std::back_inserter(keys), keys.size() * (100 - hitPercentage) / 100, random);
std::size_t n = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
const auto& key = keys[n++ % keys.size()];
auto result = cache.get(key.mAgentBounds, key.mTilePosition, key.mRecastMesh);

View file

@ -104,7 +104,7 @@ namespace
std::minstd_rand random;
std::vector<ESM::RefId> refIds = generateStringRefIds(state.range(0), random);
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(refIds[i].serialize());
if (++i >= refIds.size())
@ -118,7 +118,7 @@ namespace
std::vector<std::string> serializedRefIds
= generateSerializedStringRefIds(state.range(0), random, [](ESM::RefId v) { return v.serialize(); });
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(ESM::RefId::deserialize(serializedRefIds[i]));
if (++i >= serializedRefIds.size())
@ -131,7 +131,7 @@ namespace
std::minstd_rand random;
std::vector<ESM::RefId> refIds = generateStringRefIds(state.range(0), random);
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(refIds[i].serializeText());
if (++i >= refIds.size())
@ -145,7 +145,7 @@ namespace
std::vector<std::string> serializedRefIds
= generateSerializedStringRefIds(state.range(0), random, [](ESM::RefId v) { return v.serializeText(); });
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i]));
if (++i >= serializedRefIds.size())
@ -158,7 +158,7 @@ namespace
std::minstd_rand random;
std::vector<ESM::RefId> refIds = generateGeneratedRefIds(random);
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(refIds[i].serializeText());
if (++i >= refIds.size())
@ -172,7 +172,7 @@ namespace
std::vector<std::string> serializedRefIds
= generateSerializedGeneratedRefIds(random, [](ESM::RefId v) { return v.serializeText(); });
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i]));
if (++i >= serializedRefIds.size())
@ -185,7 +185,7 @@ namespace
std::minstd_rand random;
std::vector<ESM::RefId> refIds = generateIndexRefIds(random);
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(refIds[i].serializeText());
if (++i >= refIds.size())
@ -199,7 +199,7 @@ namespace
std::vector<std::string> serializedRefIds
= generateSerializedIndexRefIds(random, [](ESM::RefId v) { return v.serializeText(); });
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i]));
if (++i >= serializedRefIds.size())
@ -212,7 +212,7 @@ namespace
std::minstd_rand random;
std::vector<ESM::RefId> refIds = generateESM3ExteriorCellRefIds(random);
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(refIds[i].serializeText());
if (++i >= refIds.size())
@ -226,7 +226,7 @@ namespace
std::vector<std::string> serializedRefIds
= generateSerializedESM3ExteriorCellRefIds(random, [](ESM::RefId v) { return v.serializeText(); });
std::size_t i = 0;
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(ESM::RefId::deserializeText(serializedRefIds[i]));
if (++i >= serializedRefIds.size())

View file

@ -9,7 +9,7 @@ namespace
{
void settingsManager(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(Settings::Manager::getFloat("sky blending start", "Fog"));
}
@ -17,7 +17,7 @@ namespace
void settingsManager2(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(Settings::Manager::getFloat("near clip", "Camera"));
benchmark::DoNotOptimize(Settings::Manager::getBool("transparent postpass", "Post Processing"));
@ -26,7 +26,7 @@ namespace
void settingsManager3(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(Settings::Manager::getFloat("near clip", "Camera"));
benchmark::DoNotOptimize(Settings::Manager::getBool("transparent postpass", "Post Processing"));
@ -36,7 +36,7 @@ namespace
void localStatic(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
static float v = Settings::Manager::getFloat("sky blending start", "Fog");
benchmark::DoNotOptimize(v);
@ -45,7 +45,7 @@ namespace
void localStatic2(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
static float v1 = Settings::Manager::getFloat("near clip", "Camera");
static bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing");
@ -56,7 +56,7 @@ namespace
void localStatic3(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
static float v1 = Settings::Manager::getFloat("near clip", "Camera");
static bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing");
@ -69,7 +69,7 @@ namespace
void settingsStorage(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
float v = Settings::fog().mSkyBlendingStart.get();
benchmark::DoNotOptimize(v);
@ -78,7 +78,7 @@ namespace
void settingsStorage2(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
bool v1 = Settings::postProcessing().mTransparentPostpass.get();
float v2 = Settings::camera().mNearClip.get();
@ -89,7 +89,7 @@ namespace
void settingsStorage3(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
bool v1 = Settings::postProcessing().mTransparentPostpass.get();
float v2 = Settings::camera().mNearClip.get();
@ -102,7 +102,7 @@ namespace
void settingsStorageGet(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(Settings::get<float>("Fog", "sky blending start"));
}
@ -110,7 +110,7 @@ namespace
void settingsStorageGet2(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(Settings::get<bool>("Post Processing", "transparent postpass"));
benchmark::DoNotOptimize(Settings::get<float>("Camera", "near clip"));
@ -119,7 +119,7 @@ namespace
void settingsStorageGet3(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto _ : state)
{
benchmark::DoNotOptimize(Settings::get<bool>("Post Processing", "transparent postpass"));
benchmark::DoNotOptimize(Settings::get<float>("Camera", "near clip"));

View file

@ -53,8 +53,6 @@ namespace
bpo::options_description makeOptionsDescription()
{
using Fallback::FallbackMap;
bpo::options_description result;
auto addOption = result.add_options();
addOption("help", "print help message");
@ -87,7 +85,8 @@ namespace
"\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n"
"\n\twin1252 - Western European (Latin) alphabet, used by default");
addOption("fallback", bpo::value<FallbackMap>()->default_value(FallbackMap(), "")->multitoken()->composing(),
addOption("fallback",
bpo::value<Fallback::FallbackMap>()->default_value(Fallback::FallbackMap(), "")->multitoken()->composing(),
"fallback values");
Files::ConfigurationManager::addCommonOptions(result);

View file

@ -5,7 +5,9 @@
#include <components/detournavigator/makenavmesh.hpp>
#include <components/detournavigator/navmeshdbutils.hpp>
#include <components/detournavigator/serialization.hpp>
#include <components/files/conversion.hpp>
#include <components/loadinglistener/loadinglistener.hpp>
#include <components/testing/util.hpp>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
@ -372,6 +374,106 @@ namespace
}
}
TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh)
{
mRecastMeshManager.setWorldspace(mWorldspace, nullptr);
addHeightFieldPlane(mRecastMeshManager);
mSettings.mEnableWriteRecastMeshToFile = true;
const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest");
mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/";
Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix;
AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(1, mSettings);
const std::map<TilePosition, ChangeType> changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } };
updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
updater.wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.obj"));
}
TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh_with_revision)
{
mRecastMeshManager.setWorldspace(mWorldspace, nullptr);
addHeightFieldPlane(mRecastMeshManager);
mSettings.mEnableWriteRecastMeshToFile = true;
mSettings.mEnableRecastMeshFileNameRevision = true;
const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest");
mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/";
Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix;
AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(1, mSettings);
const std::map<TilePosition, ChangeType> changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } };
updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
updater.wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.1.2.obj"));
}
TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_recast_mesh_to_absent_file_should_not_fail_tile_generation)
{
mRecastMeshManager.setWorldspace(mWorldspace, nullptr);
addHeightFieldPlane(mRecastMeshManager);
mSettings.mEnableWriteRecastMeshToFile = true;
const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent";
mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/";
Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix;
AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(1, mSettings);
const std::map<TilePosition, ChangeType> changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } };
updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
updater.wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u);
EXPECT_FALSE(std::filesystem::exists(dir));
}
TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh)
{
mRecastMeshManager.setWorldspace(mWorldspace, nullptr);
addHeightFieldPlane(mRecastMeshManager);
mSettings.mEnableWriteNavMeshToFile = true;
const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest");
mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/";
Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix;
AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(1, mSettings);
const std::map<TilePosition, ChangeType> changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } };
updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
updater.wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.bin"));
}
TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh_with_revision)
{
mRecastMeshManager.setWorldspace(mWorldspace, nullptr);
addHeightFieldPlane(mRecastMeshManager);
mSettings.mEnableWriteNavMeshToFile = true;
mSettings.mEnableNavMeshFileNameRevision = true;
const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest");
mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/";
Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix;
AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(1, mSettings);
const std::map<TilePosition, ChangeType> changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } };
updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
updater.wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.1.1.bin"));
}
TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_navmesh_to_absent_file_should_not_fail_tile_generation)
{
mRecastMeshManager.setWorldspace(mWorldspace, nullptr);
addHeightFieldPlane(mRecastMeshManager);
mSettings.mEnableWriteNavMeshToFile = true;
const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent";
mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/";
Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix;
AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr);
const auto navMeshCacheItem = std::make_shared<GuardedNavMeshCacheItem>(1, mSettings);
const std::map<TilePosition, ChangeType> changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } };
updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
updater.wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u);
EXPECT_FALSE(std::filesystem::exists(dir));
}
struct DetourNavigatorSpatialJobQueueTest : Test
{
const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, osg::Vec3f(1, 1, 1) };

View file

@ -88,4 +88,31 @@ namespace
EXPECT_EQ(reader->getFileOffset(), sInitialOffset);
}
}
TEST_F(ESM3ReadersCacheWithContentFile, CachedSizeAndName)
{
ESM::ReadersCache readers(2);
{
readers.get(0)->openRaw(std::make_unique<std::istringstream>("123"), "closed0.omwaddon");
readers.get(1)->openRaw(std::make_unique<std::istringstream>("12345"), "closed1.omwaddon");
readers.get(2)->openRaw(std::make_unique<std::istringstream>("1234567"), "free.omwaddon");
}
auto busy = readers.get(3);
busy->openRaw(std::make_unique<std::istringstream>("123456789"), "busy.omwaddon");
EXPECT_EQ(readers.getFileSize(0), 3);
EXPECT_EQ(readers.getName(0), "closed0.omwaddon");
EXPECT_EQ(readers.getFileSize(1), 5);
EXPECT_EQ(readers.getName(1), "closed1.omwaddon");
EXPECT_EQ(readers.getFileSize(2), 7);
EXPECT_EQ(readers.getName(2), "free.omwaddon");
EXPECT_EQ(readers.getFileSize(3), 9);
EXPECT_EQ(readers.getName(3), "busy.omwaddon");
// not-yet-seen indices give zero for their size
EXPECT_EQ(readers.getFileSize(4), 0);
}
}

View file

@ -30,7 +30,7 @@ namespace
TEST(FilesGetHash, shouldClearErrors)
{
const auto fileName = temporaryFilePath("fileName");
const auto fileName = outputFilePath("fileName");
std::string content;
std::fill_n(std::back_inserter(content), 1, 'a');
std::istringstream stream(content);
@ -41,7 +41,7 @@ namespace
TEST_P(FilesGetHash, shouldReturnHashForStringStream)
{
const auto fileName = temporaryFilePath("fileName");
const auto fileName = outputFilePath("fileName");
std::string content;
std::fill_n(std::back_inserter(content), GetParam().mSize, 'a');
std::istringstream stream(content);

View file

@ -2,6 +2,7 @@
#include <components/misc/strings/conversion.hpp>
#include <components/settings/parser.hpp>
#include <components/settings/values.hpp>
#include <components/testing/util.hpp>
#include <gtest/gtest.h>
@ -24,5 +25,9 @@ int main(int argc, char** argv)
Settings::StaticValues::init();
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
const int result = RUN_ALL_TESTS();
if (result == 0)
std::filesystem::remove_all(TestingOpenMW::outputDir());
return result;
}

View file

@ -16,7 +16,7 @@ namespace
ShaderManager mManager;
ShaderManager::DefineMap mDefines;
ShaderManagerTest() { mManager.setShaderPath("tests_output"); }
ShaderManagerTest() { mManager.setShaderPath(TestingOpenMW::outputDir()); }
template <class F>
void withShaderFile(const std::string& content, F&& f)

View file

@ -8,6 +8,7 @@
#include <QList>
#include <QMessageBox>
#include <QPair>
#include <QProgressDialog>
#include <QPushButton>
#include <algorithm>
@ -351,9 +352,17 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
if (!resourcesVfs.isEmpty())
directories.insert(0, { resourcesVfs });
QIcon containsDataIcon(":/images/openmw-plugin.png");
QProgressDialog progressBar("Adding data directories", {}, 0, directories.count(), this);
progressBar.setWindowModality(Qt::WindowModal);
progressBar.setValue(0);
std::unordered_set<QString> visitedDirectories;
for (const Config::SettingValue& currentDir : directories)
{
progressBar.setValue(progressBar.value() + 1);
if (!visitedDirectories.insert(currentDir.value).second)
continue;
@ -402,7 +411,7 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
// Add a "data file" icon if the directory contains a content file
if (mSelector->containsDataFiles(currentDir.value))
{
item->setIcon(QIcon(":/images/openmw-plugin.png"));
item->setIcon(containsDataIcon);
tooltip << tr("Contains content file(s)");
}
@ -765,7 +774,7 @@ void Launcher::DataFilesPage::addSubdirectories(bool append)
return;
QString rootPath = QFileDialog::getExistingDirectory(
this, tr("Select Directory"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::Option::ReadOnly);
this, tr("Select Directory"), {}, QFileDialog::ShowDirsOnly | QFileDialog::Option::ReadOnly);
if (rootPath.isEmpty())
return;

View file

@ -1,20 +1,16 @@
set(NAVMESHTOOL
set(NAVMESHTOOL_LIB
worldspacedata.cpp
navmesh.cpp
main.cpp
)
source_group(apps\\navmeshtool FILES ${NAVMESHTOOL})
add_library(openmw-navmeshtool-lib STATIC
${NAVMESHTOOL}
)
source_group(apps\\navmeshtool FILES ${NAVMESHTOOL_LIB} main.cpp)
add_library(openmw-navmeshtool-lib STATIC ${NAVMESHTOOL_LIB})
if (ANDROID)
add_library(openmw-navmeshtool SHARED
main.cpp
)
add_library(openmw-navmeshtool SHARED main.cpp)
else()
openmw_add_executable(openmw-navmeshtool ${NAVMESHTOOL})
openmw_add_executable(openmw-navmeshtool main.cpp)
endif()
target_link_libraries(openmw-navmeshtool openmw-navmeshtool-lib)

View file

@ -62,8 +62,6 @@ namespace NavMeshTool
bpo::options_description makeOptionsDescription()
{
using Fallback::FallbackMap;
bpo::options_description result;
auto addOption = result.add_options();
addOption("help", "print help message");
@ -225,7 +223,8 @@ namespace NavMeshTool
Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager, &bgsmFileManager, expiryDelay);
Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager, expiryDelay);
DetourNavigator::RecastGlobalAllocator::init();
DetourNavigator::Settings navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager();
DetourNavigator::Settings navigatorSettings
= DetourNavigator::makeSettingsFromSettingsManager(Debug::getRecastMaxLogLevel());
navigatorSettings.mRecast.mSwimHeightScale
= EsmLoader::getGameSetting(esmData.mGameSettings, "fSwimHeightScale").getFloat();

View file

@ -38,8 +38,6 @@
#include "view/doc/viewmanager.hpp"
using namespace Fallback;
CS::Editor::Editor(int argc, char** argv)
: mConfigVariables(readConfiguration())
, mSettingsState(mCfgMgr)
@ -124,7 +122,10 @@ boost::program_options::variables_map CS::Editor::readConfiguration()
->default_value(std::vector<std::string>(), "fallback-archive")
->multitoken());
addOption("fallback",
boost::program_options::value<FallbackMap>()->default_value(FallbackMap(), "")->multitoken()->composing(),
boost::program_options::value<Fallback::FallbackMap>()
->default_value(Fallback::FallbackMap(), "")
->multitoken()
->composing(),
"fallback values");
Files::ConfigurationManager::addCommonOptions(desc);
@ -141,7 +142,7 @@ std::pair<Files::PathContainer, std::vector<std::string>> CS::Editor::readConfig
{
boost::program_options::variables_map& variables = mConfigVariables;
Fallback::Map::init(variables["fallback"].as<FallbackMap>().mMap);
Fallback::Map::init(variables["fallback"].as<Fallback::FallbackMap>().mMap);
mEncodingName = variables["encoding"].as<std::string>();
mDocumentManager.setEncoding(ToUTF8::calculateEncoding(mEncodingName));

View file

@ -1,4 +1,5 @@
#include <components/debug/debugging.hpp>
#include <components/testing/util.hpp>
#include <gtest/gtest.h>
@ -7,5 +8,9 @@ int main(int argc, char* argv[])
Log::sMinDebugLevel = Debug::getDebugLevel();
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
const int result = RUN_ALL_TESTS();
if (result == 0)
std::filesystem::remove_all(TestingOpenMW::outputDir());
return result;
}

View file

@ -373,11 +373,15 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager)
, mScriptConsoleMode(false)
, mActivationDistanceOverride(-1)
, mGrab(true)
, mExportFonts(false)
, mRandomSeed(0)
, mNewGame(false)
, mCfgMgr(configurationManager)
, mGlMaxTextureImageUnits(0)
{
#if SDL_VERSION_ATLEAST(2, 24, 0)
SDL_SetHint(SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, "1");
#endif
SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); // We use only gamepads
Uint32 flags
@ -807,7 +811,7 @@ void OMW::Engine::prepareEngine()
rootNode->addChild(guiRoot);
mWindowManager = std::make_unique<MWGui::WindowManager>(mWindow, mViewer, guiRoot, mResourceSystem.get(),
mWorkQueue.get(), mCfgMgr.getLogPath(), mScriptConsoleMode, mTranslationDataStorage, mEncoding,
mWorkQueue.get(), mCfgMgr.getLogPath(), mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts,
Version::getOpenmwVersionDescription(), shadersSupported, mCfgMgr);
mEnvironment.setWindowManager(*mWindowManager);
@ -847,7 +851,7 @@ void OMW::Engine::prepareEngine()
}
listener->loadingOff();
mWorld->init(mViewer, std::move(rootNode), mWorkQueue.get(), *mUnrefQueue);
mWorld->init(mMaxRecastLogLevel, mViewer, std::move(rootNode), mWorkQueue.get(), *mUnrefQueue);
mEnvironment.setWorldScene(mWorld->getWorldScene());
mWorld->setupPlayer();
mWorld->setRandomSeed(mRandomSeed);
@ -1109,6 +1113,11 @@ void OMW::Engine::setWarningsMode(int mode)
mWarningsMode = mode;
}
void OMW::Engine::enableFontExport(bool exportFonts)
{
mExportFonts = exportFonts;
}
void OMW::Engine::setSaveGameFile(const std::filesystem::path& savegame)
{
mSaveGameFile = savegame;

View file

@ -4,6 +4,7 @@
#include <filesystem>
#include <components/compiler/extensions.hpp>
#include <components/debug/debuglog.hpp>
#include <components/esm/refid.hpp>
#include <components/files/collections.hpp>
#include <components/settings/settings.hpp>
@ -171,7 +172,9 @@ namespace OMW
// Grab mouse?
bool mGrab;
bool mExportFonts;
unsigned int mRandomSeed;
Debug::Level mMaxRecastLogLevel = Debug::Error;
Compiler::Extensions mExtensions;
std::unique_ptr<Compiler::Context> mScriptContext;
@ -180,6 +183,9 @@ namespace OMW
Translation::Storage mTranslationDataStorage;
bool mNewGame;
Files::ConfigurationManager& mCfgMgr;
int mGlMaxTextureImageUnits;
// not implemented
Engine(const Engine&);
Engine& operator=(const Engine&);
@ -251,14 +257,14 @@ namespace OMW
void setWarningsMode(int mode);
void enableFontExport(bool exportFonts);
/// Set the save game file to load after initialising the engine.
void setSaveGameFile(const std::filesystem::path& savegame);
void setRandomSeed(unsigned int seed);
private:
Files::ConfigurationManager& mCfgMgr;
int mGlMaxTextureImageUnits;
void setRecastMaxLogLevel(Debug::Level value) { mMaxRecastLogLevel = value; }
};
}

View file

@ -28,8 +28,6 @@ extern "C" __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 0x
#include <unistd.h>
#endif
using namespace Fallback;
/**
* \brief Parses application command line and calls \ref Cfg::ConfigurationManager
* to parse configuration files.
@ -152,9 +150,10 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati
engine.setSaveGameFile(variables["load-savegame"].as<Files::MaybeQuotedPath>().u8string());
// other settings
Fallback::Map::init(variables["fallback"].as<FallbackMap>().mMap);
Fallback::Map::init(variables["fallback"].as<Fallback::FallbackMap>().mMap);
engine.setSoundUsage(!variables["no-sound"].as<bool>());
engine.setActivationDistanceOverride(variables["activate-dist"].as<int>());
engine.enableFontExport(variables["export-fonts"].as<bool>());
engine.setRandomSeed(variables["random-seed"].as<unsigned int>());
return true;
@ -220,6 +219,8 @@ int runApplication(int argc, char* argv[])
Files::ConfigurationManager cfgMgr;
std::unique_ptr<OMW::Engine> engine = std::make_unique<OMW::Engine>(cfgMgr);
engine->setRecastMaxLogLevel(Debug::getRecastMaxLogLevel());
if (parseOptions(argc, argv, *engine, cfgMgr))
{
if (!Misc::checkRequiredOSGPluginsArePresent())

View file

@ -200,7 +200,7 @@ namespace MWBase
///< Skip the animation for the given MW-reference for one frame. Calls to this function for
/// references that are currently not in the scene should be ignored.
virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0;
virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) = 0;
virtual bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const = 0;

View file

@ -363,7 +363,7 @@ namespace MWBase
void windowVisibilityChange(bool visible) override = 0;
void windowResized(int x, int y) override = 0;
void windowClosed() override = 0;
virtual bool isWindowVisible() = 0;
virtual bool isWindowVisible() const = 0;
virtual void watchActor(const MWWorld::Ptr& ptr) = 0;
virtual MWWorld::Ptr getWatchedActor() const = 0;

View file

@ -237,7 +237,12 @@ namespace MWClass
bool Container::hasToolTip(const MWWorld::ConstPtr& ptr) const
{
if (const MWWorld::CustomData* data = ptr.getRefData().getCustomData())
return !canBeHarvested(ptr) || data->asContainerCustomData().mStore.hasVisibleItems();
{
if (!canBeHarvested(ptr))
return true;
const MWWorld::ContainerStore& store = data->asContainerCustomData().mStore;
return !store.isResolved() || store.hasVisibleItems();
}
return true;
}

View file

@ -1343,12 +1343,13 @@ namespace MWGui
return codePoint == '\r';
}
// Normal no-break space (0x00A0) is ignored here
// because Morrowind compatibility requires us to render its glyph
static bool ucsSpace(int codePoint)
{
switch (codePoint)
{
case 0x0020: // SPACE
case 0x00A0: // NO-BREAK SPACE
case 0x1680: // OGHAM SPACE MARK
case 0x180E: // MONGOLIAN VOWEL SEPARATOR
case 0x2000: // EN QUAD
@ -1373,12 +1374,14 @@ namespace MWGui
}
}
// No-break spaces (0x00A0, 0x202F, 0xFEFF - normal, narrow, zero width)
// are ignored here for obvious reasons
// Figure space (0x2007) is not a breaking space either
static bool ucsBreakingSpace(int codePoint)
{
switch (codePoint)
{
case 0x0020: // SPACE
// case 0x00A0: // NO-BREAK SPACE
case 0x1680: // OGHAM SPACE MARK
case 0x180E: // MONGOLIAN VOWEL SEPARATOR
case 0x2000: // EN QUAD
@ -1388,15 +1391,12 @@ namespace MWGui
case 0x2004: // THREE-PER-EM SPACE
case 0x2005: // FOUR-PER-EM SPACE
case 0x2006: // SIX-PER-EM SPACE
case 0x2007: // FIGURE SPACE
case 0x2008: // PUNCTUATION SPACE
case 0x2009: // THIN SPACE
case 0x200A: // HAIR SPACE
case 0x200B: // ZERO WIDTH SPACE
case 0x202F: // NARROW NO-BREAK SPACE
case 0x205F: // MEDIUM MATHEMATICAL SPACE
case 0x3000: // IDEOGRAPHIC SPACE
// case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE
return true;
default:
return false;

View file

@ -29,11 +29,26 @@ namespace MWGui
{
Misc::FrameRateLimiter frameRateLimiter
= Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit());
const MWBase::WindowManager& windowManager = *MWBase::Environment::get().getWindowManager();
bool paused = false;
while (mRunning)
{
if (windowManager.isWindowVisible())
{
if (paused)
{
mVideo->resume();
paused = false;
}
// If finished playing, start again
if (!mVideo->update())
mVideo->playVideo("video\\menu_background.bik");
}
else if (!paused)
{
paused = true;
mVideo->pause();
}
frameRateLimiter.limit();
}
}

View file

@ -146,7 +146,7 @@ namespace MWGui
WindowManager::WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot,
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::filesystem::path& logpath,
bool consoleOnlyScripts, Translation::Storage& translationDataStorage, ToUTF8::FromType encoding,
const std::string& versionDescription, bool useShaders, Files::ConfigurationManager& cfgMgr)
bool exportFonts, const std::string& versionDescription, bool useShaders, Files::ConfigurationManager& cfgMgr)
: mOldUpdateMask(0)
, mOldCullMask(0)
, mStore(nullptr)
@ -215,7 +215,8 @@ namespace MWGui
MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag);
// Load fonts
mFontLoader = std::make_unique<Gui::FontLoader>(encoding, resourceSystem->getVFS(), mScalingFactor);
mFontLoader
= std::make_unique<Gui::FontLoader>(encoding, resourceSystem->getVFS(), mScalingFactor, exportFonts);
// Register own widgets with MyGUI
MyGUI::FactoryManager::getInstance().registerFactory<MWGui::Widgets::MWSkill>("Widget");
@ -729,6 +730,9 @@ namespace MWGui
return;
}
if (mGuiModes.empty())
return;
GuiModeState& state = mGuiModeStates[mGuiModes.back()];
for (const auto& window : state.mWindows)
{
@ -1214,7 +1218,7 @@ namespace MWGui
// TODO: check if any windows are now off-screen and move them back if so
}
bool WindowManager::isWindowVisible()
bool WindowManager::isWindowVisible() const
{
return mWindowVisible;
}

View file

@ -128,7 +128,7 @@ namespace MWGui
WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot,
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const std::filesystem::path& logpath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage,
ToUTF8::FromType encoding, const std::string& versionDescription, bool useShaders,
ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, bool useShaders,
Files::ConfigurationManager& cfgMgr);
virtual ~WindowManager();
@ -290,7 +290,7 @@ namespace MWGui
void windowVisibilityChange(bool visible) override;
void windowResized(int x, int y) override;
void windowClosed() override;
bool isWindowVisible() override;
bool isWindowVisible() const override;
void watchActor(const MWWorld::Ptr& ptr) override;
MWWorld::Ptr getWatchedActor() const override;

View file

@ -213,6 +213,7 @@ namespace MWLua
{ "NavMeshNotFound", DetourNavigator::Status::NavMeshNotFound },
{ "StartPolygonNotFound", DetourNavigator::Status::StartPolygonNotFound },
{ "EndPolygonNotFound", DetourNavigator::Status::EndPolygonNotFound },
{ "TargetPolygonNotFound", DetourNavigator::Status::TargetPolygonNotFound },
{ "MoveAlongSurfaceFailed", DetourNavigator::Status::MoveAlongSurfaceFailed },
{ "FindPathOverPolygonsFailed", DetourNavigator::Status::FindPathOverPolygonsFailed },
{ "InitNavMeshQueryFailed", DetourNavigator::Status::InitNavMeshQueryFailed },

View file

@ -289,8 +289,9 @@ namespace MWMechanics
const ESM::RefId& enchantmentId = slot->getClass().getEnchantment(*slot);
if (enchantmentId.empty())
continue;
const ESM::Enchantment* enchantment = world->getStore().get<ESM::Enchantment>().find(enchantmentId);
if (enchantment->mData.mType != ESM::Enchantment::ConstantEffect)
const ESM::Enchantment* enchantment
= world->getStore().get<ESM::Enchantment>().search(enchantmentId);
if (enchantment == nullptr || enchantment->mData.mType != ESM::Enchantment::ConstantEffect)
continue;
if (std::find_if(mSpells.begin(), mSpells.end(),
[&](const ActiveSpellParams& params) {

View file

@ -2028,7 +2028,7 @@ namespace MWMechanics
iter->second->getCharacterController().skipAnim();
}
bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const
bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) const
{
const auto iter = mIndex.find(ptr.mRef);
if (iter != mIndex.end())

View file

@ -119,7 +119,7 @@ namespace MWMechanics
std::string_view startKey, std::string_view stopKey, bool forceLoop);
void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable);
void skipAnimation(const MWWorld::Ptr& ptr) const;
bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const;
bool checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) const;
bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const;
void persistAnimationStates() const;
void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted);

View file

@ -40,15 +40,15 @@ namespace MWMechanics
static const std::size_t MAX_IDLE_SIZE = 8;
const std::string AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = {
std::string("idle2"),
std::string("idle3"),
std::string("idle4"),
std::string("idle5"),
std::string("idle6"),
std::string("idle7"),
std::string("idle8"),
std::string("idle9"),
const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = {
"idle2",
"idle3",
"idle4",
"idle5",
"idle6",
"idle7",
"idle8",
"idle9",
};
namespace
@ -680,7 +680,7 @@ namespace MWMechanics
{
if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle))
{
const std::string& groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle];
const std::string_view groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle];
return MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, groupName, 0, 1);
}
else
@ -695,7 +695,7 @@ namespace MWMechanics
{
if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle))
{
const std::string& groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle];
const std::string_view groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle];
return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, groupName);
}
else

View file

@ -3,6 +3,7 @@
#include "typedaipackage.hpp"
#include <string_view>
#include <vector>
#include "aitemporarybase.hpp"
@ -181,9 +182,7 @@ namespace MWMechanics
const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage);
/// lookup table for converting idleSelect value to groupName
static const std::string sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1];
static int OffsetToPreventOvercrowding();
static const std::string_view sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1];
};
}

View file

@ -778,7 +778,8 @@ namespace MWMechanics
else
mObjects.skipAnimation(ptr);
}
bool MechanicsManager::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName)
bool MechanicsManager::checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName)
{
if (ptr.getClass().isActor())
return mActors.checkAnimationPlaying(ptr, groupName);

View file

@ -146,7 +146,7 @@ namespace MWMechanics
std::string_view startKey, std::string_view stopKey, bool forceLoop) override;
void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) override;
void skipAnimation(const MWWorld::Ptr& ptr) override;
bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) override;
bool checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) override;
bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const override;
void persistAnimationStates() override;
void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) override;

View file

@ -161,7 +161,7 @@ namespace MWRender
bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const
{
if (Settings::game().mShieldSheathing)
if (Settings::game().mShieldSheathing && mObjectRoot)
{
const MWWorld::Class& cls = mPtr.getClass();
MWMechanics::CreatureStats& stats = cls.getCreatureStats(mPtr);

View file

@ -1695,7 +1695,7 @@ namespace MWRender
mGlowUpdater->setColor(color);
mGlowUpdater->setDuration(glowDuration);
}
else
else if (mObjectRoot)
mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, color, glowDuration);
}
}
@ -1869,7 +1869,7 @@ namespace MWRender
void Animation::setAlpha(float alpha)
{
if (alpha == mAlpha)
if (alpha == mAlpha || !mObjectRoot)
return;
mAlpha = alpha;

View file

@ -259,6 +259,7 @@ namespace MWRender
void CreatureWeaponAnimation::addControllers()
{
Animation::addControllers();
if (mObjectRoot)
WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get());
}

View file

@ -49,18 +49,6 @@ namespace MWRender
&& exts.glslLanguageVersion >= minimumGLVersionRequiredForCompute;
#endif
static bool pipelineLogged = false;
if (!pipelineLogged)
{
if (mUseCompute)
Log(Debug::Info) << "Initialized compute shader pipeline for water ripples";
else
Log(Debug::Info) << "Initialized fallback fragment shader pipeline for water ripples";
pipelineLogged = true;
}
for (size_t i = 0; i < mState.size(); ++i)
{
osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet;
@ -98,6 +86,18 @@ namespace MWRender
else
setupFragmentPipeline();
if (mProgramBlobber != nullptr)
{
static bool pipelineLogged = [&] {
if (mUseCompute)
Log(Debug::Info) << "Initialized compute shader pipeline for water ripples";
else
Log(Debug::Info) << "Initialized fallback fragment shader pipeline for water ripples";
return true;
}();
(void)pipelineLogged;
}
setCullCallback(new osg::NodeCallback);
setUpdateCallback(new osg::NodeCallback);
}
@ -109,21 +109,36 @@ namespace MWRender
Shader::ShaderManager::DefineMap defineMap = { { "rippleMapSize", std::to_string(sRTTSize) + ".0" } };
osg::ref_ptr<osg::Shader> vertex = shaderManager.getShader("fullscreen_tri.vert", {}, osg::Shader::VERTEX);
osg::ref_ptr<osg::Shader> blobber
= shaderManager.getShader("ripples_blobber.frag", defineMap, osg::Shader::FRAGMENT);
osg::ref_ptr<osg::Shader> simulate
= shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT);
if (vertex == nullptr || blobber == nullptr || simulate == nullptr)
{
Log(Debug::Error) << "Failed to load shaders required for fragment shader ripple pipeline";
return;
}
mProgramBlobber = shaderManager.getProgram(
vertex, shaderManager.getShader("ripples_blobber.frag", defineMap, osg::Shader::FRAGMENT));
mProgramSimulation = shaderManager.getProgram(
std::move(vertex), shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT));
mProgramBlobber = shaderManager.getProgram(vertex, std::move(blobber));
mProgramSimulation = shaderManager.getProgram(std::move(vertex), std::move(simulate));
}
void RipplesSurface::setupComputePipeline()
{
auto& shaderManager = mResourceSystem->getSceneManager()->getShaderManager();
mProgramBlobber = shaderManager.getProgram(
nullptr, shaderManager.getShader("core/ripples_blobber.comp", {}, osg::Shader::COMPUTE));
mProgramSimulation = shaderManager.getProgram(
nullptr, shaderManager.getShader("core/ripples_simulate.comp", {}, osg::Shader::COMPUTE));
osg::ref_ptr<osg::Shader> blobber
= shaderManager.getShader("core/ripples_blobber.comp", {}, osg::Shader::COMPUTE);
osg::ref_ptr<osg::Shader> simulate
= shaderManager.getShader("core/ripples_simulate.comp", {}, osg::Shader::COMPUTE);
if (blobber == nullptr || simulate == nullptr)
{
Log(Debug::Error) << "Failed to load shaders required for compute shader ripple pipeline";
return;
}
mProgramBlobber = shaderManager.getProgram(nullptr, std::move(blobber));
mProgramSimulation = shaderManager.getProgram(nullptr, std::move(simulate));
}
void RipplesSurface::updateState(const osg::FrameStamp& frameStamp, State& state)
@ -191,6 +206,9 @@ namespace MWRender
void RipplesSurface::drawImplementation(osg::RenderInfo& renderInfo) const
{
if (mProgramBlobber == nullptr || mProgramSimulation == nullptr)
return;
osg::State& state = *renderInfo.getState();
const std::size_t currentFrame = state.getFrameStamp()->getFrameNumber() % 2;
const State& frameState = mState[currentFrame];

View file

@ -406,8 +406,7 @@ namespace MWWorld
template <class T>
ContainerStoreIteratorBase(const ContainerStoreIteratorBase<T>& other)
{
char CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR[IsConvertible<T, PtrType, void>::value ? 1 : -1];
((void)CANNOT_CONVERT_CONST_ITERATOR_TO_ITERATOR);
static_assert(IsConvertible<T, PtrType, void>::value);
copy(other);
}

View file

@ -290,14 +290,14 @@ namespace MWWorld
mSwimHeightScale = mStore.get<ESM::GameSetting>().find("fSwimHeightScale")->mValue.getFloat();
}
void World::init(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode, SceneUtil::WorkQueue* workQueue,
SceneUtil::UnrefQueue& unrefQueue)
void World::init(Debug::Level maxRecastLogLevel, osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode,
SceneUtil::WorkQueue* workQueue, SceneUtil::UnrefQueue& unrefQueue)
{
mPhysics = std::make_unique<MWPhysics::PhysicsSystem>(mResourceSystem, rootNode);
if (Settings::navigator().mEnable)
{
auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager();
auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(maxRecastLogLevel);
navigatorSettings.mRecast.mSwimHeightScale = mSwimHeightScale;
mNavigator = DetourNavigator::makeNavigator(navigatorSettings, mUserDataPath);
}

View file

@ -4,6 +4,7 @@
#include <osg/Timer>
#include <osg/ref_ptr>
#include <components/debug/debuglog.hpp>
#include <components/esm3/readerscache.hpp>
#include <components/misc/rng.hpp>
#include <components/settings/settings.hpp>
@ -201,8 +202,8 @@ namespace MWWorld
Loading::Listener* listener);
// Must be called after `loadData`.
void init(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode, SceneUtil::WorkQueue* workQueue,
SceneUtil::UnrefQueue& unrefQueue);
void init(Debug::Level maxRecastLogLevel, osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode,
SceneUtil::WorkQueue* workQueue, SceneUtil::UnrefQueue& unrefQueue);
virtual ~World();

View file

@ -2,6 +2,7 @@
#include <components/misc/strings/conversion.hpp>
#include <components/settings/parser.hpp>
#include <components/settings/values.hpp>
#include <components/testing/util.hpp>
#include <gtest/gtest.h>
@ -24,5 +25,9 @@ int main(int argc, char* argv[])
Settings::StaticValues::init();
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
const int result = RUN_ALL_TESTS();
if (result == 0)
std::filesystem::remove_all(TestingOpenMW::outputDir());
return result;
}

View file

@ -59,7 +59,7 @@ list(APPEND COMPONENT_FILES "${OpenMW_BINARY_DIR}/${OSG_PLUGIN_CHECKER_CPP_FILE}
add_component_dir (lua
luastate scriptscontainer asyncpackage utilpackage serialization configuration l10n storage utf8
shapes/box inputactions yamlloader scripttracker
shapes/box inputactions yamlloader scripttracker luastateptr
)
add_component_dir (l10n

View file

@ -1,6 +1,7 @@
#include "gamesettings.hpp"
#include <QDir>
#include <QProgressDialog>
#include <QRegularExpression>
#include <components/files/configurationmanager.hpp>
@ -37,8 +38,13 @@ void Config::GameSettings::validatePaths()
mDataDirs.clear();
QProgressDialog progressBar("Validating paths", {}, 0, paths.count() + 1);
progressBar.setWindowModality(Qt::WindowModal);
progressBar.setValue(0);
for (const auto& dataDir : paths)
{
progressBar.setValue(progressBar.value() + 1);
if (QDir(dataDir.value).exists())
{
SettingValue copy = dataDir;
@ -50,6 +56,8 @@ void Config::GameSettings::validatePaths()
// Do the same for data-local
const QString& local = mSettings.value(QString("data-local")).value;
progressBar.setValue(progressBar.value() + 1);
if (!local.isEmpty() && QDir(local).exists())
{
mDataLocal = QDir(local).canonicalPath();

View file

@ -10,6 +10,7 @@
#include <QDataStream>
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QFont>
#include <QIODevice>
@ -78,14 +79,10 @@ ContentSelectorModel::EsmFile* ContentSelectorModel::ContentModel::item(int row)
}
const ContentSelectorModel::EsmFile* ContentSelectorModel::ContentModel::item(const QString& name) const
{
EsmFile::FileProperty fp = EsmFile::FileProperty_FileName;
if (name.contains('/'))
fp = EsmFile::FileProperty_FilePath;
bool path = name.contains('/');
for (const EsmFile* file : mFiles)
{
if (name.compare(file->fileProperty(fp).toString(), Qt::CaseInsensitive) == 0)
if (name.compare(path ? file->filePath() : file->fileName(), Qt::CaseInsensitive) == 0)
return file;
}
return nullptr;
@ -310,7 +307,6 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const
{
setCheckState(file->filePath(), success);
emit dataChanged(index, index);
checkForLoadOrderErrors();
}
else
return success;
@ -425,7 +421,6 @@ bool ContentSelectorModel::ContentModel::dropMimeData(
dataChanged(index(minRow, 0), index(maxRow, 0));
// at this point we know that drag and drop has finished.
checkForLoadOrderErrors();
return true;
}
@ -552,15 +547,13 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf
bool ContentSelectorModel::ContentModel::containsDataFiles(const QString& path)
{
QDir dir(path);
QStringList filters;
filters << "*.esp"
<< "*.esm"
<< "*.omwgame"
<< "*.omwaddon";
dir.setNameFilters(filters);
return dir.entryList().count() != 0;
QDirIterator it(path, filters, QDir::Files | QDir::NoDotAndDotDot);
return it.hasNext();
}
void ContentSelectorModel::ContentModel::clearFiles()
@ -707,12 +700,13 @@ void ContentSelectorModel::ContentModel::setNonUserContent(const QStringList& fi
bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile* file) const
{
return mPluginsWithLoadOrderError.contains(file->filePath());
int index = indexFromItem(file).row();
auto errors = checkForLoadOrderErrors(file, index);
return !errors.empty();
}
void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileList)
{
mPluginsWithLoadOrderError.clear();
int previousPosition = -1;
for (const QString& filepath : fileList)
{
@ -725,7 +719,6 @@ void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileL
if (filePosition < previousPosition)
{
mFiles.move(filePosition, previousPosition);
emit dataChanged(index(filePosition, 0, QModelIndex()), index(previousPosition, 0, QModelIndex()));
}
else
{
@ -733,24 +726,7 @@ void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileL
}
}
}
checkForLoadOrderErrors();
}
void ContentSelectorModel::ContentModel::checkForLoadOrderErrors()
{
for (int row = 0; row < mFiles.count(); ++row)
{
EsmFile* file = mFiles.at(row);
bool isRowInError = checkForLoadOrderErrors(file, row).count() != 0;
if (isRowInError)
{
mPluginsWithLoadOrderError.insert(file->filePath());
}
else
{
mPluginsWithLoadOrderError.remove(file->filePath());
}
}
emit dataChanged(index(0, 0), index(rowCount(), columnCount()));
}
QList<ContentSelectorModel::LoadOrderError> ContentSelectorModel::ContentModel::checkForLoadOrderErrors(
@ -791,11 +767,12 @@ QList<ContentSelectorModel::LoadOrderError> ContentSelectorModel::ContentModel::
QString ContentSelectorModel::ContentModel::toolTip(const EsmFile* file) const
{
if (isLoadOrderError(file))
int index = indexFromItem(file).row();
auto errors = checkForLoadOrderErrors(file, index);
if (!errors.empty())
{
QString text("<b>");
int index = indexFromItem(item(file->filePath())).row();
for (const LoadOrderError& error : checkForLoadOrderErrors(file, index))
for (const LoadOrderError& error : errors)
{
assert(error.errorCode() != LoadOrderError::ErrorCode::ErrorCode_None);
@ -900,7 +877,6 @@ ContentSelectorModel::ContentFileList ContentSelectorModel::ContentModel::checke
void ContentSelectorModel::ContentModel::uncheckAll()
{
emit layoutAboutToBeChanged();
mCheckedFiles.clear();
emit layoutChanged();
emit dataChanged(index(0, 0), index(rowCount(), columnCount()), { Qt::CheckStateRole, Qt::UserRole + 1 });
}

View file

@ -69,9 +69,6 @@ namespace ContentSelectorModel
void refreshModel();
/// Checks all plug-ins for load order errors and updates mPluginsWithLoadOrderError with plug-ins with issues
void checkForLoadOrderErrors();
private:
void addFile(EsmFile* file);
@ -89,7 +86,6 @@ namespace ContentSelectorModel
QStringList mNonUserContent;
std::set<const EsmFile*> mCheckedFiles;
QHash<QString, bool> mNewFiles;
QSet<QString> mPluginsWithLoadOrderError;
QString mEncoding;
QIcon mWarningIcon;
QIcon mErrorIcon;

View file

@ -48,18 +48,18 @@ namespace ContentSelectorModel
void addGameFile(const QString& name) { mGameFiles.append(name); }
QVariant fileProperty(const FileProperty prop) const;
QString fileName() const { return mFileName; }
QString author() const { return mAuthor; }
const QString& fileName() const { return mFileName; }
const QString& author() const { return mAuthor; }
QDateTime modified() const { return mModified; }
QString formatVersion() const { return mVersion; }
QString filePath() const { return mPath; }
const QString& formatVersion() const { return mVersion; }
const QString& filePath() const { return mPath; }
bool builtIn() const { return mBuiltIn; }
bool fromAnotherConfigFile() const { return mFromAnotherConfigFile; }
bool isMissing() const { return mPath.isEmpty(); }
/// @note Contains file names, not paths.
const QStringList& gameFiles() const { return mGameFiles; }
QString description() const { return mDescription; }
const QString& description() const { return mDescription; }
QString toolTip() const
{
if (isMissing())

View file

@ -211,7 +211,6 @@ void ContentSelectorView::ContentSelector::addFiles(const QString& path, bool ne
ui->gameFileView->setCurrentIndex(0);
mContentModel->uncheckAll();
mContentModel->checkForLoadOrderErrors();
}
void ContentSelectorView::ContentSelector::sortFiles()
@ -254,7 +253,6 @@ void ContentSelectorView::ContentSelector::slotCurrentGameFileIndexChanged(int i
oldIndex = index;
setGameFileSelected(index, true);
mContentModel->checkForLoadOrderErrors();
}
emit signalCurrentGamefileIndexChanged(index);

View file

@ -237,7 +237,7 @@ namespace Crash
// must remain until monitor has finished
waitMonitor();
std::string message = "OpenMW has encountered a fatal error.\nCrash log saved to '"
std::string message = "OpenMW has encountered a fatal error.\nCrash dump saved to '"
+ Misc::StringUtils::u8StringToString(getCrashDumpPath(*mShm).u8string())
+ "'.\nPlease report this to https://gitlab.com/OpenMW/openmw/issues !";
SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr);

View file

@ -21,6 +21,7 @@ namespace Crash
// the main openmw process in task manager.
static constexpr const int CrashCatcherTimeout = 2500;
static constexpr const int CrashCatcherThawTimeout = 250;
struct CrashSHM;

View file

@ -87,9 +87,10 @@ namespace Crash
SetEvent(mSignalAppEvent);
}
bool CrashMonitor::waitApp() const
bool CrashMonitor::waitApp(bool thawMode) const
{
return WaitForSingleObject(mSignalMonitorEvent, CrashCatcherTimeout) == WAIT_OBJECT_0;
return WaitForSingleObject(mSignalMonitorEvent, thawMode ? CrashCatcherThawTimeout : CrashCatcherTimeout)
== WAIT_OBJECT_0;
}
bool CrashMonitor::isAppAlive() const
@ -185,7 +186,7 @@ namespace Crash
frozen = false;
}
if (!mFreezeAbort && waitApp())
if (!mFreezeAbort && waitApp(frozen))
{
shmLock();
@ -215,7 +216,7 @@ namespace Crash
{
handleCrash(true);
TerminateProcess(mAppProcessHandle, 0xDEAD);
std::string message = "OpenMW appears to have frozen.\nCrash log saved to '"
std::string message = "OpenMW has frozen.\nCrash dump saved to '"
+ Misc::StringUtils::u8StringToString(getFreezeDumpPath(*mShm).u8string())
+ "'.\nPlease report this to https://gitlab.com/OpenMW/openmw/issues !";
SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr);
@ -289,10 +290,10 @@ namespace Crash
{
std::thread messageBoxThread([&]() {
SDL_MessageBoxButtonData button = { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0, "Abort" };
SDL_MessageBoxData messageBoxData = { SDL_MESSAGEBOX_ERROR, nullptr, "OpenMW appears to have frozen",
"OpenMW appears to have frozen. Press Abort to terminate it and generate a crash dump.\nIf OpenMW "
"hasn't actually frozen, this message box will disappear a within a few seconds of it becoming "
"responsive.",
SDL_MessageBoxData messageBoxData = { SDL_MESSAGEBOX_ERROR, nullptr, "OpenMW has frozen",
"OpenMW has frozen. This should never happen. Press Abort to terminate it and generate a crash dump to "
"help diagnose the problem.\nOpenMW may unfreeze if you wait, and this message box will disappear "
"after it becomes responsive.",
1, &button, nullptr };
int buttonId;

View file

@ -41,7 +41,7 @@ namespace Crash
void signalApp() const;
bool waitApp() const;
bool waitApp(bool thawMode) const;
bool isAppAlive() const;

View file

@ -106,6 +106,8 @@ namespace Debug
logListener = std::move(listener);
}
namespace
{
class DebugOutputBase : public boost::iostreams::sink
{
public:
@ -137,7 +139,8 @@ namespace Debug
#endif
prefixSize = std::strftime(prefix + 1, sizeof(prefix) - 1, "%T", &time_info) + 1;
char levelLetter = " EWIVD*"[int(level)];
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
const auto ms
= std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
prefixSize += snprintf(prefix + prefixSize, sizeof(prefix) - prefixSize, ".%03u %c] ",
static_cast<unsigned>(ms % 1000), levelLetter);
}
@ -152,7 +155,8 @@ namespace Debug
writeImpl(prefix, prefixSize, level);
writeImpl(msg.data(), lineSize, level);
if (logListener)
logListener(level, std::string_view(prefix, prefixSize), std::string_view(msg.data(), lineSize));
logListener(
level, std::string_view(prefix, prefixSize), std::string_view(msg.data(), lineSize));
msg = msg.substr(lineSize);
}
@ -192,8 +196,6 @@ namespace Debug
};
#else
namespace
{
struct Record
{
std::string mValue;
@ -324,9 +326,24 @@ namespace Debug
First mFirst;
Second mSecond;
};
}
#endif
Level toLevel(std::string_view value)
{
if (value == "ERROR")
return Error;
if (value == "WARNING")
return Warning;
if (value == "INFO")
return Info;
if (value == "VERBOSE")
return Verbose;
if (value == "DEBUG")
return Debug;
return Verbose;
}
static std::unique_ptr<std::ostream> rawStdout = nullptr;
static std::unique_ptr<std::ostream> rawStderr = nullptr;
static std::unique_ptr<std::mutex> rawStderrMutex = nullptr;
@ -340,6 +357,7 @@ namespace Debug
static boost::iostreams::stream_buffer<Tee<Buffer, Coloured>> bufferedOut;
static boost::iostreams::stream_buffer<Tee<Buffer, Coloured>> bufferedErr;
#endif
}
std::ostream& getRawStdout()
{
@ -359,23 +377,19 @@ namespace Debug
Level getDebugLevel()
{
if (const char* env = getenv("OPENMW_DEBUG_LEVEL"))
{
const std::string_view value(env);
if (value == "ERROR")
return Error;
if (value == "WARNING")
return Warning;
if (value == "INFO")
return Info;
if (value == "VERBOSE")
return Verbose;
if (value == "DEBUG")
return Debug;
}
return toLevel(env);
return Verbose;
}
Level getRecastMaxLogLevel()
{
if (const char* env = getenv("OPENMW_RECAST_MAX_LOG_LEVEL"))
return toLevel(env);
return Error;
}
void setupLogging(const std::filesystem::path& logDir, std::string_view appName)
{
Log::sMinDebugLevel = getDebugLevel();

View file

@ -36,6 +36,8 @@ namespace Debug
Level getDebugLevel();
Level getRecastMaxLogLevel();
// Redirect cout and cerr to the log file
void setupLogging(const std::filesystem::path& logDir, std::string_view appName);

View file

@ -452,10 +452,10 @@ namespace DetourNavigator
Log(Debug::Debug) << "Start process navigator jobs by thread=" << std::this_thread::get_id();
Misc::setCurrentThreadIdlePriority();
while (!mShouldStop)
{
try
{
if (JobIt job = getNextJob(); job != mJobs.end())
{
try
{
const JobStatus status = processJob(*job);
Log(Debug::Debug) << "Processed job " << job->mId << " with status=" << status
@ -480,12 +480,20 @@ namespace DetourNavigator
}
}
}
else
cleanupLastUpdates();
}
catch (const std::exception& e)
{
Log(Debug::Error) << "AsyncNavMeshUpdater::process exception: " << e.what();
Log(Debug::Warning) << "Failed to process navmesh job " << job->mId
<< " for worldspace=" << job->mWorldspace << " agent=" << job->mAgentBounds
<< " changedTile=(" << job->mChangedTile << ")"
<< " changeType=" << job->mChangeType
<< " by thread=" << std::this_thread::get_id() << ": " << e.what();
unlockTile(job->mId, job->mAgentBounds, job->mChangedTile);
removeJob(job);
}
}
else
{
cleanupLastUpdates();
}
}
Log(Debug::Debug) << "Stop navigator jobs processing by thread=" << std::this_thread::get_id();
@ -493,7 +501,8 @@ namespace DetourNavigator
JobStatus AsyncNavMeshUpdater::processJob(Job& job)
{
Log(Debug::Debug) << "Processing job " << job.mId << " for agent=(" << job.mAgentBounds << ")"
Log(Debug::Debug) << "Processing job " << job.mId << " for worldspace=" << job.mWorldspace
<< " agent=" << job.mAgentBounds << ""
<< " changedTile=(" << job.mChangedTile << ")"
<< " changeType=" << job.mChangeType << " by thread=" << std::this_thread::get_id();
@ -543,7 +552,14 @@ namespace DetourNavigator
return JobStatus::Done;
}
try
{
writeDebugRecastMesh(mSettings, job.mChangedTile, *recastMesh);
}
catch (const std::exception& e)
{
Log(Debug::Warning) << "Failed to write debug recast mesh: " << e.what();
}
NavMeshTilesCache::Value cachedNavMeshData
= mNavMeshTilesCache.get(job.mAgentBounds, job.mChangedTile, *recastMesh);
@ -666,12 +682,19 @@ namespace DetourNavigator
mPresentTiles.insert(std::make_tuple(job.mAgentBounds, job.mChangedTile));
}
try
{
writeDebugNavMesh(mSettings, navMeshCacheItem, navMeshVersion);
}
catch (const std::exception& e)
{
Log(Debug::Warning) << "Failed to write debug navmesh: " << e.what();
}
return isSuccess(status) ? JobStatus::Done : JobStatus::Fail;
}
JobIt AsyncNavMeshUpdater::getNextJob()
JobIt AsyncNavMeshUpdater::getNextJob() noexcept
{
std::unique_lock<std::mutex> lock(mMutex);
@ -746,7 +769,7 @@ namespace DetourNavigator
return mJobs.size();
}
void AsyncNavMeshUpdater::cleanupLastUpdates()
void AsyncNavMeshUpdater::cleanupLastUpdates() noexcept
{
const auto now = std::chrono::steady_clock::now();

View file

@ -244,7 +244,7 @@ namespace DetourNavigator
inline JobStatus handleUpdateNavMeshStatus(UpdateNavMeshStatus status, const Job& job,
const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh);
JobIt getNextJob();
inline JobIt getNextJob() noexcept;
void postThreadJob(JobIt job, std::deque<JobIt>& queue);
@ -254,7 +254,7 @@ namespace DetourNavigator
inline std::size_t getTotalJobs() const;
void cleanupLastUpdates();
inline void cleanupLastUpdates() noexcept;
inline void waitUntilJobsDoneForNotPresentTiles(Loading::Listener* listener);

View file

@ -523,7 +523,7 @@ namespace DetourNavigator
std::unique_ptr<PreparedNavMeshData> prepareNavMeshTileData(const RecastMesh& recastMesh, ESM::RefId worldspace,
const TilePosition& tilePosition, const AgentBounds& agentBounds, const RecastSettings& settings)
{
RecastContext context(worldspace, tilePosition, agentBounds);
RecastContext context(worldspace, tilePosition, agentBounds, recastMesh.getVersion(), settings.mMaxLogLevel);
const auto [minZ, maxZ] = getBoundsByZ(recastMesh, agentBounds.mHalfExtents.z(), settings);

View file

@ -1,7 +1,7 @@
#include "recastcontext.hpp"
#include "debug.hpp"
#include "components/debug/debuglog.hpp"
#include <components/debug/debuglog.hpp>
#include <sstream>
@ -23,25 +23,30 @@ namespace DetourNavigator
return Debug::Debug;
}
std::string formatPrefix(
ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds)
std::string formatPrefix(ESM::RefId worldspace, const TilePosition& tilePosition,
const AgentBounds& agentBounds, const Version& version)
{
std::ostringstream stream;
stream << "Worldspace: " << worldspace << "; tile position: " << tilePosition.x() << ", "
<< tilePosition.y() << "; agent bounds: " << agentBounds << "; ";
<< tilePosition.y() << "; agent bounds: " << agentBounds << "; version: " << version << "; ";
return stream.str();
}
}
RecastContext::RecastContext(
ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds)
: mPrefix(formatPrefix(worldspace, tilePosition, agentBounds))
RecastContext::RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition,
const AgentBounds& agentBounds, const Version& version, Debug::Level maxLogLevel)
: mMaxLogLevel(maxLogLevel)
, mPrefix(formatPrefix(worldspace, tilePosition, agentBounds, version))
{
}
void RecastContext::doLog(const rcLogCategory category, const char* msg, const int len)
{
if (len > 0)
Log(getLogLevel(category)) << mPrefix << std::string_view(msg, static_cast<std::size_t>(len));
if (msg == nullptr || len <= 0)
return;
const Debug::Level level = getLogLevel(category);
if (level > mMaxLogLevel)
return;
Log(level) << mPrefix << std::string_view(msg, static_cast<std::size_t>(len));
}
}

View file

@ -3,6 +3,7 @@
#include "tileposition.hpp"
#include <components/debug/debuglog.hpp>
#include <components/esm/refid.hpp>
#include <string>
@ -12,15 +13,18 @@
namespace DetourNavigator
{
struct AgentBounds;
struct Version;
class RecastContext final : public rcContext
{
public:
explicit RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds);
explicit RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds,
const Version& version, Debug::Level maxLogLevel);
const std::string& getPrefix() const { return mPrefix; }
private:
Debug::Level mMaxLogLevel;
std::string mPrefix;
void doLog(rcLogCategory category, const char* msg, int len) override;

View file

@ -44,7 +44,7 @@ namespace DetourNavigator
};
}
RecastSettings makeRecastSettingsFromSettingsManager()
RecastSettings makeRecastSettingsFromSettingsManager(Debug::Level maxLogLevel)
{
RecastSettings result;
@ -63,6 +63,7 @@ namespace DetourNavigator
result.mRegionMergeArea = ::Settings::navigator().mRegionMergeArea;
result.mRegionMinArea = ::Settings::navigator().mRegionMinArea;
result.mTileSize = ::Settings::navigator().mTileSize;
result.mMaxLogLevel = maxLogLevel;
return result;
}
@ -80,11 +81,11 @@ namespace DetourNavigator
}
}
Settings makeSettingsFromSettingsManager()
Settings makeSettingsFromSettingsManager(Debug::Level maxLogLevel)
{
Settings result;
result.mRecast = makeRecastSettingsFromSettingsManager();
result.mRecast = makeRecastSettingsFromSettingsManager(maxLogLevel);
result.mDetour = makeDetourSettingsFromSettingsManager();
const NavMeshLimits limits = getNavMeshTileLimits(result.mDetour);

View file

@ -1,6 +1,8 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGS_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGS_H
#include <components/debug/debuglog.hpp>
#include <chrono>
#include <string>
@ -23,6 +25,7 @@ namespace DetourNavigator
int mRegionMergeArea = 0;
int mRegionMinArea = 0;
int mTileSize = 0;
Debug::Level mMaxLogLevel = Debug::Error;
};
struct DetourSettings
@ -55,7 +58,7 @@ namespace DetourNavigator
inline constexpr std::int64_t navMeshFormatVersion = 2;
Settings makeSettingsFromSettingsManager();
Settings makeSettingsFromSettingsManager(Debug::Level maxLogLevel);
}
#endif

View file

@ -1,8 +1,9 @@
#include "luascripts.hpp"
#include "components/esm3/esmreader.hpp"
#include "components/esm3/esmwriter.hpp"
#include <components/esm3/esmreader.hpp>
#include <components/esm3/esmwriter.hpp>
#include <components/lua/luastateptr.hpp>
#include <components/lua/serialization.hpp>
// List of all records, that are related to Lua.
@ -102,13 +103,16 @@ void ESM::LuaScriptsCfg::adjustRefNums(const ESMReader& esm)
throw std::runtime_error("Incorrect contentFile index");
};
lua_State* L = luaL_newstate();
LuaUtil::LuaStatePtr state(luaL_newstate());
if (state == nullptr)
throw std::runtime_error("Failed to create Lua runtime");
LuaUtil::BasicSerializer serializer(adjustRefNumFn);
auto adjustLuaData = [&](std::string& data) {
if (data.empty())
return;
sol::object luaData = LuaUtil::deserialize(L, data, &serializer);
sol::object luaData = LuaUtil::deserialize(state.get(), data, &serializer);
data = LuaUtil::serialize(luaData, &serializer);
};
@ -123,7 +127,6 @@ void ESM::LuaScriptsCfg::adjustRefNums(const ESMReader& esm)
refCfg.mRefnumContentFile = adjustRefNumFn(refCfg.mRefnumContentFile);
}
}
lua_close(L);
}
void ESM::LuaScriptsCfg::save(ESMWriter& esm) const

View file

@ -73,10 +73,10 @@ namespace ESM
int index = getIndex();
for (int i = 0; i < getIndex(); i++)
{
const ESM::ReadersCache::BusyItem reader = readers.get(static_cast<std::size_t>(i));
if (reader->getFileSize() == 0)
if (readers.getFileSize(static_cast<std::size_t>(i)) == 0)
continue; // Content file in non-ESM format
const auto fnamecandidate = Files::pathToUnicodeString(reader->getName().filename());
const auto fnamecandidate
= Files::pathToUnicodeString(readers.getName(static_cast<std::size_t>(i)).filename());
if (Misc::StringUtils::ciEqual(fname, fnamecandidate))
{
index = i;

View file

@ -47,6 +47,7 @@ namespace ESM
{
it->mReader.open(*it->mName);
it->mName.reset();
it->mFileSize.reset();
}
mBusyItems.splice(mBusyItems.end(), mClosedItems, it);
break;
@ -57,6 +58,46 @@ namespace ESM
return BusyItem(*this, it);
}
const std::filesystem::path& ReadersCache::getName(std::size_t index) const
{
const auto indexIt = mIndex.find(index);
if (indexIt == mIndex.end())
throw std::logic_error("ESMReader at index " + std::to_string(index) + " has not been created yet");
switch (indexIt->second->mState)
{
case State::Busy:
case State::Free:
return indexIt->second->mReader.getName();
case State::Closed:
if (indexIt->second->mName)
return *indexIt->second->mName;
throw std::logic_error("ESMReader at index " + std::to_string(index) + " has forgotten its filename");
default:
throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state");
}
}
std::size_t ReadersCache::getFileSize(std::size_t index)
{
const auto indexIt = mIndex.find(index);
if (indexIt == mIndex.end())
return 0;
switch (indexIt->second->mState)
{
case State::Busy:
case State::Free:
if (!indexIt->second->mReader.getName().empty())
return indexIt->second->mReader.getFileSize();
throw std::logic_error("ESMReader at index " + std::to_string(index) + " has not been opened yet");
case State::Closed:
if (indexIt->second->mFileSize)
return *indexIt->second->mFileSize;
throw std::logic_error("ESMReader at index " + std::to_string(index) + " has forgotten its file size");
default:
throw std::logic_error("ESMReader at index " + std::to_string(index) + " in unknown state");
}
}
void ReadersCache::closeExtraReaders()
{
while (!mFreeItems.empty() && mBusyItems.size() + mFreeItems.size() + 1 > mCapacity)
@ -65,6 +106,7 @@ namespace ESM
if (it->mReader.isOpen())
{
it->mName = it->mReader.getName();
it->mFileSize = it->mReader.getFileSize();
it->mReader.close();
}
mClosedItems.splice(mClosedItems.end(), mFreeItems, it);

View file

@ -26,6 +26,7 @@ namespace ESM
State mState = State::Busy;
ESMReader mReader;
std::optional<std::filesystem::path> mName;
std::optional<std::size_t> mFileSize;
Item() = default;
};
@ -55,6 +56,10 @@ namespace ESM
BusyItem get(std::size_t index);
const std::filesystem::path& getName(std::size_t index) const;
std::size_t getFileSize(std::size_t index);
void clear();
private:

View file

@ -114,6 +114,9 @@ void ESM4::Quest::load(ESM4::Reader& reader)
case ESM::fourCC("NNAM"): // FO3
case ESM::fourCC("QOBJ"): // FO3
case ESM::fourCC("NAM0"): // FO3
case ESM::fourCC("SLSD"): // FO3
case ESM::fourCC("SCVR"): // FO3
case ESM::fourCC("SCRV"): // FO3
case ESM::fourCC("ANAM"): // TES5
case ESM::fourCC("DNAM"): // TES5
case ESM::fourCC("ENAM"): // TES5

View file

@ -24,8 +24,6 @@ namespace Fallback
};
// Parses and validates a fallback map from boost program_options.
// Note: for boost to pick up the validate function, you need to pull in the namespace e.g.
// by using namespace Fallback;
void validate(boost::any& v, std::vector<std::string> const& tokens, FallbackMap*, int);
}

View file

@ -36,12 +36,14 @@ namespace Files
{
std::filesystem::path userPath = std::filesystem::current_path();
WCHAR path[MAX_PATH + 1] = {};
PWSTR cString;
HRESULT result = SHGetKnownFolderPath(FOLDERID_Documents, 0, nullptr, &cString);
if (SUCCEEDED(result))
userPath = std::filesystem::path(cString);
else
Log(Debug::Error) << "Error " << result << " when getting Documents path";
if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_PERSONAL | CSIDL_FLAG_CREATE, nullptr, 0, path)))
{
userPath = std::filesystem::path(path);
}
CoTaskMemFree(cString);
return userPath / "My Games" / mName;
}
@ -54,14 +56,19 @@ namespace Files
std::filesystem::path WindowsPath::getGlobalConfigPath() const
{
// The concept of a global config path is absurd on Windows.
// Always use local config instead.
// The virtual base class requires that we provide this, though.
std::filesystem::path globalPath = std::filesystem::current_path();
WCHAR path[MAX_PATH + 1] = {};
PWSTR cString;
HRESULT result = SHGetKnownFolderPath(FOLDERID_ProgramFiles, 0, nullptr, &cString);
if (SUCCEEDED(result))
globalPath = std::filesystem::path(cString);
else
Log(Debug::Error) << "Error " << result << " when getting Program Files path";
if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_PROGRAM_FILES | CSIDL_FLAG_CREATE, nullptr, 0, path)))
{
globalPath = std::filesystem::path(path);
}
CoTaskMemFree(cString);
return globalPath / mName;
}

View file

@ -227,9 +227,10 @@ namespace
namespace Gui
{
FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor)
FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor, bool exportFonts)
: mVFS(vfs)
, mScalingFactor(scalingFactor)
, mExportFonts(exportFonts)
{
if (encoding == ToUTF8::WINDOWS_1252)
mEncoding = ToUTF8::CP437;
@ -407,7 +408,8 @@ namespace Gui
file.reset();
// Create the font texture
std::string bitmapFilename = "fonts/" + std::string(name_) + ".tex";
const std::string name(name_);
const std::string bitmapFilename = "fonts/" + name + ".tex";
Files::IStreamPtr bitmapFile = mVFS->get(bitmapFilename);
@ -429,6 +431,19 @@ namespace Gui
<< bitmapFile->gcount() << "/" << (width * height * 4) << " bytes)";
bitmapFile.reset();
if (mExportFonts)
{
osg::ref_ptr<osg::Image> image = new osg::Image;
image->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE);
assert(image->isDataContiguous());
memcpy(image->data(), textureData.data(), textureData.size());
// Convert to OpenGL origin for sensible output
image->flipVertical();
Log(Debug::Info) << "Writing " << name + ".png";
osgDB::writeImageFile(*image, name + ".png");
}
MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture(bitmapFilename);
tex->createManual(width, height, MyGUI::TextureUsage::Write, MyGUI::PixelFormat::R8G8B8A8);
unsigned char* texData = reinterpret_cast<unsigned char*>(tex->lock(MyGUI::TextureUsage::Write));
@ -626,6 +641,13 @@ namespace Gui
code->addAttribute("size", "0 0");
}
if (mExportFonts)
{
Log(Debug::Info) << "Writing " << name + ".xml";
xmlDocument.createDeclaration();
xmlDocument.save(name + ".xml");
}
// Register the font with MyGUI
MyGUI::ResourceManualFont* font = static_cast<MyGUI::ResourceManualFont*>(
MyGUI::FactoryManager::getInstance().createObject("Resource", "ResourceManualFont"));

View file

@ -25,7 +25,8 @@ namespace Gui
class FontLoader
{
public:
FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor);
/// @param exportFonts export the converted fonts (Images and XML with glyph metrics) to files?
FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, float scalingFactor, bool exportFonts);
void overrideLineHeight(MyGUI::xml::ElementPtr _node, std::string_view _file, MyGUI::Version _version);
@ -35,6 +36,7 @@ namespace Gui
ToUTF8::FromType mEncoding;
const VFS::Manager* mVFS;
float mScalingFactor;
bool mExportFonts;
void loadFonts();
void loadFont(const std::string& fontName, const std::string& fontId);

View file

@ -11,6 +11,7 @@
#include <components/files/conversion.hpp>
#include <components/vfs/manager.hpp>
#include "luastateptr.hpp"
#include "scriptscontainer.hpp"
#include "utf8.hpp"
@ -151,37 +152,37 @@ namespace LuaUtil
return newPtr;
}
lua_State* LuaState::createLuaRuntime(LuaState* luaState)
LuaStatePtr LuaState::createLuaRuntime(LuaState* luaState)
{
if (sProfilerEnabled)
{
Log(Debug::Info) << "Initializing LuaUtil::LuaState with profiler";
lua_State* L = lua_newstate(&trackingAllocator, luaState);
if (L)
return L;
else
{
LuaStatePtr state(lua_newstate(&trackingAllocator, luaState));
if (state != nullptr)
return state;
sProfilerEnabled = false;
Log(Debug::Error)
<< "Failed to initialize LuaUtil::LuaState with custom allocator; disabling Lua profiler";
}
Log(Debug::Error) << "Failed to initialize LuaUtil::LuaState with custom allocator; disabling Lua profiler";
}
Log(Debug::Info) << "Initializing LuaUtil::LuaState without profiler";
lua_State* L = luaL_newstate();
if (!L)
throw std::runtime_error("Can't create Lua runtime");
return L;
LuaStatePtr state(luaL_newstate());
if (state == nullptr)
throw std::runtime_error("Failed to create Lua runtime");
return state;
}
LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf, const LuaStateSettings& settings)
: mSettings(settings)
, mLuaHolder(createLuaRuntime(this))
, mSol(mLuaHolder.get())
, mLuaState([&] {
LuaStatePtr state = createLuaRuntime(this);
sol::set_default_state(state.get());
return state;
}())
, mSol(mLuaState.get())
, mConf(conf)
, mVFS(vfs)
{
if (sProfilerEnabled)
lua_sethook(mLuaHolder.get(), &countHook, LUA_MASKCOUNT, countHookStep);
lua_sethook(mLuaState.get(), &countHook, LUA_MASKCOUNT, countHookStep);
protectedCall([&](LuaView& view) {
auto& sol = view.sol();

View file

@ -10,6 +10,7 @@
#include <components/vfs/pathutil.hpp>
#include "configuration.hpp"
#include "luastateptr.hpp"
namespace VFS
{
@ -188,7 +189,7 @@ namespace LuaUtil
static void countHook(lua_State* L, lua_Debug* ar);
static void* trackingAllocator(void* ud, void* ptr, size_t osize, size_t nsize);
lua_State* createLuaRuntime(LuaState* luaState);
static LuaStatePtr createLuaRuntime(LuaState* luaState);
struct AllocOwner
{
@ -206,25 +207,8 @@ namespace LuaUtil
uint64_t mSmallAllocMemoryUsage = 0;
std::vector<int64_t> mMemoryUsage;
class LuaStateHolder
{
public:
LuaStateHolder(lua_State* L)
: L(L)
{
sol::set_default_state(L);
}
~LuaStateHolder() { lua_close(L); }
LuaStateHolder(const LuaStateHolder&) = delete;
LuaStateHolder(LuaStateHolder&&) = delete;
lua_State* get() { return L; }
private:
lua_State* L;
};
// Must be declared before mSol and all sol-related objects. Then on exit it will be destructed the last.
LuaStateHolder mLuaHolder;
LuaStatePtr mLuaState;
sol::state_view mSol;
const ScriptsConfiguration* mConf;

View file

@ -0,0 +1,18 @@
#ifndef OPENMW_COMPONENTS_LUA_LUASTATEPTR_H
#define OPENMW_COMPONENTS_LUA_LUASTATEPTR_H
#include <sol/state.hpp>
#include <memory>
namespace LuaUtil
{
struct CloseLuaState
{
void operator()(lua_State* state) noexcept { lua_close(state); }
};
using LuaStatePtr = std::unique_ptr<lua_State, CloseLuaState>;
}
#endif

View file

@ -391,6 +391,7 @@ namespace NifOsg
osg::ref_ptr<SceneUtil::Skeleton> skel = new SceneUtil::Skeleton;
skel->setStateSet(created->getStateSet());
skel->setName(created->getName());
skel->setUserDataContainer(created->getUserDataContainer());
for (unsigned int i = 0; i < created->getNumChildren(); ++i)
skel->addChild(created->getChild(i));
created->removeChildren(0, created->getNumChildren());

View file

@ -119,7 +119,8 @@ namespace SceneUtil
osg::ref_ptr<osg::Light> light(new osg::Light);
lightSource->setNodeMask(lightMask);
float radius = esmLight.mRadius;
// The minimum scene light radius is 16 in Morrowind
const float radius = std::max(esmLight.mRadius, 16.f);
lightSource->setRadius(radius);
configureLight(light, radius, isExterior);

View file

@ -2,10 +2,7 @@
#define STEREO_FRUSTUM_H
#include <osg/BoundingBox>
#include <osg/Camera>
#include <osg/Matrix>
#include <osg/StateSet>
#include <osg/Vec3>
#include <array>
#include <map>
@ -13,6 +10,7 @@
namespace osg
{
class Camera;
class FrameBufferObject;
class Texture2D;
class Texture2DMultisample;
@ -24,7 +22,7 @@ namespace osgViewer
class Viewer;
}
namespace usgUtil
namespace osgUtil
{
class CullVisitor;
}

View file

@ -1,6 +1,7 @@
#ifndef OPENMW_COMPONENTS_TESTING_UTIL_H
#define OPENMW_COMPONENTS_TESTING_UTIL_H
#include <chrono>
#include <filesystem>
#include <initializer_list>
#include <memory>
@ -14,26 +15,37 @@
namespace TestingOpenMW
{
inline std::filesystem::path outputFilePath(const std::string name)
inline std::filesystem::path outputDir()
{
std::filesystem::path dir("tests_output");
std::filesystem::create_directory(dir);
static const std::string run
= std::to_string(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
std::filesystem::path dir = std::filesystem::temp_directory_path() / "openmw" / "tests" / run;
std::filesystem::create_directories(dir);
return dir;
}
inline std::filesystem::path outputFilePath(std::string_view name)
{
std::filesystem::path dir = outputDir();
return dir / Misc::StringUtils::stringToU8String(name);
}
inline std::filesystem::path outputDirPath(const std::filesystem::path& subpath)
{
std::filesystem::path path = outputDir();
path /= subpath;
std::filesystem::create_directories(path);
return path;
}
inline std::filesystem::path outputFilePathWithSubDir(const std::filesystem::path& subpath)
{
std::filesystem::path path("tests_output");
std::filesystem::path path = outputDir();
path /= subpath;
std::filesystem::create_directories(path.parent_path());
return path;
}
inline std::filesystem::path temporaryFilePath(const std::string name)
{
return std::filesystem::temp_directory_path() / name;
}
class VFSTestFile : public VFS::File
{
public:

View file

@ -28,13 +28,8 @@ A `Launchpad PPA <https://launchpad.net/~openmw/+archive/openmw>`_ is available.
Add it and install OpenMW::
$ sudo add-apt-repository ppa:openmw/openmw
$ sudo apt-get update
$ sudo apt-get install openmw openmw-launcher
.. note::
OpenMW-CS must be installed separately by typing::
$ sudo apt-get install openmw-cs
$ sudo apt update
$ sudo apt install openmw
The Arch Linux Way
==================

View file

@ -15,6 +15,8 @@ Morrowind .fnt fonts
Morrowind uses a custom ``.fnt`` file format. It is not compatible with the Windows Font File ``.fnt`` format.
To our knowledge, the format is undocumented. OpenMW can load this format and convert it on the fly into something usable
(see font loader `source code <https://gitlab.com/OpenMW/openmw/blob/master/components/fontloader/fontloader.cpp>`_).
You can use --export-fonts command line option to write the converted font
(a PNG image and an XML file describing the position of each glyph in the image) to the current directory.
They can be used instead of TrueType fonts if needed by specifying their ``.fnt`` files names in the ``openmw.cfg``. For example:

3
extern/.clang-tidy vendored Normal file
View file

@ -0,0 +1,3 @@
Checks: >-
-clang-analyzer-core.NullDereference,
-clang-analyzer-cplusplus.NewDelete

View file

@ -2,13 +2,13 @@
<Resource type="ResourceTrueTypeFont">
<Property key="Source" value="MysticCards.ttf"/>
<Property key="Antialias" value="false"/>
<Property key="SubstituteCode" value="63"/>
<Property key="SubstituteCode" value="95"/>
<Property key="TabWidth" value="8"/>
<Property key="OffsetHeight" value="0"/>
<Property key="Resolution" value="70"/>
<Codes>
<Code range="33 126"/>
<Code range="161"/>
<Code range="160 161"/>
<Code range="173"/>
<Code range="175 177"/>
<Code range="180"/>

Binary file not shown.

View file

@ -24,7 +24,7 @@ return {
interface = {
--- Interface version
-- @field [parent=#Crimes] #number version
version = 1,
version = 2,
---
-- Commits a crime as if done through an in-game action. Can only be used in global context.
@ -42,7 +42,7 @@ return {
"faction id passed to commitCrime must be a string or nil")
assert(type(options.arg) == "number" or options.arg == nil,
"arg value passed to commitCrime must be a number or nil")
assert(type(options.victimAware) == "number" or options.victimAware == nil,
assert(type(options.victimAware) == "boolean" or options.victimAware == nil,
"victimAware value passed to commitCrime must be a boolean or nil")
assert(options.type ~= nil, "crime type passed to commitCrime cannot be nil")

View file

@ -221,14 +221,14 @@
-- Can be used only in local scripts on self. Can also be evoked by sending an AddVfx event to the target actor.
-- @function [parent=#animation] addVfx
-- @param openmw.core#GameObject actor
-- @param #string model #string model path (normally taken from a record such as @{openmw.types#StaticRecord.model} or similar)
-- @param #string model path (normally taken from a record such as @{openmw.types#StaticRecord.model} or similar)
-- @param #table options optional table of parameters. Can contain:
--
-- * `loop` - boolean, if true the effect will loop until removed (default: 0).
-- * `loop` - boolean, if true the effect will loop until removed (default: false).
-- * `boneName` - name of the bone to attach the vfx to. (default: "")
-- * `particleTextureOverride` - name of the particle texture to use. (default: "")
-- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: "").
-- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: 1)
-- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true)
--
-- @usage local mgef = core.magic.effects.records[myEffectName]
-- anim.addVfx(self, 'VFX_Hands', {boneName = 'Bip01 L Hand', particleTextureOverride = mgef.particle, loop = mgef.continuousVfx, vfxId = mgef.id..'_myuniquenamehere'})

View file

@ -59,7 +59,7 @@
-- @field [parent=#nearby] #COLLISION_TYPE COLLISION_TYPE
---
-- Result of raycasing
-- Result of raycasting
-- @type RayCastingResult
-- @field [parent=#RayCastingResult] #boolean hit Is there a collision? (true/false)
-- @field [parent=#RayCastingResult] openmw.util#Vector3 hitPos Position of the collision point (nil if no collision)
@ -69,7 +69,7 @@
---
-- A table of parameters for @{#nearby.castRay}
-- @type CastRayOptions
-- @field openmw.core#GameObject ignore An object to ignore (specify here the source of the ray)
-- @field #any ignore An @{openmw.core#GameObject} or @{openmw.core#ObjectList} to ignore (specify here the source of the ray, or other objects which should not collide)
-- @field #number collisionType Object types to work with (see @{openmw.nearby#COLLISION_TYPE})
-- @field #number radius The radius of the ray (zero by default). If not zero then castRay actually casts a sphere with given radius.
-- NOTE: currently `ignore` is not supported if `radius>0`.
@ -92,7 +92,7 @@
---
-- A table of parameters for @{#nearby.castRenderingRay} and @{#nearby.asyncCastRenderingRay}
-- @type CastRenderingRayOptions
-- @field #table ignore A list of @{openmw.core#GameObject} to ignore while doing the ray cast
-- @field #any ignore A @{openmw.core#GameObject} or @{openmw.core#ObjectList} to ignore while doing the ray cast
---
-- Cast ray from one point to another and find the first visual intersection with anything in the scene.
@ -102,7 +102,7 @@
-- @function [parent=#nearby] castRenderingRay
-- @param openmw.util#Vector3 from Start point of the ray.
-- @param openmw.util#Vector3 to End point of the ray.
-- @param #CastRenderingRayOptions
-- @param #CastRenderingRayOptions options An optional table with additional optional arguments
-- @return #RayCastingResult
---

View file

@ -45,7 +45,7 @@
-- @field [parent=#ActorControls] #boolean run true - run, false - walk
-- @field [parent=#ActorControls] #boolean sneak If true - sneak
-- @field [parent=#ActorControls] #boolean jump If true - initiate a jump
-- @field [parent=#ActorControls] #ATTACK_TYPE use Activates the readied weapon/spell according to a provided value. For weapons, keeping this value modified will charge the attack until set to @{#ATTACK_TYPE.NoAttack}. If an @#ATTACK_TYPE} not appropriate for a currently equipped weapon provided - an appropriate @{#ATTACK_TYPE} will be used instead.
-- @field [parent=#ActorControls] #ATTACK_TYPE use Activates the readied weapon/spell according to a provided value. For weapons, keeping this value modified will charge the attack until set to @{#ATTACK_TYPE.NoAttack}. If an @{#ATTACK_TYPE} not appropriate for a currently equipped weapon provided - an appropriate @{#ATTACK_TYPE} will be used instead.
---
-- Enables or disables standard AI (enabled by default).

View file

@ -1123,7 +1123,7 @@
-- @field #string id The record ID of the NPC
-- @field #string name
-- @field #string race
-- @field #string class Name of the NPC's class (e. g. Acrobat)
-- @field #string class ID of the NPC's class (e.g. acrobat)
-- @field #string model Path to the model associated with this NPC, used for animations.
-- @field #string mwscript MWScript on this NPC (can be nil)
-- @field #string hair Path to the hair body part model

View file

@ -7,7 +7,7 @@ local vfs = require('openmw.vfs')
local world = require('openmw.world')
local I = require('openmw.interfaces')
local function testTimers()
testing.registerGlobalTest('timers', function()
testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result')
testing.expectAlmostEqual(core.getSimulationTimeScale(), 1, 'incorrect getSimulationTimeScale result')
@ -39,9 +39,10 @@ local function testTimers()
testing.expectGreaterOrEqual(ts1, 0.5, 'async:newSimulationTimer failed')
testing.expectGreaterOrEqual(th2, 72, 'async:newUnsavableGameTimer failed')
testing.expectGreaterOrEqual(ts2, 1, 'async:newUnsavableSimulationTimer failed')
end
end)
local function testTeleport()
testing.registerGlobalTest('teleport', function()
local player = world.players[1]
player:teleport('', util.vector3(100, 50, 500), util.transform.rotateZ(math.rad(90)))
coroutine.yield()
testing.expect(player.cell.isExterior, 'teleport to exterior failed')
@ -71,16 +72,16 @@ local function testTeleport()
testing.expectEqualWithDelta(player.position.x, 50, 1, 'incorrect position after teleporting')
testing.expectEqualWithDelta(player.position.y, -100, 1, 'incorrect position after teleporting')
testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(-90), 0.05, 'teleporting changes rotation')
end
end)
local function testGetGMST()
testing.registerGlobalTest('getGMST', function()
testing.expectEqual(core.getGMST('non-existed gmst'), nil)
testing.expectEqual(core.getGMST('Water_RippleFrameCount'), 4)
testing.expectEqual(core.getGMST('Inventory_DirectionalDiffuseR'), 0.5)
testing.expectEqual(core.getGMST('Level_Up_Level2'), 'something')
end
end)
local function testMWScript()
testing.registerGlobalTest('MWScript', function()
local variableStoreCount = 18
local variableStore = world.mwscript.getGlobalVariables(player)
testing.expectEqual(variableStoreCount, #variableStore)
@ -100,7 +101,7 @@ local function testMWScript()
indexCheck = indexCheck + 1
end
testing.expectEqual(variableStoreCount, indexCheck)
end
end)
local function testRecordStore(store, storeName, skipPairs)
testing.expect(store.records)
@ -121,7 +122,7 @@ local function testRecordStore(store, storeName, skipPairs)
testing.expectEqual(status, true, storeName)
end
local function testRecordStores()
testing.registerGlobalTest('record stores', function()
for key, type in pairs(types) do
if type.records then
testRecordStore(type, key)
@ -140,9 +141,9 @@ local function testRecordStores()
testRecordStore(types.NPC.classes, "classes")
testRecordStore(types.NPC.races, "races")
testRecordStore(types.Player.birthSigns, "birthSigns")
end
end)
local function testRecordCreation()
testing.registerGlobalTest('record creation', function()
local newLight = {
isCarriable = true,
isDynamic = true,
@ -165,9 +166,9 @@ local function testRecordCreation()
for key, value in pairs(newLight) do
testing.expectEqual(record[key], value)
end
end
end)
local function testUTF8Chars()
testing.registerGlobalTest('UTF-8 characters', function()
testing.expectEqual(utf8.codepoint("😀"), 0x1F600)
local chars = {}
@ -192,9 +193,9 @@ local function testUTF8Chars()
testing.expectEqual(utf8.codepoint(char), codepoint)
testing.expectEqual(utf8.len(char), 1)
end
end
end)
local function testUTF8Strings()
testing.registerGlobalTest('UTF-8 strings', function()
local utf8str = "Hello, 你好, 🌎!"
local str = ""
@ -205,9 +206,9 @@ local function testUTF8Strings()
testing.expectEqual(utf8.len(utf8str), 13)
testing.expectEqual(utf8.offset(utf8str, 9), 11)
end
end)
local function testMemoryLimit()
testing.registerGlobalTest('memory limit', function()
local ok, err = pcall(function()
local t = {}
local n = 1
@ -218,14 +219,16 @@ local function testMemoryLimit()
end)
testing.expectEqual(ok, false, 'Script reaching memory limit should fail')
testing.expectEqual(err, 'not enough memory')
end
end)
local function initPlayer()
local player = world.players[1]
player:teleport('', util.vector3(4096, 4096, 1745), util.transform.identity)
coroutine.yield()
return player
end
local function testVFS()
testing.registerGlobalTest('vfs', function()
local file = 'test_vfs_dir/lines.txt'
local nosuchfile = 'test_vfs_dir/nosuchfile'
testing.expectEqual(vfs.fileExists(file), true, 'lines.txt should exist')
@ -269,12 +272,11 @@ local function testVFS()
for _,v in pairs(expectedLines) do
testing.expectEqual(getLine(), v)
end
end
end)
local function testCommitCrime()
initPlayer()
local player = world.players[1]
testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `testCommitCrime`')
testing.registerGlobalTest('commit crime', function()
local player = initPlayer()
testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `commit crime`')
testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts')
-- Reset crime level to have a clean slate
@ -292,82 +294,59 @@ local function testCommitCrime()
types.Player.setCrimeLevel(player, 0)
testing.expectEqual(I.Crimes.commitCrime(player, { victim = victim, type = types.Player.OFFENSE_TYPE.Theft, arg = 50 }).wasCrimeSeen, true, "Running a crime with a valid victim should notify them when the player is not sneaking, even if it's not explicitly passed in")
testing.expectEqual(types.Player.getCrimeLevel(player), 0, "Crime level should not change if the victim's alarm value is low and there's no other witnesses")
end
end)
local function testRecordModelProperty()
initPlayer()
testing.registerGlobalTest('record model property', function()
local player = world.players[1]
testing.expectEqual(types.NPC.record(player).model, 'meshes/basicplayer.dae')
end)
local function registerPlayerTest(name)
testing.registerGlobalTest(name, function()
local player = initPlayer()
testing.runLocalTest(player, name)
end)
end
tests = {
{'timers', testTimers},
{'rotating player with controls.yawChange should change rotation', function()
initPlayer()
testing.runLocalTest(player, 'playerYawRotation')
end},
{'rotating player with controls.pitchChange should change rotation', function()
initPlayer()
testing.runLocalTest(player, 'playerPitchRotation')
end},
{'rotating player with controls.pitchChange and controls.yawChange should change rotation', function()
initPlayer()
testing.runLocalTest(player, 'playerPitchAndYawRotation')
end},
{'rotating player should not lead to nan rotation', function()
initPlayer()
testing.runLocalTest(player, 'playerRotation')
end},
{'playerForwardRunning', function()
initPlayer()
testing.runLocalTest(player, 'playerForwardRunning')
end},
{'playerDiagonalWalking', function()
initPlayer()
testing.runLocalTest(player, 'playerDiagonalWalking')
end},
{'findPath', function()
initPlayer()
testing.runLocalTest(player, 'findPath')
end},
{'findRandomPointAroundCircle', function()
initPlayer()
testing.runLocalTest(player, 'findRandomPointAroundCircle')
end},
{'castNavigationRay', function()
initPlayer()
testing.runLocalTest(player, 'castNavigationRay')
end},
{'findNearestNavMeshPosition', function()
initPlayer()
testing.runLocalTest(player, 'findNearestNavMeshPosition')
end},
{'teleport', testTeleport},
{'getGMST', testGetGMST},
{'recordStores', testRecordStores},
{'recordCreation', testRecordCreation},
{'utf8Chars', testUTF8Chars},
{'utf8Strings', testUTF8Strings},
{'mwscript', testMWScript},
{'testMemoryLimit', testMemoryLimit},
{'playerMemoryLimit', function()
initPlayer()
testing.runLocalTest(player, 'playerMemoryLimit')
end},
{'player with equipped weapon on attack should damage health of other actors', function()
initPlayer()
registerPlayerTest('player yaw rotation')
registerPlayerTest('player pitch rotation')
registerPlayerTest('player pitch and yaw rotation')
registerPlayerTest('player rotation')
registerPlayerTest('player forward running')
registerPlayerTest('player diagonal walking')
registerPlayerTest('findPath')
registerPlayerTest('findRandomPointAroundCircle')
registerPlayerTest('castNavigationRay')
registerPlayerTest('findNearestNavMeshPosition')
registerPlayerTest('player memory limit')
testing.registerGlobalTest('player weapon attack', function()
local player = initPlayer()
world.createObject('basic_dagger1h', 1):moveInto(player)
testing.runLocalTest(player, 'playerWeaponAttack')
end},
{'vfs', testVFS},
{'testCommitCrime', testCommitCrime},
{'recordModelProperty', testRecordModelProperty},
}
testing.runLocalTest(player, 'player weapon attack')
end)
testing.registerGlobalTest('load while teleporting - init player', function()
local player = world.players[1]
player:teleport('Museum of Wonders', util.vector3(0, -1500, 111), util.transform.rotateZ(math.rad(180)))
end)
testing.registerGlobalTest('load while teleporting - teleport', function()
local player = world.players[1]
local landracer = world.createObject('landracer')
landracer:teleport(player.cell, player.position + util.vector3(0, 500, 0))
coroutine.yield()
local door = world.getObjectByFormId(core.getFormId('the_hub.omwaddon', 26))
door:activateBy(player)
coroutine.yield()
landracer:teleport(player.cell, player.position)
end)
return {
engineHandlers = {
onUpdate = testing.testRunner(tests),
onPlayerAdded = function(p) player = p end,
onUpdate = testing.updateGlobal,
},
eventHandlers = testing.eventHandlers,
eventHandlers = testing.globalEventHandlers,
}

View file

@ -0,0 +1,94 @@
local testing = require('testing_util')
local matchers = require('matchers')
local menu = require('openmw.menu')
testing.registerMenuTest('save and load', function()
menu.newGame()
coroutine.yield()
menu.saveGame('save and load')
coroutine.yield()
local directorySaves = {}
directorySaves['save_and_load.omwsave'] = {
playerName = '',
playerLevel = 1,
timePlayed = 0,
description = 'save and load',
contentFiles = {
'builtin.omwscripts',
'template.omwgame',
'landracer.omwaddon',
'the_hub.omwaddon',
'test_lua_api.omwscripts',
},
creationTime = matchers.isAny(),
}
local expectedAllSaves = {}
expectedAllSaves[' - 1'] = directorySaves
testing.expectThat(menu.getAllSaves(), matchers.equalTo(expectedAllSaves))
menu.loadGame(' - 1', 'save_and_load.omwsave')
coroutine.yield()
menu.deleteGame(' - 1', 'save_and_load.omwsave')
testing.expectThat(menu.getAllSaves(), matchers.equalTo({}))
end)
testing.registerMenuTest('load while teleporting', function()
menu.newGame()
coroutine.yield()
testing.runGlobalTest('load while teleporting - init player')
menu.saveGame('load while teleporting')
coroutine.yield()
testing.runGlobalTest('load while teleporting - teleport')
menu.loadGame(' - 1', 'load_while_teleporting.omwsave')
coroutine.yield()
menu.deleteGame(' - 1', 'load_while_teleporting.omwsave')
end)
local function registerGlobalTest(name, description)
testing.registerMenuTest(description or name, function()
menu.newGame()
coroutine.yield()
testing.runGlobalTest(name)
end)
end
registerGlobalTest('timers')
registerGlobalTest('teleport')
registerGlobalTest('getGMST')
registerGlobalTest('MWScript')
registerGlobalTest('record stores')
registerGlobalTest('record creation')
registerGlobalTest('UTF-8 characters')
registerGlobalTest('UTF-8 strings')
registerGlobalTest('memory limit')
registerGlobalTest('vfs')
registerGlobalTest('commit crime')
registerGlobalTest('record model property')
registerGlobalTest('player yaw rotation', 'rotating player with controls.yawChange should change rotation')
registerGlobalTest('player pitch rotation', 'rotating player with controls.pitchChange should change rotation')
registerGlobalTest('player pitch and yaw rotation', 'rotating player with controls.pitchChange and controls.yawChange should change rotation')
registerGlobalTest('player rotation', 'rotating player should not lead to nan rotation')
registerGlobalTest('player forward running')
registerGlobalTest('player diagonal walking')
registerGlobalTest('findPath')
registerGlobalTest('findRandomPointAroundCircle')
registerGlobalTest('castNavigationRay')
registerGlobalTest('findNearestNavMeshPosition')
registerGlobalTest('player memory limit')
registerGlobalTest('player weapon attack', 'player with equipped weapon on attack should damage health of other actors')
return {
engineHandlers = {
onFrame = testing.makeUpdateMenu(),
},
eventHandlers = testing.menuEventHandlers,
}

View file

@ -1,4 +1,4 @@
content=test.omwscripts
content=test_lua_api.omwscripts
# Needed to test `core.getGMST`
fallback=Water_RippleFrameCount,4

View file

@ -6,6 +6,7 @@ local input = require('openmw.input')
local types = require('openmw.types')
local nearby = require('openmw.nearby')
local camera = require('openmw.camera')
local matchers = require('matchers')
types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Controls, false)
types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Fighting, false)
@ -40,7 +41,7 @@ local function rotateByPitch(object, target)
rotate(object, target, nil)
end
testing.registerLocalTest('playerYawRotation',
testing.registerLocalTest('player yaw rotation',
function()
local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ()
local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX()
@ -60,7 +61,7 @@ testing.registerLocalTest('playerYawRotation',
testing.expectEqualWithDelta(gamma2, initialGammaZYX, 0.05, 'Gamma rotation in ZYX convention should not change')
end)
testing.registerLocalTest('playerPitchRotation',
testing.registerLocalTest('player pitch rotation',
function()
local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ()
local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX()
@ -80,7 +81,7 @@ testing.registerLocalTest('playerPitchRotation',
testing.expectEqualWithDelta(gamma2, targetPitch, 0.05, 'Incorrect gamma rotation in ZYX convention')
end)
testing.registerLocalTest('playerPitchAndYawRotation',
testing.registerLocalTest('player pitch and yaw rotation',
function()
local targetPitch = math.rad(-30)
local targetYaw = math.rad(-60)
@ -99,7 +100,7 @@ testing.registerLocalTest('playerPitchAndYawRotation',
testing.expectEqualWithDelta(gamma2, math.rad(-16), 0.05, 'Incorrect gamma rotation in ZYX convention')
end)
testing.registerLocalTest('playerRotation',
testing.registerLocalTest('player rotation',
function()
local rotation = math.sqrt(2)
local endTime = core.getSimulationTime() + 3
@ -113,17 +114,17 @@ testing.registerLocalTest('playerRotation',
coroutine.yield()
local alpha1, gamma1 = self.rotation:getAnglesXZ()
testing.expectThat(alpha1, testing.isNotNan(), 'Alpha rotation in XZ convention is nan')
testing.expectThat(gamma1, testing.isNotNan(), 'Gamma rotation in XZ convention is nan')
testing.expectThat(alpha1, matchers.isNotNan(), 'Alpha rotation in XZ convention is nan')
testing.expectThat(gamma1, matchers.isNotNan(), 'Gamma rotation in XZ convention is nan')
local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX()
testing.expectThat(alpha2, testing.isNotNan(), 'Alpha rotation in ZYX convention is nan')
testing.expectThat(beta2, testing.isNotNan(), 'Beta rotation in ZYX convention is nan')
testing.expectThat(gamma2, testing.isNotNan(), 'Gamma rotation in ZYX convention is nan')
testing.expectThat(alpha2, matchers.isNotNan(), 'Alpha rotation in ZYX convention is nan')
testing.expectThat(beta2, matchers.isNotNan(), 'Beta rotation in ZYX convention is nan')
testing.expectThat(gamma2, matchers.isNotNan(), 'Gamma rotation in ZYX convention is nan')
end
end)
testing.registerLocalTest('playerForwardRunning',
testing.registerLocalTest('player forward running',
function()
local startPos = self.position
local endTime = core.getSimulationTime() + 1
@ -141,7 +142,7 @@ testing.registerLocalTest('playerForwardRunning',
testing.expectEqualWithDelta(direction.y, 1, 0.1, 'Run forward, Y coord')
end)
testing.registerLocalTest('playerDiagonalWalking',
testing.registerLocalTest('player diagonal walking',
function()
local startPos = self.position
local endTime = core.getSimulationTime() + 1
@ -220,7 +221,7 @@ testing.registerLocalTest('findNearestNavMeshPosition',
'Navigation mesh position ' .. testing.formatActualExpected(result, expected))
end)
testing.registerLocalTest('playerMemoryLimit',
testing.registerLocalTest('player memory limit',
function()
local ok, err = pcall(function()
local str = 'a'
@ -232,7 +233,7 @@ testing.registerLocalTest('playerMemoryLimit',
testing.expectEqual(err, 'not enough memory')
end)
testing.registerLocalTest('playerWeaponAttack',
testing.registerLocalTest('player weapon attack',
function()
camera.setMode(camera.MODE.ThirdPerson)
@ -346,5 +347,5 @@ return {
engineHandlers = {
onFrame = testing.updateLocal,
},
eventHandlers = testing.eventHandlers
eventHandlers = testing.localEventHandlers,
}

View file

@ -1,2 +0,0 @@
GLOBAL: test.lua
PLAYER: player.lua

View file

@ -0,0 +1,3 @@
MENU: menu.lua
GLOBAL: global.lua
PLAYER: player.lua

View file

@ -0,0 +1,218 @@
local module = {}
---
-- Matcher verifying that distance between given value and expected is not greater than maxDistance.
-- @function elementsAreArray
-- @param expected#vector.
-- @usage
-- expectThat(util.vector2(0, 0), closeToVector(util.vector2(0, 1), 1))
function module.closeToVector(expected, maxDistance)
return function(actual)
local distance = (expected - actual):length()
if distance <= maxDistance then
return ''
end
return string.format('%s is too far from expected %s: %s > %s', actual, expected, distance, maxDistance)
end
end
---
-- Matcher verifying that given value is an array each element of which matches elements of expected.
-- @function elementsAreArray
-- @param expected#array of values or matcher functions.
-- @usage
-- local t = {42, 13}
-- local matcher = function(actual)
-- if actual ~= 42 then
-- return string.format('%s is not 42', actual)
-- end
-- return ''
-- end
-- expectThat({42, 13}, elementsAreArray({matcher, 13}))
function module.elementsAreArray(expected)
local expected_matchers = {}
for i, v in ipairs(expected) do
if type(v) == 'function' then
expected_matchers[i] = v
else
expected_matchers[i] = function (other)
if expected[i].__eq(expected[i], other) then
return ''
end
return string.format('%s element %s does no match expected: %s', i, other, expected[i])
end
end
end
return function(actual)
if #actual < #expected_matchers then
return string.format('number of elements is less than expected: %s < %s', #actual, #expected_matchers)
end
local message = ''
for i, v in ipairs(actual) do
if i > #expected_matchers then
message = string.format('%s\n%s element is out of expected range: %s', message, i, #expected_matchers)
break
end
local match_message = expected_matchers[i](v)
if match_message ~= '' then
message = string.format('%s\n%s', message, match_message)
end
end
return message
end
end
---
-- Matcher verifying that given number is not a nan.
-- @function isNotNan
-- @usage
-- expectThat(value, isNotNan())
function module.isNotNan()
return function(actual)
if actual ~= actual then
return 'actual value is nan, expected to be not nan'
end
return ''
end
end
---
-- Matcher accepting any value.
-- @function isAny
-- @usage
-- expectThat(value, isAny())
function module.isAny()
return function(actual)
return ''
end
end
local function serializeArray(a)
local result = nil
for _, v in ipairs(a) do
if result == nil then
result = string.format('{%s', serialize(v))
else
result = string.format('%s, %s', result, serialize(v))
end
end
if result == nil then
return '{}'
end
return string.format('%s}', result)
end
local function serializeTable(t)
local result = nil
for k, v in pairs(t) do
if result == nil then
result = string.format('{%q = %s', k, serialize(v))
else
result = string.format('%s, %q = %s', result, k, serialize(v))
end
end
if result == nil then
return '{}'
end
return string.format('%s}', result)
end
local function isArray(t)
local i = 1
for _ in pairs(t) do
if t[i] == nil then
return false
end
i = i + 1
end
return true
end
function serialize(v)
local t = type(v)
if t == 'string' then
return string.format('%q', v)
elseif t == 'table' then
if isArray(v) then
return serializeArray(v)
end
return serializeTable(v)
end
return string.format('%s', v)
end
local function compareScalars(v1, v2)
if v1 == v2 then
return ''
end
if type(v1) == 'string' then
return string.format('%q ~= %q', v1, v2)
end
return string.format('%s ~= %s', v1, v2)
end
local function collectKeys(t)
local result = {}
for key in pairs(t) do
table.insert(result, key)
end
table.sort(result)
return result
end
local function compareTables(t1, t2)
local keys1 = collectKeys(t1)
local keys2 = collectKeys(t2)
if #keys1 ~= #keys2 then
return string.format('table size mismatch: %d ~= %d', #keys1, #keys2)
end
for i = 1, #keys1 do
local key1 = keys1[i]
local key2 = keys2[i]
if key1 ~= key2 then
return string.format('table keys mismatch: %q ~= %q', key1, key2)
end
local d = compare(t1[key1], t2[key2])
if d ~= '' then
return string.format('table values mismatch at key %s: %s', serialize(key1), d)
end
end
return ''
end
function compare(v1, v2)
local type1 = type(v1)
local type2 = type(v2)
if type2 == 'function' then
return v2(v1)
end
if type1 ~= type2 then
return string.format('types mismatch: %s ~= %s', type1, type2)
end
if type1 == 'nil' then
return ''
elseif type1 == 'table' then
return compareTables(v1, v2)
elseif type1 == 'nil' or type1 == 'boolean' or type1 == 'number' or type1 == 'string' then
return compareScalars(v1, v2)
end
error('unsupported type: %s', type1)
end
---
-- Matcher verifying that given value is equal to expected. Accepts nil, boolean, number, string and table or matcher
-- function.
-- @function equalTo
-- @usage
-- expectThat({a = {42, 'foo', {b = true}}}, equalTo({a = {42, 'foo', {b = true}}}))
function module.equalTo(expected)
return function(actual)
local diff = compare(actual, expected)
if diff == '' then
return ''
end
return string.format('%s; actual: %s; expected: %s', diff, serialize(actual, ''), serialize(expected, ''))
end
end
return module

View file

@ -2,11 +2,31 @@ local core = require('openmw.core')
local util = require('openmw.util')
local M = {}
local menuTestsOrder = {}
local menuTests = {}
local globalTestsOrder = {}
local globalTests = {}
local globalTestRunner = nil
local currentGlobalTest = nil
local currentGlobalTestError = nil
local localTests = {}
local localTestRunner = nil
local currentLocalTest = nil
local currentLocalTestError = nil
function M.testRunner(tests)
local fn = function()
local function makeTestCoroutine(fn)
local co = coroutine.create(fn)
return function()
if coroutine.status(co) ~= 'dead' then
coroutine.resume(co)
end
end
end
local function runTests(tests)
for i, test in ipairs(tests) do
local name, fn = unpack(test)
print('TEST_START', i, name)
@ -19,11 +39,48 @@ function M.testRunner(tests)
end
core.quit()
end
local co = coroutine.create(fn)
return function()
if coroutine.status(co) ~= 'dead' then
coroutine.resume(co)
function M.makeUpdateMenu()
return makeTestCoroutine(function()
print('Running menu tests...')
runTests(menuTestsOrder)
end)
end
function M.makeUpdateGlobal()
return makeTestCoroutine(function()
print('Running global tests...')
runTests(globalTestsOrder)
end)
end
function M.registerMenuTest(name, fn)
menuTests[name] = fn
table.insert(menuTestsOrder, {name, fn})
end
function M.runGlobalTest(name)
currentGlobalTest = name
currentGlobalTestError = nil
core.sendGlobalEvent('runGlobalTest', name)
while currentGlobalTest do
coroutine.yield()
end
if currentGlobalTestError then
error(currentGlobalTestError, 2)
end
end
function M.registerGlobalTest(name, fn)
globalTests[name] = fn
table.insert(globalTestsOrder, {name, fn})
end
function M.updateGlobal()
if globalTestRunner and coroutine.status(globalTestRunner) ~= 'dead' then
coroutine.resume(globalTestRunner)
else
globalTestRunner = nil
end
end
@ -39,7 +96,21 @@ function M.runLocalTest(obj, name)
end
end
function M.expect(cond, delta, msg)
function M.registerLocalTest(name, fn)
localTests[name] = fn
end
function M.updateLocal()
if localTestRunner and coroutine.status(localTestRunner) ~= 'dead' then
if not core.isWorldPaused() then
coroutine.resume(localTestRunner)
end
else
localTestRunner = nil
end
end
function M.expect(cond, msg)
if not cond then
error(msg or '"true" expected', 2)
end
@ -87,76 +158,6 @@ function M.expectNotEqual(v1, v2, msg)
end
end
function M.closeToVector(expected, maxDistance)
return function(actual)
local distance = (expected - actual):length()
if distance <= maxDistance then
return ''
end
return string.format('%s is too far from expected %s: %s > %s', actual, expected, distance, maxDistance)
end
end
---
-- Matcher verifying that given value is an array each element of which matches elements of expected.
-- @function elementsAreArray
-- @param expected#array of values or matcher functions.
-- @usage
-- local t = {42, 13}
-- local matcher = function(actual)
-- if actual ~= 42 then
-- return string.format('%s is not 42', actual)
-- end
-- return ''
-- end
-- expectThat({42, 13}, elementsAreArray({matcher, 13}))
function M.elementsAreArray(expected)
local expected_matchers = {}
for i, v in ipairs(expected) do
if type(v) == 'function' then
expected_matchers[i] = v
else
expected_matchers[i] = function (other)
if expected[i].__eq(expected[i], other) then
return ''
end
return string.format('%s element %s does no match expected: %s', i, other, expected[i])
end
end
end
return function(actual)
if #actual < #expected_matchers then
return string.format('number of elements is less than expected: %s < %s', #actual, #expected_matchers)
end
local message = ''
for i, v in ipairs(actual) do
if i > #expected_matchers then
message = string.format('%s\n%s element is out of expected range: %s', message, i, #expected_matchers)
break
end
local match_message = expected_matchers[i](v)
if match_message ~= '' then
message = string.format('%s\n%s', message, match_message)
end
end
return message
end
end
---
-- Matcher verifying that given number is not a nan.
-- @function isNotNan
-- @usage
-- expectThat(value, isNotNan())
function M.isNotNan(expected)
return function(actual)
if actual ~= actual then
return 'actual value is nan, expected to be not nan'
end
return ''
end
end
---
-- Verifies that given value matches provided matcher.
-- @function expectThat
@ -182,28 +183,50 @@ function M.formatActualExpected(actual, expected)
return string.format('actual: %s, expected: %s', actual, expected)
end
local localTests = {}
local localTestRunner = nil
-- used only in menu scripts
M.menuEventHandlers = {
globalTestFinished = function(data)
if data.name ~= currentGlobalTest then
error(string.format('globalTestFinished with incorrect name %s, expected %s', data.name, currentGlobalTest), 2)
end
currentGlobalTest = nil
currentGlobalTestError = data.errMsg
end,
}
function M.registerLocalTest(name, fn)
localTests[name] = fn
-- used only in global scripts
M.globalEventHandlers = {
runGlobalTest = function(name)
fn = globalTests[name]
local types = require('openmw.types')
local world = require('openmw.world')
if not fn then
types.Player.sendMenuEvent(world.players[1], 'globalTestFinished', {name=name, errMsg='Global test is not found'})
return
end
globalTestRunner = coroutine.create(function()
local status, err = pcall(fn)
if status then
err = nil
end
types.Player.sendMenuEvent(world.players[1], 'globalTestFinished', {name=name, errMsg=err})
end)
end,
localTestFinished = function(data)
if data.name ~= currentLocalTest then
error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest), 2)
end
currentLocalTest = nil
currentLocalTestError = data.errMsg
end,
}
function M.updateLocal()
if localTestRunner and coroutine.status(localTestRunner) ~= 'dead' then
if not core.isWorldPaused() then
coroutine.resume(localTestRunner)
end
else
localTestRunner = nil
end
end
M.eventHandlers = {
runLocalTest = function(name) -- used only in local scripts
-- used only in local scripts
M.localEventHandlers = {
runLocalTest = function(name)
fn = localTests[name]
if not fn then
core.sendGlobalEvent('localTestFinished', {name=name, errMsg='Test not found'})
core.sendGlobalEvent('localTestFinished', {name=name, errMsg='Local test is not found'})
return
end
localTestRunner = coroutine.create(function()
@ -214,13 +237,6 @@ M.eventHandlers = {
core.sendGlobalEvent('localTestFinished', {name=name, errMsg=err})
end)
end,
localTestFinished = function(data) -- used only in global scripts
if data.name ~= currentLocalTest then
error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest))
end
currentLocalTest = nil
currentLocalTestError = data.errMsg
end,
}
return M

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