Compare commits

...

54 commits

Author SHA1 Message Date
David Cernat
8aad93b904
Merge pull request #519 from TES3MP/0.7.0-alpha
[General] Update positions for dead players on other clients
2019-03-24 03:54:43 +02:00
David Cernat
3effd5f1ff [General] Update positions for dead players on other clients
Dead players will now show up at the correct cell and position for living players, making server scripts that allow players to revive each other much more functional.
2019-03-24 03:52:05 +02:00
David Cernat
4692f29b9d
Merge pull request #517 from uramer/0.7.0markers
update player map markers when client changes cell
2019-03-22 21:39:06 +02:00
David Cernat
03d377ec54
Merge pull request #518 from TES3MP/0.7.0-alpha
[General] Rename CellReplace packet into CellReset
2019-03-22 21:36:07 +02:00
David Cernat
8ff2d1b829 [General] Rename CellReplace packet into CellReset 2019-03-22 21:33:34 +02:00
David Cernat
cb82318c36 [General] Fix problems with Utils::getArchitectureType() 2019-03-22 03:09:11 +02:00
uramer
3b2098382b update player map markers when client changes cell 2019-03-21 16:27:15 +01:00
David Cernat
cb5e24e6c5
Merge pull request #516 from TES3MP/0.7.0-alpha
[Server] Add GetMillisecondsSinceServerStart() server function
2019-03-20 20:05:30 +02:00
David Cernat
91f82d845c [Server] Add GetMillisecondsSinceServerStart() server function 2019-03-20 20:02:31 +02:00
David Cernat
d35026bbf5
Merge pull request #515 from TES3MP/0.7.0-alpha
0.7.0 alpha
2019-03-20 18:58:31 +02:00
David Cernat
bd677726bf [Server] Add StatsFunctions that get/set damage to attributes/skills 2019-03-20 18:54:35 +02:00
David Cernat
9fc4c83858 [Client] Send skill/attribute packets when skills/attributes are damaged 2019-03-20 18:40:46 +02:00
David Cernat
ece39748de [Server] Fix typo causing recursion in deprecated actor list function 2019-03-20 17:01:21 +02:00
David Cernat
5c4d3df551 [Server] Deprecate DoesFileExist(), add DoesFilePathExist() 2019-03-19 04:52:58 +02:00
David Cernat
2cdabddc0e [Server] Move most MiscellaneousFunctions to ServerFunctions 2019-03-19 04:25:33 +02:00
David Cernat
b46767de6e [Server] Clean up recent additions to ServerFunctions 2019-03-19 03:57:16 +02:00
David Cernat
911079e0bc
Merge pull request #512 from TES3MP/0.7.0-alpha
0.7.0 alpha
2019-03-12 05:38:36 +02:00
David Cernat
331fa86844 [Server] Call OnServerPostInit after OnRequestDataFileList
This allows different actions to be taken in OnServerPostInit based on what the data files being used are.
2019-03-12 05:36:33 +02:00
David Cernat
a0ec9dfd2e [Server] Rename OnRequestPluginList into OnRequestDataFileList 2019-03-12 03:18:57 +02:00
David Cernat
986528c67d [Server] Add error message as argument to OnServerScriptCrash 2019-03-12 02:15:20 +02:00
David Cernat
552a94a0ca [Server] Add OnServerScriptCrash script event 2019-03-10 00:46:40 +02:00
David Cernat
a508a0faf8 [Server] Turn GetArguments() from ScriptFunctions into Utils function 2019-02-24 01:43:04 +02:00
David Cernat
dcbc9d1831 [Client] Print cells for actor deaths 2019-02-21 21:51:02 +02:00
David Cernat
828c52138f [Documentation] Update readme and credits
According to some legal advice I've received, the "TES3MP Team" is too ambiguous of a legal entity, so – with Koncord's agreement – the copyright is now assigned specifically to us, the project's developers.
2019-02-19 17:29:29 +02:00
David Cernat
69e7d3f2a7 [Documentation] Update credits 2019-02-14 13:07:54 +02:00
David Cernat
f3b8a5b909 [General] Check integrity of credits only on Windows clients
This avoids the problems that were encountered in Linux and macOS builds regarding this check while also still addressing the scenario where official Windows builds had their credits modified by people unrelated to the project.
2019-02-14 00:29:55 +02:00
David Cernat
a0ad0b29bc Merge branch '0.7.0' of https://github.com/TES3MP/openmw-tes3mp into 0.7.0 2019-02-13 21:57:24 +02:00
David Cernat
222837976c [Server] Fix type name warning for Player
The warning in Visual Studio was: "'Player': type name first seen using 'class' now seen using 'struct'"
2019-02-13 21:56:47 +02:00
Koncord
77386525f2 [General] Update Travis CI 2019-02-14 01:56:10 +08:00
David Cernat
c058dce346 [General] Clarify meaning of commit hash displayed on start 2019-01-31 13:39:06 +02:00
David Cernat
1df1515c7e [Client] Add logging for invalid enchantmentIds in RecordHelper 2019-01-23 01:04:59 +02:00
David Cernat
999ce857c7 [Client] Add logging for records ignored due to their invalid baseIds 2019-01-23 00:48:06 +02:00
David Cernat
db7e09f441 [Client] Use more consistent logging when reading dynamic record packets 2019-01-23 00:38:05 +02:00
David Cernat
0fa116b47d [Client] Remove useless lines in RecordHelper 2019-01-23 00:20:51 +02:00
Koncord
0df32accca [Server] Fix ARM build 2019-01-21 12:02:02 +08:00
David Cernat
fd40e8c971 [Client] Prevent ObjectState spam by not resending an already sent state 2019-01-15 14:26:00 +02:00
David Cernat
6e47b65205 [Client] Set attribute increases & level progress after correct packets
Originally, the PlayerSkill packet contained skills, attribute increases and level progress. In 78441c769a, the attribute increases were moved to the PlayerAttribute packet and the level progress was moved to the PlayerLevel packet, but – due to an oversight – attribute increases and level progress were still being applied to the local player only when a PlayerSkill packet was received, based on whatever values were stored from the last PlayerAttribute and PlayerLevel packets.
2019-01-11 14:26:13 +02:00
David Cernat
f481c85e07 [Client] Use ADD before REMOVE for PlayerInventory in repair/recharge
Previously, when recharging or repairing an item, the client sent a PlayerInventory packet to the server with the old version of the item that was supposed to be removed and then it sent a PlayerInventory packet with the new version of the item that was supposed to be added.

Unfortunately, the current CoreScripts make it so custom items using generated IDs have their records deleted when they are completely removed from the world, however briefly, even if they are added back immediately afterwards. In practice, this meant that – before this commit – recharging or repairing a custom item led to its removal from the player inventory stored on the server, followed by the deletion of its record, followed by its readdition to the inventory (but with the record staying deleted). Logging out and logging back in immediately prevented the player from receiving the item anymore because of its now non-existent record.
2019-01-11 13:08:26 +02:00
David Cernat
8a99f215f6 [Client] Add LocalPlayer::sendItemChange() variant with mwmp::Item arg 2019-01-11 12:54:47 +02:00
David Cernat
db9c1b9882 [Client] Add MechanicsHelper::getItem() for getting mwmp::Item from Ptr 2019-01-11 12:53:26 +02:00
David Cernat
799241e8c6 [Client] Use informative error message for RefData::setCount() issue 2019-01-11 08:16:29 +02:00
David Cernat
43f195f0c7 [Client] Use clearer debug for actor initializations 2019-01-05 23:27:35 +02:00
David Cernat
2e1d4a9449 [Server] Fix non-Windows builds 2019-01-05 22:11:58 +02:00
David Cernat
81e2e48561 [Client] Fix item magic casting synchronization for spell scrolls
Previously, spell scrolls were used up before their IDs could be included in attacks packets supposed to be sent for them.
2018-12-31 13:24:32 +02:00
David Cernat
d83160523f [Client] Add items required for item magic casting when they are missing 2018-12-31 06:55:35 +02:00
David Cernat
433a69a588 [Client] Send all data for newly initialized LocalActors at least once 2018-12-31 04:36:59 +02:00
David Cernat
e70fd2cf3a [Server] Accept clients with wrong password on servers with no password 2018-12-31 03:52:25 +02:00
David Cernat
eb52babf29 [Server] Print IP instead of name or PID for players unable to connect
The player name was always blank in such situations, providing no useful information. The PID was not useful in any way either.
2018-12-30 18:02:26 +02:00
David Cernat
e96091fd6b [General] Use more consistent variable names for password, address, etc. 2018-12-30 17:23:12 +02:00
David Cernat
906d2a837d [Client] Send PlayerInventory packets when recharging items w/ soulgems 2018-12-30 11:58:33 +02:00
David Cernat
71679934a1 [Client] Send PlayerInventory packets when repairing items 2018-12-30 09:39:46 +02:00
David Cernat
5d9893ee92 [Client] Set actor killer correctly for spells that do damage over time
Additionally, clean up comments related to other code that sets actor killers.
2018-12-30 07:40:11 +02:00
David Cernat
6e1504f0a1 [Server] Use clearer variable & function names in TimerAPI 2018-12-30 04:15:53 +02:00
David Cernat
42b5a8054f [Server] Remove unusable position functions for players 2018-12-30 03:17:37 +02:00
67 changed files with 887 additions and 574 deletions

View file

@ -4,7 +4,7 @@ os:
osx_image: xcode9.4 osx_image: xcode9.4
language: cpp language: cpp
sudo: required sudo: required
dist: trusty dist: xenial
branches: branches:
only: only:
- master - master
@ -15,18 +15,18 @@ env:
global: global:
# The next declaration is the encrypted COVERITY_SCAN_TOKEN, created # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
# via the "travis encrypt" command using the project repo's public key # via the "travis encrypt" command using the project repo's public key
- secure: NZmvVuA0O9NJXVQ12tXQZHDJC2mbFgYNFcsicw0DgW1It2Nk5hxIkF0pfu4/Z59mhQuOPgRVjl5b0FKy2Axh0gkWc1DJEXGwNaiW5lpTMNWR1LJG5rxa8LrDUpFkycpbzfAFuTUZu5z3iYVv64XzELvBuqNGhPMu1LeBnrlech0jFNjkR9p5qtJGWb8zYcPMCC57rig8a9g1ABoVYS6UXjrKpx0946ZLRsE5ukc9pXsypGwPmOMyfzZkxxzIqFaxoE5JIEdaJTWba/6Za315ozYYIi/N35ROI1YAv5GHRe/Iw9XAa4vQpbDzjM7ZSsZdTvvQsSU598gD2xC6jFUKSrpW6GZKwM2x236fZLGnOk5Uw7DUbG+AwpcEmxBwoy9PjBl9ZF3tJykI0gROewCy8MODhdsVMKr1HGIMVBIJySm/RnNqtoDbYV8mYnSl5b8rwJiCajoiR8Zuv4CIfGneeH1a3DOQDPH/qkDsU6ilzF4ANsBlMUUpgY653KBMBmTlNuVZSH527tnD7Fg6JgHVuSQkTbRa1vSkR7Zcre604RZcAoaEdbX3bhVDasPPghU/I742L0RH3oQNlR09pPBDZ8kG7ydl4aPHwpCWnvXNM1vgxtGvnYLztwrse7IoaRXRYiMFmrso78WhMWUDKgvY4wV9aeUu0DtnMezZVIQwCKg= - secure: 1QK0yVyoOB+gf2I7XzvhXu9w/5lq4stBXIwJbVCTjz4Q4XVHCosURaW1MAgKzMrPnbFEwjyn5uQ8BwsvvfkuN1AZD0YXITgc7gyI+J1wQ/p/ljxRxglakU6WEgsTs2J5z9UmGac4YTXg+quK7YP3rv+zuGim2I2rhzImejyzp0Ym3kRCnNcy+SGBsiRaevRJMe00Ch8zGAbEhduQGeSoS6W0rcu02DNlQKiq5NktWsXR+TWWWVfIeIlQR/lbPsCd0pdxMaMv2QCY0rVbwrYxWJwr/Qe45dAdWp+8/C3PbXpeMSGxlLa33nJNX4Lf/djxbjm8KWk6edaXPajrjR/0iwcpwq0jg2Jt6XfEdnJt35F1gpXlc04sxStjG45uloOKCFYT0wdhIO1Lq+hDP54wypQl+JInd5qC001O7pwhVxO36EgKWqo8HD+BqGDBwsNj2engy9Qcp3wO6G0rLBPB3CrZsk9wrHVv5cSiQSLMhId3Xviu3ZI2qEDA+kgTvxrKrsnMj4bILVCyG5Ka2Mj22wIDW9e8oIab9oTdujax3DTN1GkD6QuOAGzwDsNwGASsgfoeZ+FUhgM75RlBWGMilgkmnF7EJ0oAXLEpjtABnEr2d4qHv+y08kOuTDBLB9ExzCIj024dYYYNLZrqPKx0ncHuCMG2QNj2aJAJEZtj1rQ=
addons: addons:
apt: apt:
sources: sources:
- sourceline: 'ppa:openmw/openmw' - sourceline: 'ppa:openmw/openmw'
- sourceline: 'ppa:rakhimov/boost' - sourceline: 'ppa:rakhimov/boost'
- ubuntu-toolchain-r-test - ubuntu-toolchain-r-test
- llvm-toolchain-precise-3.8
packages: [ packages: [
# Dev # Dev
cmake, clang-3.8, libunshield-dev, libtinyxml-dev, cmake, clang-6.0, libunshield-dev, libtinyxml-dev,
g++-6, g++-8,
# Tests # Tests
libgtest-dev, google-mock, libgtest-dev, google-mock,
# Boost # Boost
@ -45,7 +45,7 @@ addons:
project: project:
name: "TES3MP/openmw-tes3mp" name: "TES3MP/openmw-tes3mp"
description: "<Your project description here>" description: "<Your project description here>"
notification_email: stas5978@gmail.com notification_email: koncord@tes3mp.com
build_command_prepend: "cmake . -DBUILD_UNITTESTS=FALSE -DBUILD_OPENCS=FALSE -DBUILD_BSATOOL=FALSE -DBUILD_ESMTOOL=FALSE -DBUILD_MWINIIMPORTER=FALSE -DBUILD_LAUNCHER=FALSE" build_command_prepend: "cmake . -DBUILD_UNITTESTS=FALSE -DBUILD_OPENCS=FALSE -DBUILD_BSATOOL=FALSE -DBUILD_ESMTOOL=FALSE -DBUILD_MWINIIMPORTER=FALSE -DBUILD_LAUNCHER=FALSE"
build_command: "make -j3" build_command: "make -j3"
branch_pattern: coverity_scan branch_pattern: coverity_scan
@ -53,21 +53,21 @@ matrix:
include: include:
- os: linux - os: linux
env: env:
- ANALYZE="scan-build-3.8 --use-cc clang-3.8 --use-c++ clang++-3.8 " - ANALYZE="scan-build-6.0 --use-cc clang-6.0 --use-c++ clang++-6.0 "
- MATRIX_CC="CC=clang-3.8 && CXX=clang++-3.8" - MATRIX_CC="CC=clang-6.0 && CXX=clang++-6.0"
compiler: clang compiler: clang
- os: linux - os: linux
env: env:
- MATRIX_CC="CC=gcc-6 && CXX=g++-6" - MATRIX_CC="CC=gcc-8 && CXX=g++-8"
- os: linux - os: linux
env: env:
- MATRIX_CC="CC=clang-3.8 && CXX=clang++-3.8" - MATRIX_CC="CC=clang-6.0 && CXX=clang++-6.0"
allow_failures: allow_failures:
- env: - env:
- MATRIX_CC="CC=clang-3.8 && CXX=clang++-3.8" - MATRIX_CC="CC=clang-6.0 && CXX=clang++-6.0"
- env: - env:
- ANALYZE="scan-build-3.8 --use-cc clang-3.8 --use-c++ clang++-3.8 " - ANALYZE="scan-build-6.0 --use-cc clang-6.0 --use-c++ clang++-6.0 "
- MATRIX_CC="CC=clang-3.8 && CXX=clang++-3.8" - MATRIX_CC="CC=clang-6.0 && CXX=clang++-6.0"
before_install: before_install:
- ./CI/before_install.${TRAVIS_OS_NAME}.sh - ./CI/before_install.${TRAVIS_OS_NAME}.sh

View file

@ -15,19 +15,8 @@ sudo ln -s /usr/src/gtest/build/libgtest.so /usr/lib/libgtest.so
sudo ln -s /usr/src/gtest/build/libgtest_main.so /usr/lib/libgtest_main.so sudo ln -s /usr/src/gtest/build/libgtest_main.so /usr/lib/libgtest_main.so
cd ~/ cd ~/
git clone https://github.com/TES3MP/RakNet git clone https://github.com/TES3MP/CrabNet
cd RakNet cd CrabNet
cmake . -DRAKNET_ENABLE_DLL=OFF -DRAKNET_ENABLE_SAMPLES=OFF -DCMAKE_BUILD_TYPE=Release cmake . -DCRABNET_ENABLE_DLL=OFF -DCRABNET_ENABLE_SAMPLES=OFF -DCMAKE_BUILD_TYPE=Release
make -j3 make -j3
cd ~/
git clone https://github.com/Koncord/CallFF
cd CallFF
mkdir build
cd build
cmake ../
make -j3
cd ~/
wget https://github.com/zdevito/terra/releases/download/release-2016-03-25/terra-Linux-x86_64-332a506.zip
unzip terra-Linux-x86_64-332a506.zip

View file

@ -9,8 +9,7 @@ if [ ! -z "${MATRIX_CC}" ]; then
eval "${MATRIX_CC}" eval "${MATRIX_CC}"
fi fi
export RAKNET_ROOT=~/RakNet export RAKNET_ROOT=~/CrabNet
export Terra_ROOT=~/terra-Linux-x86_64-332a506
export CODE_COVERAGE=0 export CODE_COVERAGE=0
if [ ! -z "${ANALYZE}" ]; then if [ ! -z "${ANALYZE}" ]; then
@ -36,7 +35,5 @@ ${ANALYZE}cmake .. \
-DBINDIR=/usr/games \ -DBINDIR=/usr/games \
-DCMAKE_BUILD_TYPE="None" \ -DCMAKE_BUILD_TYPE="None" \
-DUSE_SYSTEM_TINYXML=TRUE \ -DUSE_SYSTEM_TINYXML=TRUE \
-DRakNet_LIBRARY_RELEASE=~/RakNet/lib/libRakNetLibStatic.a \ -DRakNet_LIBRARY_RELEASE=~/CrabNet/lib/libRakNetLibStatic.a \
-DRakNet_LIBRARY_DEBUG=~/RakNet/lib/libRakNetLibStatic.a \ -DRakNet_LIBRARY_DEBUG=~/CrabNet/lib/libRakNetLibStatic.a
-DCallFF_INCLUDES=~/CallFF/include \
-DCallFF_LIBRARY=~/CallFF/build/src/libcallff.a

View file

@ -2,7 +2,7 @@ TES3MP
====== ======
Copyright (c) 2008-2015, OpenMW Team Copyright (c) 2008-2015, OpenMW Team
Copyright (c) 2016-2018, TES3MP Team Copyright (c) 2016-2019, Stanislav Zhukov & David Cernat
[![Build Status](https://travis-ci.org/TES3MP/openmw-tes3mp.svg?branch=0.7.0)](https://travis-ci.org/TES3MP/openmw-tes3mp) [![Build Status](https://travis-ci.org/TES3MP/openmw-tes3mp.svg?branch=0.7.0)](https://travis-ci.org/TES3MP/openmw-tes3mp)
@ -15,7 +15,7 @@ TES3MP is a project adding multiplayer functionality to [OpenMW](https://github.
Font Licenses: Font Licenses:
* DejaVuLGCSansMono.ttf: custom (see [files/mygui/DejaVu Font License.txt](https://github.com/TES3MP/openmw-tes3mp/blob/master/files/mygui/DejaVu%20Font%20License.txt) for more information) * DejaVuLGCSansMono.ttf: custom (see [files/mygui/DejaVu Font License.txt](https://github.com/TES3MP/openmw-tes3mp/blob/master/files/mygui/DejaVu%20Font%20License.txt) for more information)
Project Status Project status
-------------- --------------
[Version changelog](https://github.com/TES3MP/openmw-tes3mp/blob/master/tes3mp-changelog.md) [Version changelog](https://github.com/TES3MP/openmw-tes3mp/blob/master/tes3mp-changelog.md)
@ -24,25 +24,26 @@ As of version 0.7.0, TES3MP is fully playable, providing very extensive player,
Remaining gameplay problems mostly relate to AI and the synchronization of clientside script variables. Remaining gameplay problems mostly relate to AI and the synchronization of clientside script variables.
Donations
---------------
You can benefit the project by donating on Patreon to our two developers, [David Cernat](https://www.patreon.com/davidcernat) and [Koncord](https://www.patreon.com/Koncord), as well as by supporting [OpenMW](https://openmw.org).
Contributing Contributing
-------------- ---------------
Development has been relatively fast, but any contribution regarding [code](https://github.com/TES3MP/openmw-tes3mp/blob/master/CONTRIBUTING.md), documentation, bug hunting or video showcases is greatly appreciated. Helping us with documentation, bug hunting and video showcases is always greatly appreciated.
Test sessions are often advertised on [our Discord server](https://discord.gg/ECJk293) or in [our Steam group](https://steamcommunity.com/groups/mwmulti). For code contributions, it's best to start out with modestly sized fixes and features and work your way up. There are so many different possible implementations of more major features many of which would cause undesirable code or vision conflicts with OpenMW that those should be talked over in advance with the existing developers before effort is spent on them.
Feel free to contact the [team members](https://github.com/TES3MP/openmw-tes3mp/blob/master/tes3mp-credits.md) for any questions you might have. Feel free to contact the [team members](https://github.com/TES3MP/openmw-tes3mp/blob/master/tes3mp-credits.md) for any questions you might have.
Getting Started Getting started
--------------- ---------------
* [Quickstart guide](https://github.com/TES3MP/openmw-tes3mp/wiki/Quickstart-guide) * [Quickstart guide](https://github.com/TES3MP/openmw-tes3mp/wiki/Quickstart-guide)
* [Steam group](https://steamcommunity.com/groups/mwmulti) and its [detailed FAQ](https://steamcommunity.com/groups/mwmulti/discussions/1/353916184342480541/) * [Steam group](https://steamcommunity.com/groups/mwmulti) and its [detailed FAQ](https://steamcommunity.com/groups/mwmulti/discussions/1/353916184342480541/)
* [TES3MP section on OpenMW forums](https://forum.openmw.org/viewforum.php?f=45) * [TES3MP section on OpenMW forums](https://forum.openmw.org/viewforum.php?f=45)
* [Discord server](https://discord.gg/ECJk293)
* [Subreddit](https://www.reddit.com/r/tes3mp) * [Subreddit](https://www.reddit.com/r/tes3mp)
* [Known issues and bug reports](https://github.com/TES3MP/openmw-tes3mp/issues) * [Known issues and bug reports](https://github.com/TES3MP/openmw-tes3mp/issues)
Donations
---------------
You can benefit the project by contributing to the Patreon pages of our two developers, [David Cernat](https://www.patreon.com/davidcernat) and [Koncord](https://www.patreon.com/Koncord), as well as by supporting [OpenMW](https://openmw.org).

View file

@ -75,9 +75,9 @@ Networking::~Networking()
delete worldstatePacketController; delete worldstatePacketController;
} }
void Networking::setServerPassword(std::string passw) noexcept void Networking::setServerPassword(std::string password) noexcept
{ {
serverPassword = passw.empty() ? TES3MP_DEFAULT_PASSW : passw; serverPassword = password.empty() ? TES3MP_DEFAULT_PASSW : password;
} }
bool Networking::isPassworded() const bool Networking::isPassworded() const
@ -98,25 +98,32 @@ void Networking::processPlayerPacket(RakNet::Packet *packet)
if (!myPacket->isPacketValid()) if (!myPacket->isPacketValid())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, "Invalid handshake packet from %d", player->getId()); LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, "Invalid handshake packet from client at %s", packet->systemAddress.ToString());
kickPlayer(player->guid); kickPlayer(player->guid);
return; return;
} }
if (player->isHandshaked()) if (player->isHandshaked())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_WARN, "Wrong handshake with player %d, name: %s", player->getId(), LOG_MESSAGE_SIMPLE(Log::LOG_WARN, "Wrong handshake with client at %s", packet->systemAddress.ToString());
player->npc.mName.c_str());
kickPlayer(player->guid); kickPlayer(player->guid);
return; return;
} }
if (player->passw != serverPassword) if (player->serverPassword != serverPassword)
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_WARN, "Wrong server password for player %d, name: %s (pass: %s)", if (isPassworded())
player->getId(), player->npc.mName.c_str(), player->passw.c_str()); {
kickPlayer(player->guid); LOG_MESSAGE_SIMPLE(Log::LOG_WARN, "Wrong server password %s used by client at %s",
return; player->serverPassword.c_str(), packet->systemAddress.ToString());
kickPlayer(player->guid);
return;
}
else
{
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Client at %s tried to join using password, despite the server not being passworded",
packet->systemAddress.ToString());
}
} }
player->setHandshake(); player->setHandshake();
return; return;
@ -125,9 +132,8 @@ void Networking::processPlayerPacket(RakNet::Packet *packet)
if (!player->isHandshaked()) if (!player->isHandshaked())
{ {
player->incrementHandshakeAttempts(); player->incrementHandshakeAttempts();
LOG_MESSAGE_SIMPLE(Log::LOG_WARN, "Have not completed handshake with player %d", player->getId()); LOG_MESSAGE_SIMPLE(Log::LOG_WARN, "Have not completed handshake with client at %s", packet->systemAddress.ToString());
LOG_APPEND(Log::LOG_WARN, "- Attempts so far: %i", player->getHandshakeAttempts());
LOG_MESSAGE_SIMPLE(Log::LOG_WARN, "Attempts so far: %i", player->getHandshakeAttempts());
if (player->getHandshakeAttempts() > 20) if (player->getHandshakeAttempts() > 20)
kickPlayer(player->guid, false); kickPlayer(player->guid, false);
@ -579,8 +585,8 @@ void Networking::InitQuery(std::string queryAddr, unsigned short queryPort)
void Networking::postInit() void Networking::postInit()
{ {
Script::Call<Script::CallbackIdentity("OnRequestDataFileList")>();
Script::Call<Script::CallbackIdentity("OnServerPostInit")>(); Script::Call<Script::CallbackIdentity("OnServerPostInit")>();
Script::Call<Script::CallbackIdentity("OnRequestPluginList")>();
} }
PacketPreInit::PluginContainer &Networking::getSamples() PacketPreInit::PluginContainer &Networking::getSamples()

View file

@ -21,7 +21,6 @@
#include "Cell.hpp" #include "Cell.hpp"
#include "CellController.hpp" #include "CellController.hpp"
struct Player;
typedef std::map<RakNet::RakNetGUID, Player*> TPlayers; typedef std::map<RakNet::RakNetGUID, Player*> TPlayers;
typedef std::map<unsigned short, Player*> TSlots; typedef std::map<unsigned short, Player*> TSlots;

View file

@ -1,7 +1,3 @@
//
// Created by koncord on 15.03.16.
//
#include "TimerAPI.hpp" #include "TimerAPI.hpp"
#include <chrono> #include <chrono>
@ -14,7 +10,7 @@ Timer::Timer(ScriptFunc callback, long msec, const std::string& def, std::vector
{ {
targetMsec = msec; targetMsec = msec;
this->args = args; this->args = args;
end = true; isEnded = true;
} }
#if defined(ENABLE_LUA) #if defined(ENABLE_LUA)
@ -22,13 +18,13 @@ Timer::Timer(lua_State *lua, ScriptFuncLua callback, long msec, const std::strin
{ {
targetMsec = msec; targetMsec = msec;
this->args = args; this->args = args;
end = true; isEnded = true;
} }
#endif #endif
void Timer::Tick() void Timer::Tick()
{ {
if (end) if (isEnded)
return; return;
const auto duration = chrono::system_clock::now().time_since_epoch(); const auto duration = chrono::system_clock::now().time_since_epoch();
@ -36,19 +32,19 @@ void Timer::Tick()
if (time - startTime >= targetMsec) if (time - startTime >= targetMsec)
{ {
end = true; isEnded = true;
Call(args); Call(args);
} }
} }
bool Timer::IsEnd() bool Timer::IsEnded()
{ {
return end; return isEnded;
} }
void Timer::Stop() void Timer::Stop()
{ {
end = true; isEnded = true;
} }
void Timer::Restart(int msec) void Timer::Restart(int msec)
@ -59,7 +55,7 @@ void Timer::Restart(int msec)
void Timer::Start() void Timer::Start()
{ {
end = false; isEnded = false;
const auto duration = chrono::system_clock::now().time_since_epoch(); const auto duration = chrono::system_clock::now().time_since_epoch();
const auto msec = chrono::duration_cast<chrono::milliseconds>(duration).count(); const auto msec = chrono::duration_cast<chrono::milliseconds>(duration).count();
@ -172,12 +168,12 @@ void TimerAPI::StopTimer(int timerid)
} }
} }
bool TimerAPI::IsEndTimer(int timerid) bool TimerAPI::IsTimerElapsed(int timerid)
{ {
bool ret = false; bool ret = false;
try try
{ {
ret = timers.at(timerid)->IsEnd(); ret = timers.at(timerid)->IsEnded();
} }
catch(...) catch(...)
{ {

View file

@ -1,7 +1,3 @@
//
// Created by koncord on 15.03.16.
//
#ifndef OPENMW_TIMERAPI_HPP #ifndef OPENMW_TIMERAPI_HPP
#define OPENMW_TIMERAPI_HPP #define OPENMW_TIMERAPI_HPP
@ -27,7 +23,7 @@ namespace mwmp
#endif #endif
void Tick(); void Tick();
bool IsEnd(); bool IsEnded();
void Stop(); void Stop();
void Start(); void Start();
void Restart(int msec); void Restart(int msec);
@ -36,7 +32,7 @@ namespace mwmp
std::string publ, arg_types; std::string publ, arg_types;
std::vector<boost::any> args; std::vector<boost::any> args;
Script *scr; Script *scr;
bool end; bool isEnded;
}; };
class TimerAPI class TimerAPI
@ -50,7 +46,7 @@ namespace mwmp
static void ResetTimer(int timerid, long msec); static void ResetTimer(int timerid, long msec);
static void StartTimer(int timerid); static void StartTimer(int timerid);
static void StopTimer(int timerid); static void StopTimer(int timerid);
static bool IsEndTimer(int timerid); static bool IsTimerElapsed(int timerid);
static void Terminate(); static void Terminate();

View file

@ -537,7 +537,7 @@ void ActorFunctions::InitializeActorList(unsigned short pid) noexcept
void ActorFunctions::CopyLastActorListToStore() noexcept void ActorFunctions::CopyLastActorListToStore() noexcept
{ {
CopyLastActorListToStore(); CopyReceivedActorListToStore();
} }
unsigned int ActorFunctions::GetActorRefNumIndex(unsigned int index) noexcept unsigned int ActorFunctions::GetActorRefNumIndex(unsigned int index) noexcept

View file

@ -1,38 +1,12 @@
#include "Miscellaneous.hpp" #include "Miscellaneous.hpp"
#include <components/misc/stringops.hpp>
#include <components/openmw-mp/Log.hpp> #include <components/openmw-mp/Log.hpp>
#include <apps/openmw-mp/Script/ScriptFunctions.hpp>
#include <apps/openmw-mp/Networking.hpp> #include <apps/openmw-mp/Networking.hpp>
#include <iostream> #include <iostream>
using namespace std; using namespace std;
static std::string tempFilename;
bool MiscellaneousFunctions::DoesFileExist(const char *filePath) noexcept
{
return boost::filesystem::exists(filePath);
}
const char *MiscellaneousFunctions::GetCaseInsensitiveFilename(const char *folderPath, const char *filename) noexcept
{
if (!boost::filesystem::exists(folderPath)) return "invalid";
boost::filesystem::directory_iterator end_itr; // default construction yields past-the-end
for (boost::filesystem::directory_iterator itr(folderPath); itr != end_itr; ++itr)
{
if (Misc::StringUtils::ciEqual(itr->path().filename().string(), filename))
{
tempFilename = itr->path().filename().string();
return tempFilename.c_str();
}
}
return "invalid";
}
unsigned int MiscellaneousFunctions::GetLastPlayerId() noexcept unsigned int MiscellaneousFunctions::GetLastPlayerId() noexcept
{ {
return Players::getLastPlayerId(); return Players::getLastPlayerId();
@ -47,13 +21,3 @@ void MiscellaneousFunctions::SetCurrentMpNum(int mpNum) noexcept
{ {
mwmp::Networking::getPtr()->setCurrentMpNum(mpNum); mwmp::Networking::getPtr()->setCurrentMpNum(mpNum);
} }
void MiscellaneousFunctions::LogMessage(unsigned short level, const char *message) noexcept
{
LOG_MESSAGE_SIMPLE(level, "[Script]: %s", message);
}
void MiscellaneousFunctions::LogAppend(unsigned short level, const char *message) noexcept
{
LOG_APPEND(level, "[Script]: %s", message);
}

View file

@ -4,42 +4,15 @@
#include "../Types.hpp" #include "../Types.hpp"
#define MISCELLANEOUSAPI \ #define MISCELLANEOUSAPI \
{"DoesFileExist", MiscellaneousFunctions::DoesFileExist},\
{"GetCaseInsensitiveFilename", MiscellaneousFunctions::GetCaseInsensitiveFilename},\
\
{"GetLastPlayerId", MiscellaneousFunctions::GetLastPlayerId},\ {"GetLastPlayerId", MiscellaneousFunctions::GetLastPlayerId},\
\ \
{"GetCurrentMpNum", MiscellaneousFunctions::GetCurrentMpNum},\ {"GetCurrentMpNum", MiscellaneousFunctions::GetCurrentMpNum},\
{"SetCurrentMpNum", MiscellaneousFunctions::SetCurrentMpNum},\ {"SetCurrentMpNum", MiscellaneousFunctions::SetCurrentMpNum}
\
{"LogMessage", MiscellaneousFunctions::LogMessage},\
{"LogAppend", MiscellaneousFunctions::LogAppend}
class MiscellaneousFunctions class MiscellaneousFunctions
{ {
public: public:
/**
* \brief Check whether a certain file exists.
*
* This will be a case sensitive check on case sensitive filesystems.
*
* Whenever you want to enforce case insensitivity, use GetCaseInsensitiveFilename() instead.
*
* \return Whether the file exists or not.
*/
static bool DoesFileExist(const char *filePath) noexcept;
/**
* \brief Get the first filename in a folder that has a case insensitive match with the filename
* argument.
*
* This is used to retain case insensitivity when opening data files on Linux.
*
* \return The filename that matches.
*/
static const char *GetCaseInsensitiveFilename(const char *folderPath, const char *filename) noexcept;
/** /**
* \brief Get the last player ID currently connected to the server. * \brief Get the last player ID currently connected to the server.
* *
@ -75,30 +48,6 @@ public:
* \return void * \return void
*/ */
static void SetCurrentMpNum(int mpNum) noexcept; static void SetCurrentMpNum(int mpNum) noexcept;
/**
* \brief Write a log message with its own timestamp.
*
* It will have "[Script]:" prepended to it so as to mark it as a script-generated log message.
*
* \param level The logging level used (0 for LOG_VERBOSE, 1 for LOG_INFO, 2 for LOG_WARN,
* 3 for LOG_ERROR, 4 for LOG_FATAL).
* \param message The message logged.
* \return void
*/
static void LogMessage(unsigned short level, const char *message) noexcept;
/**
* \brief Write a log message without its own timestamp.
*
* It will have "[Script]:" prepended to it so as to mark it as a script-generated log message.
*
* \param level The logging level used (0 for LOG_VERBOSE, 1 for LOG_INFO, 2 for LOG_WARN,
* 3 for LOG_ERROR, 4 for LOG_FATAL).
* \param message The message logged.
* \return void
*/
static void LogAppend(unsigned short level, const char *message) noexcept;
}; };
#endif //OPENMW_MISCELLANEOUSAPI_HPP #endif //OPENMW_MISCELLANEOUSAPI_HPP

View file

@ -7,20 +7,6 @@
#include <iostream> #include <iostream>
using namespace std; using namespace std;
void PositionFunctions::GetPos(unsigned short pid, float *x, float *y, float *z) noexcept
{
*x = 0.00;
*y = 0.00;
*z = 0.00;
Player *player;
GET_PLAYER(pid, player,);
*x = player->position.pos[0];
*y = player->position.pos[1];
*z = player->position.pos[2];
}
double PositionFunctions::GetPosX(unsigned short pid) noexcept double PositionFunctions::GetPosX(unsigned short pid) noexcept
{ {
Player *player; Player *player;
@ -69,20 +55,6 @@ double PositionFunctions::GetPreviousCellPosZ(unsigned short pid) noexcept
return player->previousCellPosition.pos[2]; return player->previousCellPosition.pos[2];
} }
void PositionFunctions::GetRot(unsigned short pid, float *x, float *y, float *z) noexcept
{
*x = 0.00;
*y = 0.00;
*z = 0.00;
Player *player;
GET_PLAYER(pid, player, );
*x = player->position.rot[0];
*y = player->position.rot[1];
*z = player->position.rot[2];
}
double PositionFunctions::GetRotX(unsigned short pid) noexcept double PositionFunctions::GetRotX(unsigned short pid) noexcept
{ {
Player *player; Player *player;

View file

@ -4,7 +4,6 @@
#include "../Types.hpp" #include "../Types.hpp"
#define POSITIONAPI \ #define POSITIONAPI \
{"GetPos", PositionFunctions::GetPos},\
{"GetPosX", PositionFunctions::GetPosX},\ {"GetPosX", PositionFunctions::GetPosX},\
{"GetPosY", PositionFunctions::GetPosY},\ {"GetPosY", PositionFunctions::GetPosY},\
{"GetPosZ", PositionFunctions::GetPosZ},\ {"GetPosZ", PositionFunctions::GetPosZ},\
@ -13,7 +12,6 @@
{"GetPreviousCellPosY", PositionFunctions::GetPreviousCellPosY},\ {"GetPreviousCellPosY", PositionFunctions::GetPreviousCellPosY},\
{"GetPreviousCellPosZ", PositionFunctions::GetPreviousCellPosZ},\ {"GetPreviousCellPosZ", PositionFunctions::GetPreviousCellPosZ},\
\ \
{"GetRot", PositionFunctions::GetRot},\
{"GetRotX", PositionFunctions::GetRotX},\ {"GetRotX", PositionFunctions::GetRotX},\
{"GetRotZ", PositionFunctions::GetRotZ},\ {"GetRotZ", PositionFunctions::GetRotZ},\
\ \
@ -29,18 +27,6 @@ class PositionFunctions
{ {
public: public:
/**
* \brief Assign the player's positional coordinate values to the variables passed as
* parameters.
*
* \param pid The player ID.
* \param x The variable for the X position.
* \param y The variable for the Y position.
* \param z The variable for the Z position.
* \return void
*/
static void GetPos(unsigned short pid, float *x, float *y, float *z) noexcept;
/** /**
* \brief Get the X position of a player. * \brief Get the X position of a player.
* *
@ -89,18 +75,6 @@ public:
*/ */
static double GetPreviousCellPosZ(unsigned short pid) noexcept; static double GetPreviousCellPosZ(unsigned short pid) noexcept;
/**
* \brief Assign the player's rotational coordinate values to the variables passed as
* parameters.
*
* \param pid The player ID.
* \param x The variable for the X rotation.
* \param y The variable for the Y rotation.
* \param z The variable for the Z rotation.
* \return void
*/
static void GetRot(unsigned short pid, float *x, float *y, float *z) noexcept;
/** /**
* \brief Get the X rotation of a player. * \brief Get the X rotation of a player.
* *

View file

@ -1,5 +1,6 @@
#include "Server.hpp" #include "Server.hpp"
#include <components/misc/stringops.hpp>
#include <components/openmw-mp/NetworkMessages.hpp> #include <components/openmw-mp/NetworkMessages.hpp>
#include <components/openmw-mp/Log.hpp> #include <components/openmw-mp/Log.hpp>
#include <components/openmw-mp/Version.hpp> #include <components/openmw-mp/Version.hpp>
@ -9,6 +10,18 @@
#include <apps/openmw-mp/MasterClient.hpp> #include <apps/openmw-mp/MasterClient.hpp>
#include <Script/Script.hpp> #include <Script/Script.hpp>
static std::string tempFilename;
static std::chrono::high_resolution_clock::time_point startupTime = std::chrono::high_resolution_clock::now();
void ServerFunctions::LogMessage(unsigned short level, const char *message) noexcept
{
LOG_MESSAGE_SIMPLE(level, "[Script]: %s", message);
}
void ServerFunctions::LogAppend(unsigned short level, const char *message) noexcept
{
LOG_APPEND(level, "[Script]: %s", message);
}
void ServerFunctions::StopServer(int code) noexcept void ServerFunctions::StopServer(int code) noexcept
{ {
@ -35,6 +48,40 @@ void ServerFunctions::UnbanAddress(const char *ipAddress) noexcept
mwmp::Networking::getPtr()->unbanAddress(ipAddress); mwmp::Networking::getPtr()->unbanAddress(ipAddress);
} }
bool ServerFunctions::DoesFilePathExist(const char *filePath) noexcept
{
return boost::filesystem::exists(filePath);
}
const char *ServerFunctions::GetCaseInsensitiveFilename(const char *folderPath, const char *filename) noexcept
{
if (!boost::filesystem::exists(folderPath)) return "invalid";
boost::filesystem::directory_iterator end_itr; // default construction yields past-the-end
for (boost::filesystem::directory_iterator itr(folderPath); itr != end_itr; ++itr)
{
if (Misc::StringUtils::ciEqual(itr->path().filename().string(), filename))
{
tempFilename = itr->path().filename().string();
return tempFilename.c_str();
}
}
return "invalid";
}
const char* ServerFunctions::GetDataPath() noexcept
{
return Script::GetModDir();
}
unsigned int ServerFunctions::GetMillisecondsSinceServerStart() noexcept
{
std::chrono::high_resolution_clock::time_point currentTime = std::chrono::high_resolution_clock::now();
std::chrono::milliseconds milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - startupTime);
return milliseconds.count();
}
const char *ServerFunctions::GetOperatingSystemType() noexcept const char *ServerFunctions::GetOperatingSystemType() noexcept
{ {
return Utils::getOperatingSystemType().c_str(); return Utils::getOperatingSystemType().c_str();
@ -137,34 +184,46 @@ void ServerFunctions::SetRuleValue(const char *key, double value) noexcept
mc->SetRuleValue(key, value); mc->SetRuleValue(key, value);
} }
void ServerFunctions::AddPluginHash(const char *pluginName, const char *hashStr) noexcept void ServerFunctions::AddDataFileRequirement(const char *dataFilename, const char *checksumString) noexcept
{ {
auto &samples = mwmp::Networking::getPtr()->getSamples(); auto &samples = mwmp::Networking::getPtr()->getSamples();
auto it = std::find_if(samples.begin(), samples.end(), [&pluginName](mwmp::PacketPreInit::PluginPair &item) { auto it = std::find_if(samples.begin(), samples.end(), [&dataFilename](mwmp::PacketPreInit::PluginPair &item) {
return item.first == pluginName; return item.first == dataFilename;
}); });
if (it != samples.end()) if (it != samples.end())
it->second.push_back((unsigned) std::stoul(hashStr)); it->second.push_back((unsigned) std::stoul(checksumString));
else else
{ {
mwmp::PacketPreInit::HashList hashList; mwmp::PacketPreInit::HashList checksumList;
unsigned hash = 0; unsigned checksum = 0;
if (strlen(hashStr) != 0) if (strlen(checksumString) != 0)
{ {
hash = (unsigned) std::stoul(hashStr); checksum = (unsigned) std::stoul(checksumString);
hashList.push_back(hash); checksumList.push_back(checksum);
} }
samples.emplace_back(pluginName, hashList); samples.emplace_back(dataFilename, checksumList);
auto mclient = mwmp::Networking::getPtr()->getMasterClient(); auto mclient = mwmp::Networking::getPtr()->getMasterClient();
if (mclient) if (mclient)
mclient->PushPlugin({pluginName, hash}); mclient->PushPlugin({dataFilename, checksum});
} }
} }
// All methods below are deprecated versions of methods from above
bool ServerFunctions::DoesFileExist(const char *filePath) noexcept
{
return DoesFilePathExist(filePath);
}
const char* ServerFunctions::GetModDir() noexcept const char* ServerFunctions::GetModDir() noexcept
{ {
return Script::GetModDir(); return GetDataPath();
}
void ServerFunctions::AddPluginHash(const char *pluginName, const char *checksumString) noexcept
{
AddDataFileRequirement(pluginName, checksumString);
} }

View file

@ -4,38 +4,73 @@
#include "../Types.hpp" #include "../Types.hpp"
#define SERVERAPI \ #define SERVERAPI \
{"StopServer", ServerFunctions::StopServer},\ {"LogMessage", ServerFunctions::LogMessage},\
{"LogAppend", ServerFunctions::LogAppend},\
\ \
{"Kick", ServerFunctions::Kick},\ {"StopServer", ServerFunctions::StopServer},\
{"BanAddress", ServerFunctions::BanAddress},\
{"UnbanAddress", ServerFunctions::UnbanAddress},\
\ \
{"GetOperatingSystemType", ServerFunctions::GetOperatingSystemType},\ {"Kick", ServerFunctions::Kick},\
{"GetArchitectureType", ServerFunctions::GetArchitectureType},\ {"BanAddress", ServerFunctions::BanAddress},\
{"GetServerVersion", ServerFunctions::GetServerVersion},\ {"UnbanAddress", ServerFunctions::UnbanAddress},\
{"GetProtocolVersion", ServerFunctions::GetProtocolVersion},\
{"GetAvgPing", ServerFunctions::GetAvgPing},\
{"GetIP", ServerFunctions::GetIP},\
{"GetMaxPlayers", ServerFunctions::GetMaxPlayers},\
{"GetPort", ServerFunctions::GetPort},\
{"HasPassword", ServerFunctions::HasPassword},\
{"GetPluginEnforcementState", ServerFunctions::GetPluginEnforcementState},\
{"GetScriptErrorIgnoringState", ServerFunctions::GetScriptErrorIgnoringState},\
\ \
{"SetGameMode", ServerFunctions::SetGameMode},\ {"DoesFilePathExist", ServerFunctions::DoesFilePathExist},\
{"SetHostname", ServerFunctions::SetHostname},\ {"GetCaseInsensitiveFilename", ServerFunctions::GetCaseInsensitiveFilename},\
{"SetServerPassword", ServerFunctions::SetServerPassword},\ {"GetDataPath", ServerFunctions::GetDataPath},\
{"SetPluginEnforcementState", ServerFunctions::SetPluginEnforcementState},\ {"GetMillisecondsSinceServerStart", ServerFunctions::GetMillisecondsSinceServerStart},\
{"SetScriptErrorIgnoringState", ServerFunctions::SetScriptErrorIgnoringState},\ {"GetOperatingSystemType", ServerFunctions::GetOperatingSystemType},\
{"SetRuleString", ServerFunctions::SetRuleString},\ {"GetArchitectureType", ServerFunctions::GetArchitectureType},\
{"SetRuleValue", ServerFunctions::SetRuleValue},\ {"GetServerVersion", ServerFunctions::GetServerVersion},\
{"AddPluginHash", ServerFunctions::AddPluginHash},\ {"GetProtocolVersion", ServerFunctions::GetProtocolVersion},\
{"GetModDir", ServerFunctions::GetModDir} {"GetAvgPing", ServerFunctions::GetAvgPing},\
{"GetIP", ServerFunctions::GetIP},\
{"GetMaxPlayers", ServerFunctions::GetMaxPlayers},\
{"GetPort", ServerFunctions::GetPort},\
{"HasPassword", ServerFunctions::HasPassword},\
{"GetPluginEnforcementState", ServerFunctions::GetPluginEnforcementState},\
{"GetScriptErrorIgnoringState", ServerFunctions::GetScriptErrorIgnoringState},\
\
{"SetGameMode", ServerFunctions::SetGameMode},\
{"SetHostname", ServerFunctions::SetHostname},\
{"SetServerPassword", ServerFunctions::SetServerPassword},\
{"SetPluginEnforcementState", ServerFunctions::SetPluginEnforcementState},\
{"SetScriptErrorIgnoringState", ServerFunctions::SetScriptErrorIgnoringState},\
{"SetRuleString", ServerFunctions::SetRuleString},\
{"SetRuleValue", ServerFunctions::SetRuleValue},\
\
{"AddDataFileRequirement", ServerFunctions::AddDataFileRequirement},\
\
{"DoesFileExist", ServerFunctions::DoesFileExist},\
{"GetModDir", ServerFunctions::GetModDir},\
{"AddPluginHash", ServerFunctions::AddPluginHash}
class ServerFunctions class ServerFunctions
{ {
public: public:
/**
* \brief Write a log message with its own timestamp.
*
* It will have "[Script]:" prepended to it so as to mark it as a script-generated log message.
*
* \param level The logging level used (0 for LOG_VERBOSE, 1 for LOG_INFO, 2 for LOG_WARN,
* 3 for LOG_ERROR, 4 for LOG_FATAL).
* \param message The message logged.
* \return void
*/
static void LogMessage(unsigned short level, const char *message) noexcept;
/**
* \brief Write a log message without its own timestamp.
*
* It will have "[Script]:" prepended to it so as to mark it as a script-generated log message.
*
* \param level The logging level used (0 for LOG_VERBOSE, 1 for LOG_INFO, 2 for LOG_WARN,
* 3 for LOG_ERROR, 4 for LOG_FATAL).
* \param message The message logged.
* \return void
*/
static void LogAppend(unsigned short level, const char *message) noexcept;
/** /**
* \brief Shut down the server. * \brief Shut down the server.
* *
@ -68,6 +103,41 @@ public:
*/ */
static void UnbanAddress(const char *ipAddress) noexcept; static void UnbanAddress(const char *ipAddress) noexcept;
/**
* \brief Check whether a certain file path exists.
*
* This will be a case sensitive check on case sensitive filesystems.
*
* Whenever you want to enforce case insensitivity, use GetCaseInsensitiveFilename() instead.
*
* \return Whether the file exists or not.
*/
static bool DoesFilePathExist(const char *filePath) noexcept;
/**
* \brief Get the first filename in a folder that has a case insensitive match with the filename
* argument.
*
* This is used to retain case insensitivity when opening data files on Linux.
*
* \return The filename that matches.
*/
static const char *GetCaseInsensitiveFilename(const char *folderPath, const char *filename) noexcept;
/**
* \brief Get the path of the server's data folder.
*
* \return The data path.
*/
static const char *GetDataPath() noexcept;
/**
* \brief Get the milliseconds elapsed since the server was started.
*
* \return The time since the server's startup in milliseconds.
*/
static unsigned int GetMillisecondsSinceServerStart() noexcept;
/** /**
* \brief Get the type of the operating system used by the server. * \brief Get the type of the operating system used by the server.
* *
@ -119,7 +189,7 @@ public:
/** /**
* \brief Get the port used by the server. * \brief Get the port used by the server.
* *
* \return Port * \return The port.
*/ */
static unsigned short GetPort() noexcept; static unsigned short GetPort() noexcept;
@ -220,13 +290,24 @@ public:
static void SetRuleValue(const char *key, double value) noexcept; static void SetRuleValue(const char *key, double value) noexcept;
/** /**
* \brief Adds plugins to the internal server structure to validate players. * \brief Add a data file and a corresponding CRC32 checksum to the data file loadout
* @param pluginName Name with extension of the plugin or master file. * that connecting clients need to match.
* @param hash Hash string *
* It can be used multiple times to set multiple checksums for the same data file.
*
* Note: If an empty string is provided for the checksum, a checksum will not be
* required for that data file.
*
* @param dataFilename The filename of the data file.
* @param checksumString A string with the CRC32 checksum required.
*/ */
static void AddPluginHash(const char *pluginName, const char *hash) noexcept; static void AddDataFileRequirement(const char *dataFilename, const char *checksumString) noexcept;
// All methods below are deprecated versions of methods from above
static bool DoesFileExist(const char *filePath) noexcept;
static const char *GetModDir() noexcept; static const char *GetModDir() noexcept;
static void AddPluginHash(const char *pluginName, const char *checksumString) noexcept;
}; };
#endif //OPENMW_SERVERAPI_HPP #endif //OPENMW_SERVERAPI_HPP

View file

@ -201,6 +201,17 @@ int StatsFunctions::GetAttributeModifier(unsigned short pid, unsigned short attr
return player->creatureStats.mAttributes[attributeId].mMod; return player->creatureStats.mAttributes[attributeId].mMod;
} }
double StatsFunctions::GetAttributeDamage(unsigned short pid, unsigned short attributeId) noexcept
{
Player *player;
GET_PLAYER(pid, player, 0);
if (attributeId >= Attribute::Length)
return 0;
return player->creatureStats.mAttributes[attributeId].mDamage;
}
int StatsFunctions::GetSkillBase(unsigned short pid, unsigned short skillId) noexcept int StatsFunctions::GetSkillBase(unsigned short pid, unsigned short skillId) noexcept
{ {
Player *player; Player *player;
@ -223,6 +234,17 @@ int StatsFunctions::GetSkillModifier(unsigned short pid, unsigned short skillId)
return player->npcStats.mSkills[skillId].mMod; return player->npcStats.mSkills[skillId].mMod;
} }
double StatsFunctions::GetSkillDamage(unsigned short pid, unsigned short skillId) noexcept
{
Player *player;
GET_PLAYER(pid, player, 0);
if (skillId >= Skill::Length)
return 0;
return player->npcStats.mSkills[skillId].mDamage;
}
double StatsFunctions::GetSkillProgress(unsigned short pid, unsigned short skillId) noexcept double StatsFunctions::GetSkillProgress(unsigned short pid, unsigned short skillId) noexcept
{ {
Player *player; Player *player;
@ -437,6 +459,20 @@ void StatsFunctions::ClearAttributeModifier(unsigned short pid, unsigned short a
player->attributeIndexChanges.push_back(attributeId); player->attributeIndexChanges.push_back(attributeId);
} }
void StatsFunctions::SetAttributeDamage(unsigned short pid, unsigned short attributeId, double value) noexcept
{
Player *player;
GET_PLAYER(pid, player, );
if (attributeId >= Attribute::Length)
return;
player->creatureStats.mAttributes[attributeId].mDamage = value;
if (!Utils::vectorContains(player->attributeIndexChanges, attributeId))
player->attributeIndexChanges.push_back(attributeId);
}
void StatsFunctions::SetSkillBase(unsigned short pid, unsigned short skillId, int value) noexcept void StatsFunctions::SetSkillBase(unsigned short pid, unsigned short skillId, int value) noexcept
{ {
Player *player; Player *player;
@ -465,6 +501,20 @@ void StatsFunctions::ClearSkillModifier(unsigned short pid, unsigned short skill
player->skillIndexChanges.push_back(skillId); player->skillIndexChanges.push_back(skillId);
} }
void StatsFunctions::SetSkillDamage(unsigned short pid, unsigned short skillId, double value) noexcept
{
Player *player;
GET_PLAYER(pid, player, );
if (skillId >= Skill::Length)
return;
player->npcStats.mSkills[skillId].mDamage = value;
if (!Utils::vectorContains(player->skillIndexChanges, skillId))
player->skillIndexChanges.push_back(skillId);
}
void StatsFunctions::SetSkillProgress(unsigned short pid, unsigned short skillId, double value) noexcept void StatsFunctions::SetSkillProgress(unsigned short pid, unsigned short skillId, double value) noexcept
{ {
Player *player; Player *player;

View file

@ -30,9 +30,11 @@
\ \
{"GetAttributeBase", StatsFunctions::GetAttributeBase},\ {"GetAttributeBase", StatsFunctions::GetAttributeBase},\
{"GetAttributeModifier", StatsFunctions::GetAttributeModifier},\ {"GetAttributeModifier", StatsFunctions::GetAttributeModifier},\
{"GetAttributeDamage", StatsFunctions::GetAttributeDamage},\
\ \
{"GetSkillBase", StatsFunctions::GetSkillBase},\ {"GetSkillBase", StatsFunctions::GetSkillBase},\
{"GetSkillModifier", StatsFunctions::GetSkillModifier},\ {"GetSkillModifier", StatsFunctions::GetSkillModifier},\
{"GetSkillDamage", StatsFunctions::GetSkillDamage},\
{"GetSkillProgress", StatsFunctions::GetSkillProgress},\ {"GetSkillProgress", StatsFunctions::GetSkillProgress},\
{"GetSkillIncrease", StatsFunctions::GetSkillIncrease},\ {"GetSkillIncrease", StatsFunctions::GetSkillIncrease},\
\ \
@ -58,9 +60,11 @@
\ \
{"SetAttributeBase", StatsFunctions::SetAttributeBase},\ {"SetAttributeBase", StatsFunctions::SetAttributeBase},\
{"ClearAttributeModifier", StatsFunctions::ClearAttributeModifier},\ {"ClearAttributeModifier", StatsFunctions::ClearAttributeModifier},\
{"SetAttributeDamage", StatsFunctions::SetAttributeDamage},\
\ \
{"SetSkillBase", StatsFunctions::SetSkillBase},\ {"SetSkillBase", StatsFunctions::SetSkillBase},\
{"ClearSkillModifier", StatsFunctions::ClearSkillModifier},\ {"ClearSkillModifier", StatsFunctions::ClearSkillModifier},\
{"SetSkillDamage", StatsFunctions::SetSkillDamage},\
{"SetSkillProgress", StatsFunctions::SetSkillProgress},\ {"SetSkillProgress", StatsFunctions::SetSkillProgress},\
{"SetSkillIncrease", StatsFunctions::SetSkillIncrease},\ {"SetSkillIncrease", StatsFunctions::SetSkillIncrease},\
\ \
@ -267,6 +271,16 @@ public:
*/ */
static int GetAttributeModifier(unsigned short pid, unsigned short attributeId) noexcept; static int GetAttributeModifier(unsigned short pid, unsigned short attributeId) noexcept;
/**
* \brief Get the amount of damage (as caused through the Damage Attribute effect)
* to a player's attribute.
*
* \param pid The player ID.
* \param attributeId The attribute ID.
* \return The amount of damage to the attribute.
*/
static double GetAttributeDamage(unsigned short pid, unsigned short attributeId) noexcept;
/** /**
* \brief Get the base value of a player's skill. * \brief Get the base value of a player's skill.
* *
@ -285,6 +299,16 @@ public:
*/ */
static int GetSkillModifier(unsigned short pid, unsigned short skillId) noexcept; static int GetSkillModifier(unsigned short pid, unsigned short skillId) noexcept;
/**
* \brief Get the amount of damage (as caused through the Damage Skill effect)
* to a player's skill.
*
* \param pid The player ID.
* \param skillId The skill ID.
* \return The amount of damage to the skill.
*/
static double GetSkillDamage(unsigned short pid, unsigned short skillId) noexcept;
/** /**
* \brief Get the progress the player has made towards increasing a certain skill by 1. * \brief Get the progress the player has made towards increasing a certain skill by 1.
* *
@ -477,6 +501,17 @@ public:
*/ */
static void ClearAttributeModifier(unsigned short pid, unsigned short attributeId) noexcept; static void ClearAttributeModifier(unsigned short pid, unsigned short attributeId) noexcept;
/**
* \brief Set the amount of damage (as caused through the Damage Attribute effect) to
* a player's attribute.
*
* \param pid The player ID.
* \param attributeId The attribute ID.
* \param value The amount of damage to the player's attribute.
* \return void
*/
static void SetAttributeDamage(unsigned short pid, unsigned short attributeId, double value) noexcept;
/** /**
* \brief Set the base value of a player's skill. * \brief Set the base value of a player's skill.
* *
@ -501,6 +536,17 @@ public:
*/ */
static void ClearSkillModifier(unsigned short pid, unsigned short skillId) noexcept; static void ClearSkillModifier(unsigned short pid, unsigned short skillId) noexcept;
/**
* \brief Set the amount of damage (as caused through the Damage Skill effect) to
* a player's skill.
*
* \param pid The player ID.
* \param skillId The skill ID.
* \param value The amount of damage to the player's skill.
* \return void
*/
static void SetSkillDamage(unsigned short pid, unsigned short skillId, double value) noexcept;
/** /**
* \brief Set the progress the player has made towards increasing a certain skill by 1. * \brief Set the progress the player has made towards increasing a certain skill by 1.
* *

View file

@ -21,7 +21,7 @@ int ScriptFunctions::CreateTimerEx(ScriptFunc callback, int msec, const char *ty
try try
{ {
vector<boost::any> params; vector<boost::any> params;
GetArguments(params, args, types); Utils::getArguments(params, args, types);
return mwmp::TimerAPI::CreateTimer(callback, msec, types, params); return mwmp::TimerAPI::CreateTimer(callback, msec, types, params);
} }
@ -54,5 +54,5 @@ void ScriptFunctions::FreeTimer(int timerId) noexcept
bool ScriptFunctions::IsTimerElapsed(int timerId) noexcept bool ScriptFunctions::IsTimerElapsed(int timerId) noexcept
{ {
return TimerAPI::IsEndTimer(timerId); return TimerAPI::IsTimerElapsed(timerId);
} }

View file

@ -107,6 +107,32 @@ template<> struct F_<1> { static constexpr LuaFuctionData F{"CreateTimerEx", Lan
template<> struct F_<2> { static constexpr LuaFuctionData F{"MakePublic", LangLua::MakePublic}; }; template<> struct F_<2> { static constexpr LuaFuctionData F{"MakePublic", LangLua::MakePublic}; };
template<> struct F_<3> { static constexpr LuaFuctionData F{"CallPublic", LangLua::CallPublic}; }; template<> struct F_<3> { static constexpr LuaFuctionData F{"CallPublic", LangLua::CallPublic}; };
#ifdef __arm__
template<std::size_t... Is>
struct indices {};
template<std::size_t N, std::size_t... Is>
struct build_indices : build_indices<N-1, N-1, Is...> {};
template<std::size_t... Is>
struct build_indices<0, Is...> : indices<Is...> {};
template<std::size_t N>
using IndicesFor = build_indices<N>;
template<size_t... Indices>
LuaFuctionData *functions(indices<Indices...>)
{
static LuaFuctionData functions_[sizeof...(Indices)]{
F_<Indices>::F...
};
static_assert(
sizeof(functions_) / sizeof(functions_[0]) ==
sizeof(ScriptFunctions::functions) / sizeof(ScriptFunctions::functions[0]),
"Not all functions have been mapped to Lua");
return functions_;
}
#else
template<unsigned int I> template<unsigned int I>
struct C struct C
{ {
@ -117,7 +143,6 @@ struct C
} }
}; };
template<> template<>
struct C<0> struct C<0>
{ {
@ -141,6 +166,7 @@ LuaFuctionData *functions()
return functions_; return functions_;
} }
#endif
void LangLua::LoadProgram(const char *filename) void LangLua::LoadProgram(const char *filename)
{ {
@ -152,8 +178,11 @@ void LangLua::LoadProgram(const char *filename)
constexpr auto functions_n = sizeof(ScriptFunctions::functions) / sizeof(ScriptFunctions::functions[0]); constexpr auto functions_n = sizeof(ScriptFunctions::functions) / sizeof(ScriptFunctions::functions[0]);
#if __arm__
LuaFuctionData *functions_ = functions(IndicesFor<functions_n>{});
#else
LuaFuctionData *functions_ = functions<sizeof(ScriptFunctions::functions) / sizeof(ScriptFunctions::functions[0])>(); LuaFuctionData *functions_ = functions<sizeof(ScriptFunctions::functions) / sizeof(ScriptFunctions::functions[0])>();
#endif
luabridge::Namespace tes3mp = luabridge::getGlobalNamespace(lua).beginNamespace("tes3mp"); luabridge::Namespace tes3mp = luabridge::getGlobalNamespace(lua).beginNamespace("tes3mp");
for (unsigned i = 0; i < functions_n; i++) for (unsigned i = 0; i < functions_n; i++)

View file

@ -105,6 +105,7 @@ public:
catch (std::exception &e) catch (std::exception &e)
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, e.what()); LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, e.what());
Script::Call<Script::CallbackIdentity("OnServerScriptCrash")>(e.what());
if (!mwmp::Networking::getPtr()->getScriptErrorIgnoringState()) if (!mwmp::Networking::getPtr()->getScriptErrorIgnoringState())
throw; throw;

View file

@ -13,62 +13,6 @@ constexpr ScriptCallbackData ScriptFunctions::callbacks[];
using namespace std; using namespace std;
void ScriptFunctions::GetArguments(std::vector<boost::any> &params, va_list args, const std::string &def)
{
params.reserve(def.length());
try
{
for (char c : def)
{
switch (c)
{
case 'i':
params.emplace_back(va_arg(args, unsigned int));
break;
case 'q':
params.emplace_back(va_arg(args, signed int));
break;
case 'l':
params.emplace_back(va_arg(args, unsigned long long));
break;
case 'w':
params.emplace_back(va_arg(args, signed long long));
break;
case 'f':
params.emplace_back(va_arg(args, double));
break;
case 'p':
params.emplace_back(va_arg(args, void*));
break;
case 's':
params.emplace_back(va_arg(args, const char*));
break;
case 'b':
params.emplace_back(va_arg(args, int));
break;
default:
throw runtime_error("C++ call: Unknown argument identifier " + c);
}
}
}
catch (...)
{
va_end(args);
throw;
}
va_end(args);
}
void ScriptFunctions::MakePublic(ScriptFunc _public, const char *name, char ret_type, const char *def) noexcept void ScriptFunctions::MakePublic(ScriptFunc _public, const char *name, char ret_type, const char *def) noexcept
{ {
Public::MakePublic(_public, name, ret_type, def); Public::MakePublic(_public, name, ret_type, def);
@ -81,7 +25,7 @@ boost::any ScriptFunctions::CallPublic(const char *name, va_list args) noexcept
try try
{ {
string def = Public::GetDefinition(name); string def = Public::GetDefinition(name);
GetArguments(params, args, def); Utils::getArguments(params, args, def);
return Public::Call(name, params); return Public::Call(name, params);
} }

View file

@ -47,7 +47,6 @@ class ScriptFunctions
{ {
public: public:
static void GetArguments(std::vector<boost::any> &params, va_list args, const std::string &def);
static void MakePublic(ScriptFunc _public, const char *name, char ret_type, const char *def) noexcept; static void MakePublic(ScriptFunc _public, const char *name, char ret_type, const char *def) noexcept;
static boost::any CallPublic(const char *name, va_list args) noexcept; static boost::any CallPublic(const char *name, va_list args) noexcept;
@ -157,6 +156,7 @@ public:
{"OnServerInit", Callback<>()}, {"OnServerInit", Callback<>()},
{"OnServerPostInit", Callback<>()}, {"OnServerPostInit", Callback<>()},
{"OnServerExit", Callback<bool>()}, {"OnServerExit", Callback<bool>()},
{"OnServerScriptCrash", Callback<const char*>()},
{"OnPlayerConnect", Callback<unsigned short>()}, {"OnPlayerConnect", Callback<unsigned short>()},
{"OnPlayerDisconnect", Callback<unsigned short>()}, {"OnPlayerDisconnect", Callback<unsigned short>()},
{"OnPlayerDeath", Callback<unsigned short>()}, {"OnPlayerDeath", Callback<unsigned short>()},
@ -209,7 +209,7 @@ public:
{"OnWorldMap", Callback<unsigned short>()}, {"OnWorldMap", Callback<unsigned short>()},
{"OnWorldWeather", Callback<unsigned short>() }, {"OnWorldWeather", Callback<unsigned short>() },
{"OnMpNumIncrement", Callback<int>()}, {"OnMpNumIncrement", Callback<int>()},
{"OnRequestPluginList", Callback<>()} {"OnRequestDataFileList", Callback<>()}
}; };
}; };

View file

@ -105,7 +105,7 @@ struct ScriptFunctionPointer : public ScriptIdentity
void *addr; void *addr;
#if (!defined(__clang__) && defined(__GNUC__)) #if (!defined(__clang__) && defined(__GNUC__))
template<typename R, typename... Types> template<typename R, typename... Types>
constexpr ScriptFunctionPointer(Function<R, Types...> addr) : ScriptIdentity(addr), addr(reinterpret_cast<void*>(addr)) {} constexpr ScriptFunctionPointer(Function<R, Types...> addr) : ScriptIdentity(addr), addr((void*)(addr)) {}
#else #else
template<typename R, typename... Types> template<typename R, typename... Types>
constexpr ScriptFunctionPointer(Function<R, Types...> addr) : ScriptIdentity(addr), addr(addr) {} constexpr ScriptFunctionPointer(Function<R, Types...> addr) : ScriptIdentity(addr), addr(addr) {}

View file

@ -1,9 +1,7 @@
//
// Created by koncord on 04.03.17.
//
#include "Utils.hpp" #include "Utils.hpp"
#include <cstdarg>
using namespace std; using namespace std;
const vector<string> Utils::split(const string &str, int delimiter) const vector<string> Utils::split(const string &str, int delimiter)
@ -52,3 +50,59 @@ ESM::Cell Utils::getCellFromDescription(std::string cellDescription)
return cell; return cell;
} }
void Utils::getArguments(std::vector<boost::any> &params, va_list args, const std::string &def)
{
params.reserve(def.length());
try
{
for (char c : def)
{
switch (c)
{
case 'i':
params.emplace_back(va_arg(args, unsigned int));
break;
case 'q':
params.emplace_back(va_arg(args, signed int));
break;
case 'l':
params.emplace_back(va_arg(args, unsigned long long));
break;
case 'w':
params.emplace_back(va_arg(args, signed long long));
break;
case 'f':
params.emplace_back(va_arg(args, double));
break;
case 'p':
params.emplace_back(va_arg(args, void*));
break;
case 's':
params.emplace_back(va_arg(args, const char*));
break;
case 'b':
params.emplace_back(va_arg(args, int));
break;
default:
throw runtime_error("C++ call: Unknown argument identifier " + c);
}
}
}
catch (...)
{
va_end(args);
throw;
}
va_end(args);
}

View file

@ -1,7 +1,3 @@
//
// Created by koncord on 04.03.17.
//
#ifndef OPENMW_UTILS_HPP #ifndef OPENMW_UTILS_HPP
#define OPENMW_UTILS_HPP #define OPENMW_UTILS_HPP
@ -9,6 +5,8 @@
#include <regex> #include <regex>
#include <vector> #include <vector>
#include <boost/any.hpp>
#include <components/esm/loadcell.hpp> #include <components/esm/loadcell.hpp>
#include <components/openmw-mp/Utils.hpp> #include <components/openmw-mp/Utils.hpp>
@ -27,6 +25,8 @@ namespace Utils
ESM::Cell getCellFromDescription(std::string cellDescription); ESM::Cell getCellFromDescription(std::string cellDescription);
void getArguments(std::vector<boost::any> &params, va_list args, const std::string &def);
template<size_t N> template<size_t N>
constexpr unsigned int hash(const char(&str)[N], size_t I = N) constexpr unsigned int hash(const char(&str)[N], size_t I = N)
{ {

View file

@ -190,47 +190,27 @@ int main(int argc, char *argv[])
LOG_INIT(logLevel); LOG_INIT(logLevel);
int players = mgr.getInt("maximumPlayers", "General"); int players = mgr.getInt("maximumPlayers", "General");
string addr = mgr.getString("localAddress", "General"); string address = mgr.getString("localAddress", "General");
int port = mgr.getInt("port", "General"); int port = mgr.getInt("port", "General");
string passw = mgr.getString("password", "General"); string password = mgr.getString("password", "General");
string plugin_home = mgr.getString("home", "Plugins"); string pluginHome = mgr.getString("home", "Plugins");
string moddir = Utils::convertPath(plugin_home + "/data"); string dataDirectory = Utils::convertPath(pluginHome + "/data");
vector<string> plugins (Utils::split(mgr.getString("plugins", "Plugins"), ',')); vector<string> plugins(Utils::split(mgr.getString("plugins", "Plugins"), ','));
Utils::printVersion("TES3MP dedicated server", TES3MP_VERSION, version.mCommitHash, TES3MP_PROTO_VERSION); Utils::printVersion("TES3MP dedicated server", TES3MP_VERSION, version.mCommitHash, TES3MP_PROTO_VERSION);
// Check for unmodified tes3mp-credits file; this makes it so people can't repackage official releases with
// their own made-up credits, though it obviously has no bearing on unofficial releases that change
// the checksum below
boost::filesystem::path folderPath(boost::filesystem::initial_path<boost::filesystem::path>());
folderPath = boost::filesystem::system_complete(boost::filesystem::path(argv[0])).remove_filename();
std::string creditsPath = folderPath.string() + "/tes3mp-credits";
unsigned int expectedChecksumInt = Utils::hexStrToInt(TES3MP_CREDITS_CHECKSUM);
bool hasValidCredits = Utils::doesFileHaveChecksum(creditsPath + ".md", expectedChecksumInt);
if (!hasValidCredits)
hasValidCredits = Utils::doesFileHaveChecksum(creditsPath + ".txt", expectedChecksumInt);
if (!hasValidCredits)
{
LOG_MESSAGE_SIMPLE(Log::LOG_FATAL, "The server is shutting down");
LOG_APPEND(Log::LOG_FATAL, "- %s", TES3MP_CREDITS_ERROR);
return 1;
}
Script::SetModDir(moddir); Script::SetModDir(dataDirectory);
#ifdef ENABLE_LUA #ifdef ENABLE_LUA
LangLua::AddPackagePath(Utils::convertPath(plugin_home + "/scripts/?.lua" + ";" LangLua::AddPackagePath(Utils::convertPath(pluginHome + "/scripts/?.lua" + ";"
+ plugin_home + "/lib/lua/?.lua" + ";")); + pluginHome + "/lib/lua/?.lua" + ";"));
#ifdef _WIN32 #ifdef _WIN32
LangLua::AddPackageCPath(Utils::convertPath(plugin_home + "/lib/?.dll")); LangLua::AddPackageCPath(Utils::convertPath(pluginHome + "/lib/?.dll"));
#else #else
LangLua::AddPackageCPath(Utils::convertPath(plugin_home + "/lib/?.so")); LangLua::AddPackageCPath(Utils::convertPath(pluginHome + "/lib/?.so"));
#endif #endif
#endif #endif
@ -246,18 +226,18 @@ int main(int argc, char *argv[])
peer->SetIncomingPassword(sstr.str().c_str(), (int) sstr.str().size()); peer->SetIncomingPassword(sstr.str().c_str(), (int) sstr.str().size());
if (RakNet::NonNumericHostString(addr.c_str())) if (RakNet::NonNumericHostString(address.c_str()))
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, "You cannot use non-numeric addresses for the server."); LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, "You cannot use non-numeric addresses for the server.");
return 1; return 1;
} }
RakNet::SocketDescriptor sd((unsigned short) port, addr.c_str()); RakNet::SocketDescriptor sd((unsigned short) port, address.c_str());
try try
{ {
for (auto plugin : plugins) for (auto plugin : plugins)
Script::LoadScript(plugin.c_str(), plugin_home.c_str()); Script::LoadScript(plugin.c_str(), pluginHome.c_str());
switch (peer->Startup((unsigned) players, &sd, 1)) switch (peer->Startup((unsigned) players, &sd, 1))
{ {
@ -284,7 +264,7 @@ int main(int argc, char *argv[])
peer->SetMaximumIncomingConnections((unsigned short) (players)); peer->SetMaximumIncomingConnections((unsigned short) (players));
Networking networking(peer); Networking networking(peer);
networking.setServerPassword(passw); networking.setServerPassword(password);
if (mgr.getBool("enabled", "MasterServer")) if (mgr.getBool("enabled", "MasterServer"))
{ {
@ -327,6 +307,7 @@ int main(int argc, char *argv[])
catch (std::exception &e) catch (std::exception &e)
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, e.what()); LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, e.what());
Script::Call<Script::CallbackIdentity("OnServerScriptCrash")>(e.what());
throw; //fall through throw; //fall through
} }

View file

@ -1,7 +1,3 @@
//
// Created by koncord on 31.03.17.
//
#ifndef OPENMW_PROCESSORPLAYERPOSITION_HPP #ifndef OPENMW_PROCESSORPLAYERPOSITION_HPP
#define OPENMW_PROCESSORPLAYERPOSITION_HPP #define OPENMW_PROCESSORPLAYERPOSITION_HPP
@ -19,11 +15,7 @@ namespace mwmp
void Do(PlayerPacket &packet, Player &player) override void Do(PlayerPacket &packet, Player &player) override
{ {
//DEBUG_PRINTF(strPacketID); player.sendToLoaded(&packet);
if (!player.creatureStats.mDead)
{
player.sendToLoaded(&packet);
}
} }
}; };
} }

View file

@ -134,7 +134,7 @@ add_openmw_dir (mwmp/processors/object BaseObjectProcessor
ProcessorScriptGlobalFloat ProcessorScriptGlobalFloat
) )
add_openmw_dir (mwmp/processors/worldstate ProcessorCellCreate ProcessorCellReplace ProcessorRecordDynamic add_openmw_dir (mwmp/processors/worldstate ProcessorCellCreate ProcessorCellReset ProcessorRecordDynamic
ProcessorWorldCollisionOverride ProcessorWorldMap ProcessorWorldRegionAuthority ProcessorWorldTime ProcessorWorldCollisionOverride ProcessorWorldMap ProcessorWorldRegionAuthority ProcessorWorldTime
ProcessorWorldWeather ProcessorWorldWeather
) )

View file

@ -221,10 +221,11 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
/* /*
Start of tes3mp addition Start of tes3mp addition
Check for unmodified tes3mp-credits file; this makes it so people can't repackage official releases with Check for unmodified tes3mp-credits file on Windows; this makes it so people can't repackage official
their own made-up credits, though it obviously has no bearing on unofficial releases that change releases with their own made-up credits, though it obviously has no bearing on unofficial releases that
the checksum below change the checksum below
*/ */
#ifdef _WIN32
boost::filesystem::path folderPath(boost::filesystem::initial_path<boost::filesystem::path>()); boost::filesystem::path folderPath(boost::filesystem::initial_path<boost::filesystem::path>());
folderPath = boost::filesystem::system_complete(boost::filesystem::path(argv[0])).remove_filename(); folderPath = boost::filesystem::system_complete(boost::filesystem::path(argv[0])).remove_filename();
std::string creditsPath = folderPath.string() + "/tes3mp-credits"; std::string creditsPath = folderPath.string() + "/tes3mp-credits";
@ -242,6 +243,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "tes3mp", TES3MP_CREDITS_ERROR, 0); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "tes3mp", TES3MP_CREDITS_ERROR, 0);
return false; return false;
} }
#endif
/* /*
End of tes3mp addition End of tes3mp addition
*/ */

View file

@ -553,9 +553,10 @@ namespace MWClass
/* /*
Start of tes3mp addition Start of tes3mp addition
If the attacker was the LocalPlayer or LocalActor, record their target and send a packet with it If the attacker was the LocalPlayer or LocalActor, record their target and send a
packet with it
If the victim was a LocalActor who died, record their attacker as the deathReason If the victim was a LocalActor who died, record their attacker as the killer
*/ */
mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(attacker); mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(attacker);

View file

@ -966,12 +966,13 @@ namespace MWClass
/* /*
Start of tes3mp addition Start of tes3mp addition
If the attacker was the LocalPlayer or LocalActor, record their target and send a packet with it If the attacker was the LocalPlayer or LocalActor, record their target and send a
packet with it
If the victim was the LocalPlayer, check whether packets should be sent about If the victim was the LocalPlayer, check whether packets should be sent about
their new dynamic stats and position their new dynamic stats and position
If the victim was a LocalActor who died, record their attacker as the deathReason If the victim was a LocalActor who died, record their attacker as the killer
*/ */
mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(attacker); mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(attacker);

View file

@ -458,6 +458,15 @@ namespace MWGui
updateMagicMarkers(); updateMagicMarkers();
updateCustomMarkers(); updateCustomMarkers();
/*
Start of tes3mp addition
Update player markers when cell changes to fix their locations
*/
updatePlayerMarkers();
/*
End of tes3mp addition
*/
} }
void LocalMapBase::requestMapRender(const MWWorld::CellStore *cell) void LocalMapBase::requestMapRender(const MWWorld::CellStore *cell)

View file

@ -9,6 +9,19 @@
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
/*
Start of tes3mp addition
Include additional headers for multiplayer purposes
*/
#include "../mwmp/Main.hpp"
#include "../mwmp/Networking.hpp"
#include "../mwmp/LocalPlayer.hpp"
#include "../mwmp/MechanicsHelper.hpp"
/*
End of tes3mp addition
*/
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
@ -163,9 +176,26 @@ void Recharge::onItemClicked(MyGUI::Widget *sender, const MWWorld::Ptr& item)
const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find( const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find(
item.getClass().getEnchantment(item)); item.getClass().getEnchantment(item));
/*
Start of tes3mp change (minor)
Send PlayerInventory packets that replace the original item with the new one
*/
mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer();
mwmp::Item removedItem = MechanicsHelper::getItem(item, 1);
item.getCellRef().setEnchantmentCharge( item.getCellRef().setEnchantmentCharge(
std::min(item.getCellRef().getEnchantmentCharge() + restored, static_cast<float>(enchantment->mData.mCharge))); std::min(item.getCellRef().getEnchantmentCharge() + restored, static_cast<float>(enchantment->mData.mCharge)));
mwmp::Item addedItem = MechanicsHelper::getItem(item, 1);
localPlayer->sendItemChange(addedItem, mwmp::InventoryChanges::ADD);
localPlayer->sendItemChange(removedItem, mwmp::InventoryChanges::REMOVE);
/*
End of tes3mp change (minor)
*/
MWBase::Environment::get().getWindowManager()->playSound("Enchant Success"); MWBase::Environment::get().getWindowManager()->playSound("Enchant Success");
player.getClass().getContainerStore(player).restack(item); player.getClass().getContainerStore(player).restack(item);

View file

@ -842,6 +842,22 @@ namespace MWMechanics
if (isDamageEffect) if (isDamageEffect)
{ {
/*
Start of tes3mp addition
If the victim was a LocalActor who died, record the caster as the killer
*/
if (mwmp::Main::get().getCellController()->isLocalActor(ptr))
{
bool isSuicide = ptr == caster || caster.isEmpty();
mwmp::Main::get().getCellController()->getLocalActor(ptr)->killer = isSuicide ?
MechanicsHelper::getTarget(ptr) : MechanicsHelper::getTarget(caster);
}
/*
End of tes3mp addition
*/
if (caster == player || playerFollowers.find(caster) != playerFollowers.end()) if (caster == player || playerFollowers.find(caster) != playerFollowers.end())
{ {
if (caster.getClass().getNpcStats(caster).isWerewolf()) if (caster.getClass().getNpcStats(caster).isWerewolf())

View file

@ -4,6 +4,19 @@
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
/*
Start of tes3mp addition
Include additional headers for multiplayer purposes
*/
#include "../mwmp/Main.hpp"
#include "../mwmp/Networking.hpp"
#include "../mwmp/LocalPlayer.hpp"
#include "../mwmp/MechanicsHelper.hpp"
/*
End of tes3mp addition
*/
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
@ -57,8 +70,25 @@ void Repair::repair(const MWWorld::Ptr &itemToRepair)
// repair by 'y' points // repair by 'y' points
int charge = itemToRepair.getClass().getItemHealth(itemToRepair); int charge = itemToRepair.getClass().getItemHealth(itemToRepair);
charge = std::min(charge + y, itemToRepair.getClass().getItemMaxHealth(itemToRepair)); charge = std::min(charge + y, itemToRepair.getClass().getItemMaxHealth(itemToRepair));
/*
Start of tes3mp change (minor)
Send PlayerInventory packets that replace the original item with the new one
*/
mwmp::LocalPlayer *localPlayer = mwmp::Main::get().getLocalPlayer();
mwmp::Item removedItem = MechanicsHelper::getItem(itemToRepair, 1);
itemToRepair.getCellRef().setCharge(charge); itemToRepair.getCellRef().setCharge(charge);
mwmp::Item addedItem = MechanicsHelper::getItem(itemToRepair, 1);
localPlayer->sendItemChange(addedItem, mwmp::InventoryChanges::ADD);
localPlayer->sendItemChange(removedItem, mwmp::InventoryChanges::REMOVE);
/*
End of tes3mp change (minor)
*/
// attempt to re-stack item, in case it was fully repaired // attempt to re-stack item, in case it was fully repaired
MWWorld::ContainerStoreIterator stacked = player.getClass().getContainerStore(player).restack(itemToRepair); MWWorld::ContainerStoreIterator stacked = player.getClass().getContainerStore(player).restack(itemToRepair);

View file

@ -537,7 +537,7 @@ namespace MWMechanics
/* /*
Start of tes3mp addition Start of tes3mp addition
If the victim was a LocalPlayer or LocalActor who died, record their attacker as the deathReason If the victim was a LocalPlayer or LocalActor who died, record the caster as the killer
*/ */
if (!wasDead && isDead) if (!wasDead && isDead)
{ {

View file

@ -82,8 +82,10 @@ void Cell::updateLocal(bool forceUpdate)
} }
else else
{ {
// Forcibly update this local actor if its data has never been sent before;
// otherwise, use the current forceUpdate value
if (actor->getPtr().getRefData().isEnabled()) if (actor->getPtr().getRefData().isEnabled())
actor->update(forceUpdate); actor->update(actor->hasSentData ? forceUpdate : true);
++it; ++it;
} }
@ -397,6 +399,9 @@ void Cell::readCellChange(ActorList& actorList)
void Cell::initializeLocalActor(const MWWorld::Ptr& ptr) void Cell::initializeLocalActor(const MWWorld::Ptr& ptr)
{ {
std::string mapIndex = Main::get().getCellController()->generateMapIndex(ptr);
LOG_APPEND(Log::LOG_VERBOSE, "- Initializing LocalActor %s in %s", mapIndex.c_str(), getDescription().c_str());
LocalActor *actor = new LocalActor(); LocalActor *actor = new LocalActor();
actor->cell = *store->getCell(); actor->cell = *store->getCell();
actor->setPtr(ptr); actor->setPtr(ptr);
@ -406,16 +411,17 @@ void Cell::initializeLocalActor(const MWWorld::Ptr& ptr)
if (ptr.getClass().getCreatureStats(ptr).isDead()) if (ptr.getClass().getCreatureStats(ptr).isDead())
actor->wasDead = true; actor->wasDead = true;
std::string mapIndex = Main::get().getCellController()->generateMapIndex(ptr);
localActors[mapIndex] = actor; localActors[mapIndex] = actor;
Main::get().getCellController()->setLocalActorRecord(mapIndex, getDescription()); Main::get().getCellController()->setLocalActorRecord(mapIndex, getDescription());
LOG_APPEND(Log::LOG_VERBOSE, "- Initialized LocalActor %s in %s", mapIndex.c_str(), getDescription().c_str()); LOG_APPEND(Log::LOG_VERBOSE, "- Successfully initialized LocalActor %s in %s", mapIndex.c_str(), getDescription().c_str());
} }
void Cell::initializeLocalActors() void Cell::initializeLocalActors()
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_VERBOSE, "Initializing LocalActors in %s", getDescription().c_str());
for (const auto &mergedRef : store->getMergedRefs()) for (const auto &mergedRef : store->getMergedRefs())
{ {
if (mergedRef->mClass->isActor()) if (mergedRef->mClass->isActor())
@ -432,20 +438,24 @@ void Cell::initializeLocalActors()
initializeLocalActor(ptr); initializeLocalActor(ptr);
} }
} }
LOG_APPEND(Log::LOG_VERBOSE, "- Successfully initialized LocalActors in %s", getDescription().c_str());
} }
void Cell::initializeDedicatedActor(const MWWorld::Ptr& ptr) void Cell::initializeDedicatedActor(const MWWorld::Ptr& ptr)
{ {
std::string mapIndex = Main::get().getCellController()->generateMapIndex(ptr);
LOG_APPEND(Log::LOG_VERBOSE, "- Initializing DedicatedActor %s in %s", mapIndex.c_str(), getDescription().c_str());
DedicatedActor *actor = new DedicatedActor(); DedicatedActor *actor = new DedicatedActor();
actor->cell = *store->getCell(); actor->cell = *store->getCell();
actor->setPtr(ptr); actor->setPtr(ptr);
std::string mapIndex = Main::get().getCellController()->generateMapIndex(ptr);
dedicatedActors[mapIndex] = actor; dedicatedActors[mapIndex] = actor;
Main::get().getCellController()->setDedicatedActorRecord(mapIndex, getDescription()); Main::get().getCellController()->setDedicatedActorRecord(mapIndex, getDescription());
LOG_APPEND(Log::LOG_VERBOSE, "- Initialized DedicatedActor %s in %s", mapIndex.c_str(), getDescription().c_str()); LOG_APPEND(Log::LOG_VERBOSE, "- Successfully initialized DedicatedActor %s in %s", mapIndex.c_str(), getDescription().c_str());
} }
void Cell::initializeDedicatedActors(ActorList& actorList) void Cell::initializeDedicatedActors(ActorList& actorList)

View file

@ -77,6 +77,8 @@ void CellController::initializeCell(const ESM::Cell& cell)
// If this key doesn't exist, create it // If this key doesn't exist, create it
if (cellsInitialized.count(mapIndex) == 0) if (cellsInitialized.count(mapIndex) == 0)
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Initializing mwmp::Cell %s", cell.getDescription().c_str());
MWWorld::CellStore *cellStore = getCellStore(cell); MWWorld::CellStore *cellStore = getCellStore(cell);
if (!cellStore) return; if (!cellStore) return;
@ -84,7 +86,7 @@ void CellController::initializeCell(const ESM::Cell& cell)
mwmp::Cell *mpCell = new mwmp::Cell(cellStore); mwmp::Cell *mpCell = new mwmp::Cell(cellStore);
cellsInitialized[mapIndex] = mpCell; cellsInitialized[mapIndex] = mpCell;
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "- Initialized mwmp::Cell %s", mpCell->getDescription().c_str()); LOG_APPEND(Log::LOG_VERBOSE, "- Successfully initialized mwmp::Cell %s", cell.getDescription().c_str());
} }
} }

View file

@ -71,6 +71,13 @@ DedicatedPlayer::~DedicatedPlayer()
void DedicatedPlayer::update(float dt) void DedicatedPlayer::update(float dt)
{ {
// Only move and set anim flags if the framerate isn't too low
if (dt < 0.1)
{
move(dt);
setAnimFlags();
}
MWMechanics::CreatureStats *ptrCreatureStats = &ptr.getClass().getCreatureStats(ptr); MWMechanics::CreatureStats *ptrCreatureStats = &ptr.getClass().getCreatureStats(ptr);
MWMechanics::DynamicStat<float> value; MWMechanics::DynamicStat<float> value;
@ -100,13 +107,6 @@ void DedicatedPlayer::update(float dt)
ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Fight, 0); ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Fight, 0);
ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Flee, 0); ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Flee, 0);
ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Hello, 0); ptrCreatureStats->setAiSetting(MWMechanics::CreatureStats::AI_Hello, 0);
// Only move and set anim flags if the framerate isn't too low
if (dt < 0.1)
{
move(dt);
setAnimFlags();
}
} }
void DedicatedPlayer::move(float dt) void DedicatedPlayer::move(float dt)

View file

@ -22,6 +22,7 @@ using namespace std;
LocalActor::LocalActor() LocalActor::LocalActor()
{ {
hasSentData = false;
posWasChanged = false; posWasChanged = false;
equipmentChanged = false; equipmentChanged = false;
@ -61,14 +62,16 @@ void LocalActor::update(bool forceUpdate)
updateSpeech(); updateSpeech();
updateAttack(); updateAttack();
} }
hasSentData = true;
} }
void LocalActor::updateCell() void LocalActor::updateCell()
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_VERBOSE, "Sending ID_ACTOR_CELL_CHANGE about %s %i-%i to server", LOG_MESSAGE_SIMPLE(Log::LOG_VERBOSE, "Sending ID_ACTOR_CELL_CHANGE about %s %i-%i in cell %s to server",
refId.c_str(), refNum, mpNum); refId.c_str(), refNum, mpNum, cell.getDescription().c_str());
LOG_APPEND(Log::LOG_VERBOSE, "- Moved from %s to %s", cell.getDescription().c_str(), ptr.getCell()->getCell()->getDescription().c_str()); LOG_APPEND(Log::LOG_VERBOSE, "- Moved to cell %s", ptr.getCell()->getCell()->getDescription().c_str());
cell = *ptr.getCell()->getCell(); cell = *ptr.getCell()->getCell();
position = ptr.getRefData().getPosition(); position = ptr.getRefData().getPosition();
@ -191,8 +194,8 @@ void LocalActor::updateStatsDynamic(bool forceUpdate)
if (MechanicsHelper::isEmptyTarget(killer)) if (MechanicsHelper::isEmptyTarget(killer))
killer = MechanicsHelper::getTarget(ptr); killer = MechanicsHelper::getTarget(ptr);
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Sending ID_ACTOR_DEATH about %s %i-%i to server", LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Sending ID_ACTOR_DEATH about %s %i-%i in cell %s to server",
refId.c_str(), refNum, mpNum); refId.c_str(), refNum, mpNum, cell.getDescription().c_str());
mwmp::Main::get().getNetworking()->getActorList()->addDeathActor(*this); mwmp::Main::get().getNetworking()->getActorList()->addDeathActor(*this);

View file

@ -28,6 +28,7 @@ namespace mwmp
MWWorld::Ptr getPtr(); MWWorld::Ptr getPtr();
void setPtr(const MWWorld::Ptr& newPtr); void setPtr(const MWWorld::Ptr& newPtr);
bool hasSentData;
bool wasDead; bool wasDead;
private: private:

View file

@ -284,6 +284,7 @@ void LocalPlayer::updateAttributes(bool forceUpdate)
{ {
if (ptrNpcStats.getAttribute(i).getBase() != creatureStats.mAttributes[i].mBase || if (ptrNpcStats.getAttribute(i).getBase() != creatureStats.mAttributes[i].mBase ||
ptrNpcStats.getAttribute(i).getModifier() != creatureStats.mAttributes[i].mMod || ptrNpcStats.getAttribute(i).getModifier() != creatureStats.mAttributes[i].mMod ||
ptrNpcStats.getAttribute(i).getDamage() != creatureStats.mAttributes[i].mDamage ||
ptrNpcStats.getSkillIncrease(i) != npcStats.mSkillIncrease[i] || ptrNpcStats.getSkillIncrease(i) != npcStats.mSkillIncrease[i] ||
forceUpdate) forceUpdate)
{ {
@ -318,6 +319,7 @@ void LocalPlayer::updateSkills(bool forceUpdate)
// Update a skill if its base value has changed at all or its progress has changed enough // Update a skill if its base value has changed at all or its progress has changed enough
if (ptrNpcStats.getSkill(i).getBase() != npcStats.mSkills[i].mBase || if (ptrNpcStats.getSkill(i).getBase() != npcStats.mSkills[i].mBase ||
ptrNpcStats.getSkill(i).getModifier() != npcStats.mSkills[i].mMod || ptrNpcStats.getSkill(i).getModifier() != npcStats.mSkills[i].mMod ||
ptrNpcStats.getSkill(i).getDamage() != npcStats.mSkills[i].mDamage ||
abs(ptrNpcStats.getSkill(i).getProgress() - npcStats.mSkills[i].mProgress) > 0.75 || abs(ptrNpcStats.getSkill(i).getProgress() - npcStats.mSkills[i].mProgress) > 0.75 ||
forceUpdate) forceUpdate)
{ {
@ -920,7 +922,7 @@ void LocalPlayer::setAttributes()
{ {
MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::Ptr ptrPlayer = getPlayerPtr();
MWMechanics::CreatureStats *ptrCreatureStats = &ptrPlayer.getClass().getCreatureStats(ptrPlayer); MWMechanics::NpcStats *ptrNpcStats = &ptrPlayer.getClass().getNpcStats(ptrPlayer);
MWMechanics::AttributeValue attributeValue; MWMechanics::AttributeValue attributeValue;
for (int attributeIndex = 0; attributeIndex < 8; ++attributeIndex) for (int attributeIndex = 0; attributeIndex < 8; ++attributeIndex)
@ -928,14 +930,14 @@ void LocalPlayer::setAttributes()
// If the server wants to clear our attribute's non-zero modifier, we need to remove // If the server wants to clear our attribute's non-zero modifier, we need to remove
// the spell effect causing it, to avoid an infinite loop where the effect keeps resetting // the spell effect causing it, to avoid an infinite loop where the effect keeps resetting
// the modifier // the modifier
if (creatureStats.mAttributes[attributeIndex].mMod == 0 && ptrCreatureStats->getAttribute(attributeIndex).getModifier() > 0) if (creatureStats.mAttributes[attributeIndex].mMod == 0 && ptrNpcStats->getAttribute(attributeIndex).getModifier() > 0)
{ {
ptrCreatureStats->getActiveSpells().purgeEffectByArg(ESM::MagicEffect::FortifyAttribute, attributeIndex); ptrNpcStats->getActiveSpells().purgeEffectByArg(ESM::MagicEffect::FortifyAttribute, attributeIndex);
MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(ptrPlayer); MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(ptrPlayer);
// Is the modifier for this attribute still higher than 0? If so, unequip items that // Is the modifier for this attribute still higher than 0? If so, unequip items that
// fortify the attribute // fortify the attribute
if (ptrCreatureStats->getAttribute(attributeIndex).getModifier() > 0) if (ptrNpcStats->getAttribute(attributeIndex).getModifier() > 0)
{ {
MechanicsHelper::unequipItemsByEffect(ptrPlayer, ESM::Enchantment::ConstantEffect, ESM::MagicEffect::FortifyAttribute, attributeIndex, -1); MechanicsHelper::unequipItemsByEffect(ptrPlayer, ESM::Enchantment::ConstantEffect, ESM::MagicEffect::FortifyAttribute, attributeIndex, -1);
mwmp::Main::get().getGUIController()->refreshGuiMode(MWGui::GM_Inventory); mwmp::Main::get().getGUIController()->refreshGuiMode(MWGui::GM_Inventory);
@ -943,7 +945,9 @@ void LocalPlayer::setAttributes()
} }
attributeValue.readState(creatureStats.mAttributes[attributeIndex]); attributeValue.readState(creatureStats.mAttributes[attributeIndex]);
ptrCreatureStats->setAttribute(attributeIndex, attributeValue); ptrNpcStats->setAttribute(attributeIndex, attributeValue);
ptrNpcStats->setSkillIncrease(attributeIndex, npcStats.mSkillIncrease[attributeIndex]);
} }
} }
@ -976,11 +980,6 @@ void LocalPlayer::setSkills()
skillValue.readState(npcStats.mSkills[skillIndex]); skillValue.readState(npcStats.mSkills[skillIndex]);
ptrNpcStats->setSkill(skillIndex, skillValue); ptrNpcStats->setSkill(skillIndex, skillValue);
} }
for (int attributeIndex = 0; attributeIndex < 8; ++attributeIndex)
ptrNpcStats->setSkillIncrease(attributeIndex, npcStats.mSkillIncrease[attributeIndex]);
ptrNpcStats->setLevelProgress(npcStats.mLevelProgress);
} }
void LocalPlayer::setLevel() void LocalPlayer::setLevel()
@ -988,8 +987,9 @@ void LocalPlayer::setLevel()
MWBase::World *world = MWBase::Environment::get().getWorld(); MWBase::World *world = MWBase::Environment::get().getWorld();
MWWorld::Ptr ptrPlayer = world->getPlayerPtr(); MWWorld::Ptr ptrPlayer = world->getPlayerPtr();
MWMechanics::CreatureStats *ptrCreatureStats = &ptrPlayer.getClass().getCreatureStats(ptrPlayer); MWMechanics::NpcStats *ptrNpcStats = &ptrPlayer.getClass().getNpcStats(ptrPlayer);
ptrCreatureStats->setLevel(creatureStats.mLevel); ptrNpcStats->setLevel(creatureStats.mLevel);
ptrNpcStats->setLevelProgress(npcStats.mLevelProgress);
} }
void LocalPlayer::setBounty() void LocalPlayer::setBounty()
@ -1403,31 +1403,23 @@ void LocalPlayer::sendInventory()
getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->Send(); getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->Send();
} }
void LocalPlayer::sendItemChange(const mwmp::Item& item, unsigned int action)
{
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Sending item change for %s with action %i, count %i",
item.refId.c_str(), action, item.count);
inventoryChanges.items.clear();
inventoryChanges.items.push_back(item);
inventoryChanges.action = action;
getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->setPlayer(this);
getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->Send();
}
void LocalPlayer::sendItemChange(const MWWorld::Ptr& itemPtr, int count, unsigned int action) void LocalPlayer::sendItemChange(const MWWorld::Ptr& itemPtr, int count, unsigned int action)
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Sending item change for %s with action %i, count %i", mwmp::Item item = MechanicsHelper::getItem(itemPtr, count);
itemPtr.getCellRef().getRefId().c_str(), action, count); sendItemChange(item, action);
inventoryChanges.items.clear();
mwmp::Item item;
if (itemPtr.getClass().isGold(itemPtr))
item.refId = MWWorld::ContainerStore::sGoldId;
else
item.refId = itemPtr.getCellRef().getRefId();
item.count = count;
item.charge = itemPtr.getCellRef().getCharge();
item.enchantmentCharge = itemPtr.getCellRef().getEnchantmentCharge();
item.soul = itemPtr.getCellRef().getSoul();
inventoryChanges.items.push_back(item);
inventoryChanges.action = action;
getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->setPlayer(this);
getNetworking()->getPlayerPacket(ID_PLAYER_INVENTORY)->Send();
} }
void LocalPlayer::sendItemChange(const std::string& refId, int count, unsigned int action) void LocalPlayer::sendItemChange(const std::string& refId, int count, unsigned int action)

View file

@ -77,6 +77,7 @@ namespace mwmp
void sendClass(); void sendClass();
void sendInventory(); void sendInventory();
void sendItemChange(const mwmp::Item& item, unsigned int action);
void sendItemChange(const MWWorld::Ptr& itemPtr, int count, unsigned int action); void sendItemChange(const MWWorld::Ptr& itemPtr, int count, unsigned int action);
void sendItemChange(const std::string& refId, int count, unsigned int action); void sendItemChange(const std::string& refId, int count, unsigned int action);
void sendSpellbook(); void sendSpellbook();

View file

@ -51,8 +51,8 @@ using namespace mwmp;
using namespace std; using namespace std;
Main *Main::pMain = 0; Main *Main::pMain = 0;
std::string Main::addr = ""; std::string Main::address = "";
std::string Main::passw = TES3MP_DEFAULT_PASSW; std::string Main::serverPassword = TES3MP_DEFAULT_PASSW;
std::string Main::resourceDir = ""; std::string Main::resourceDir = "";
std::string Main::getResDir() std::string Main::getResDir()
@ -118,8 +118,8 @@ void Main::optionsDesc(boost::program_options::options_description *desc)
void Main::configure(const boost::program_options::variables_map &variables) void Main::configure(const boost::program_options::variables_map &variables)
{ {
Main::addr = variables["connect"].as<string>(); Main::address = variables["connect"].as<string>();
Main::passw = variables["password"].as<string>(); Main::serverPassword = variables["password"].as<string>();
resourceDir = variables["resources"].as<Files::EscapeHashString>().toStdString(); resourceDir = variables["resources"].as<Files::EscapeHashString>().toStdString();
} }
@ -155,22 +155,22 @@ bool Main::init(std::vector<std::string> &content, Files::Collections &collectio
int logLevel = mgr.getInt("logLevel", "General"); int logLevel = mgr.getInt("logLevel", "General");
Log::SetLevel(logLevel); Log::SetLevel(logLevel);
if (addr.empty()) if (address.empty())
{ {
pMain->server = mgr.getString("destinationAddress", "General"); pMain->server = mgr.getString("destinationAddress", "General");
pMain->port = (unsigned short) mgr.getInt("port", "General"); pMain->port = (unsigned short) mgr.getInt("port", "General");
passw = mgr.getString("password", "General"); serverPassword = mgr.getString("password", "General");
if (passw.empty()) if (serverPassword.empty())
passw = TES3MP_DEFAULT_PASSW; serverPassword = TES3MP_DEFAULT_PASSW;
} }
else else
{ {
size_t delim_pos = addr.find(':'); size_t delimPos = address.find(':');
pMain->server = addr.substr(0, delim_pos); pMain->server = address.substr(0, delimPos);
pMain->port = atoi(addr.substr(delim_pos + 1).c_str()); pMain->port = atoi(address.substr(delimPos + 1).c_str());
} }
get().mLocalPlayer->passw = passw; get().mLocalPlayer->serverPassword = serverPassword;
pMain->mNetworking->connect(pMain->server, pMain->port, content, collections); pMain->mNetworking->connect(pMain->server, pMain->port, content, collections);
RestoreMgr(mgr); RestoreMgr(mgr);

View file

@ -39,8 +39,8 @@ namespace mwmp
private: private:
static std::string resourceDir; static std::string resourceDir;
static std::string addr; static std::string address;
static std::string passw; static std::string serverPassword;
Main (const Main&); Main (const Main&);
///< not implemented ///< not implemented
Main& operator= (const Main&); Main& operator= (const Main&);

View file

@ -125,6 +125,23 @@ MWWorld::Ptr MechanicsHelper::getPlayerPtr(const Target& target)
return nullptr; return nullptr;
} }
mwmp::Item MechanicsHelper::getItem(const MWWorld::Ptr& itemPtr, int count)
{
mwmp::Item item;
if (itemPtr.getClass().isGold(itemPtr))
item.refId = MWWorld::ContainerStore::sGoldId;
else
item.refId = itemPtr.getCellRef().getRefId();
item.count = count;
item.charge = itemPtr.getCellRef().getCharge();
item.enchantmentCharge = itemPtr.getCellRef().getEnchantmentCharge();
item.soul = itemPtr.getCellRef().getSoul();
return item;
}
mwmp::Target MechanicsHelper::getTarget(const MWWorld::Ptr& ptr) mwmp::Target MechanicsHelper::getTarget(const MWWorld::Ptr& ptr)
{ {
mwmp::Target target; mwmp::Target target;
@ -393,18 +410,14 @@ void MechanicsHelper::processAttack(Attack attack, const MWWorld::Ptr& attacker)
break; break;
} }
if (it != inventoryStore.end()) // Add the item if it's missing
{ if (it == inventoryStore.end())
inventoryStore.setSelectedEnchantItem(it); it = attacker.getClass().getContainerStore(attacker).add(attack.itemId, 1, attacker);
LOG_APPEND(Log::LOG_VERBOSE, "- itemId: %s", attack.itemId.c_str());
MWBase::Environment::get().getWorld()->castSpell(attacker); inventoryStore.setSelectedEnchantItem(it);
inventoryStore.setSelectedEnchantItem(inventoryStore.end()); LOG_APPEND(Log::LOG_VERBOSE, "- itemId: %s", attack.itemId.c_str());
} MWBase::Environment::get().getWorld()->castSpell(attacker);
else inventoryStore.setSelectedEnchantItem(inventoryStore.end());
{
LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, "Could not find item %s used by %s to cast item spell!",
attack.itemId.c_str(), attacker.getCellRef().getRefId().c_str());
}
} }
} }

View file

@ -22,6 +22,7 @@ namespace MechanicsHelper
MWWorld::Ptr getPlayerPtr(const mwmp::Target& target); MWWorld::Ptr getPlayerPtr(const mwmp::Target& target);
mwmp::Item getItem(const MWWorld::Ptr& itemPtr, int count);
mwmp::Target getTarget(const MWWorld::Ptr& ptr); mwmp::Target getTarget(const MWWorld::Ptr& ptr);
void clearTarget(mwmp::Target& target); void clearTarget(mwmp::Target& target);
bool isEmptyTarget(const mwmp::Target& target); bool isEmptyTarget(const mwmp::Target& target);

View file

@ -110,7 +110,7 @@ void RecordHelper::overrideCreatureRecord(const mwmp::CreatureRecord& record)
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
@ -164,6 +164,11 @@ void RecordHelper::overrideCreatureRecord(const mwmp::CreatureRecord& record)
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
{
LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
if (isExistingId) if (isExistingId)
world->updatePtrsWithRefId(recordData.mId); world->updatePtrsWithRefId(recordData.mId);
@ -175,7 +180,7 @@ void RecordHelper::overrideNpcRecord(const mwmp::NpcRecord& record)
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
@ -186,12 +191,12 @@ void RecordHelper::overrideNpcRecord(const mwmp::NpcRecord& record)
{ {
if (!doesRaceRecordExist(recordData.mRace)) if (!doesRaceRecordExist(recordData.mRace))
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring new NPC record with invalid race provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring new NPC record with invalid race provided");
return; return;
} }
else if (!doesClassRecordExist(recordData.mClass)) else if (!doesClassRecordExist(recordData.mClass))
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring new NPC record with invalid class provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring new NPC record with invalid class provided");
return; return;
} }
else else
@ -272,6 +277,11 @@ void RecordHelper::overrideNpcRecord(const mwmp::NpcRecord& record)
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
{
LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
if (isExistingId) if (isExistingId)
world->updatePtrsWithRefId(recordData.mId); world->updatePtrsWithRefId(recordData.mId);
@ -283,18 +293,17 @@ void RecordHelper::overrideEnchantmentRecord(const mwmp::EnchantmentRecord& reco
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
bool isExistingId = doesEnchantmentRecordExist(recordData.mId);
MWBase::World *world = MWBase::Environment::get().getWorld(); MWBase::World *world = MWBase::Environment::get().getWorld();
if (record.baseId.empty()) if (record.baseId.empty())
{ {
if (recordData.mEffects.mList.empty()) if (recordData.mEffects.mList.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring new enchantment record with no effects"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring new enchantment record with no effects");
return; return;
} }
else else
@ -323,6 +332,11 @@ void RecordHelper::overrideEnchantmentRecord(const mwmp::EnchantmentRecord& reco
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
{
LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
} }
void RecordHelper::overridePotionRecord(const mwmp::PotionRecord& record) void RecordHelper::overridePotionRecord(const mwmp::PotionRecord& record)
@ -331,7 +345,7 @@ void RecordHelper::overridePotionRecord(const mwmp::PotionRecord& record)
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
@ -374,6 +388,11 @@ void RecordHelper::overridePotionRecord(const mwmp::PotionRecord& record)
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
{
LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
if (isExistingId) if (isExistingId)
world->updatePtrsWithRefId(recordData.mId); world->updatePtrsWithRefId(recordData.mId);
@ -385,7 +404,7 @@ void RecordHelper::overrideSpellRecord(const mwmp::SpellRecord& record)
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
@ -419,9 +438,11 @@ void RecordHelper::overrideSpellRecord(const mwmp::SpellRecord& record)
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
if (isExistingId) {
world->updatePtrsWithRefId(recordData.mId); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
} }
void RecordHelper::overrideArmorRecord(const mwmp::ArmorRecord& record) void RecordHelper::overrideArmorRecord(const mwmp::ArmorRecord& record)
@ -430,7 +451,7 @@ void RecordHelper::overrideArmorRecord(const mwmp::ArmorRecord& record)
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
@ -441,7 +462,7 @@ void RecordHelper::overrideArmorRecord(const mwmp::ArmorRecord& record)
{ {
if (!recordData.mEnchant.empty() && !doesEnchantmentRecordExist(recordData.mEnchant)) if (!recordData.mEnchant.empty() && !doesEnchantmentRecordExist(recordData.mEnchant))
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring new armor record with invalid enchantment provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring new armor record with invalid enchantmentId %s", recordData.mEnchant.c_str());
return; return;
} }
else else
@ -479,8 +500,13 @@ void RecordHelper::overrideArmorRecord(const mwmp::ArmorRecord& record)
if (record.baseOverrides.hasArmorRating) if (record.baseOverrides.hasArmorRating)
finalData.mData.mArmor = recordData.mData.mArmor; finalData.mData.mArmor = recordData.mData.mArmor;
if (record.baseOverrides.hasEnchantmentId && doesEnchantmentRecordExist(recordData.mEnchant)) if (record.baseOverrides.hasEnchantmentId)
finalData.mEnchant = recordData.mEnchant; {
if (doesEnchantmentRecordExist(recordData.mEnchant))
finalData.mEnchant = recordData.mEnchant;
else
LOG_APPEND(Log::LOG_INFO, "-- Ignoring invalid enchantmentId %s", recordData.mEnchant.c_str());
}
if (record.baseOverrides.hasEnchantmentCharge) if (record.baseOverrides.hasEnchantmentCharge)
finalData.mData.mEnchant = recordData.mData.mEnchant; finalData.mData.mEnchant = recordData.mData.mEnchant;
@ -493,6 +519,11 @@ void RecordHelper::overrideArmorRecord(const mwmp::ArmorRecord& record)
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
{
LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
if (isExistingId) if (isExistingId)
world->updatePtrsWithRefId(recordData.mId); world->updatePtrsWithRefId(recordData.mId);
@ -504,7 +535,7 @@ void RecordHelper::overrideBookRecord(const mwmp::BookRecord& record)
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
@ -515,7 +546,7 @@ void RecordHelper::overrideBookRecord(const mwmp::BookRecord& record)
{ {
if (!recordData.mEnchant.empty() && !doesEnchantmentRecordExist(recordData.mEnchant)) if (!recordData.mEnchant.empty() && !doesEnchantmentRecordExist(recordData.mEnchant))
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring new book record with invalid enchantment provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring new book record with invalid enchantmentId %s", recordData.mEnchant.c_str());
return; return;
} }
else else
@ -551,8 +582,13 @@ void RecordHelper::overrideBookRecord(const mwmp::BookRecord& record)
if (record.baseOverrides.hasSkillId) if (record.baseOverrides.hasSkillId)
finalData.mData.mSkillId = recordData.mData.mSkillId; finalData.mData.mSkillId = recordData.mData.mSkillId;
if (record.baseOverrides.hasEnchantmentId && doesEnchantmentRecordExist(recordData.mEnchant)) if (record.baseOverrides.hasEnchantmentId)
finalData.mEnchant = recordData.mEnchant; {
if (doesEnchantmentRecordExist(recordData.mEnchant))
finalData.mEnchant = recordData.mEnchant;
else
LOG_APPEND(Log::LOG_INFO, "-- Ignoring invalid enchantmentId %s", recordData.mEnchant.c_str());
}
if (record.baseOverrides.hasEnchantmentCharge) if (record.baseOverrides.hasEnchantmentCharge)
finalData.mData.mEnchant = recordData.mData.mEnchant; finalData.mData.mEnchant = recordData.mData.mEnchant;
@ -562,6 +598,11 @@ void RecordHelper::overrideBookRecord(const mwmp::BookRecord& record)
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
{
LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
if (isExistingId) if (isExistingId)
world->updatePtrsWithRefId(recordData.mId); world->updatePtrsWithRefId(recordData.mId);
@ -573,7 +614,7 @@ void RecordHelper::overrideClothingRecord(const mwmp::ClothingRecord& record)
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
@ -584,7 +625,7 @@ void RecordHelper::overrideClothingRecord(const mwmp::ClothingRecord& record)
{ {
if (!recordData.mEnchant.empty() && !doesEnchantmentRecordExist(recordData.mEnchant)) if (!recordData.mEnchant.empty() && !doesEnchantmentRecordExist(recordData.mEnchant))
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring new clothing record with invalid enchantment provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring new clothing record with invalid enchantmentId %s", recordData.mEnchant.c_str());
return; return;
} }
else else
@ -614,8 +655,13 @@ void RecordHelper::overrideClothingRecord(const mwmp::ClothingRecord& record)
if (record.baseOverrides.hasValue) if (record.baseOverrides.hasValue)
finalData.mData.mValue = recordData.mData.mValue; finalData.mData.mValue = recordData.mData.mValue;
if (record.baseOverrides.hasEnchantmentId && doesEnchantmentRecordExist(recordData.mEnchant)) if (record.baseOverrides.hasEnchantmentId)
finalData.mEnchant = recordData.mEnchant; {
if (doesEnchantmentRecordExist(recordData.mEnchant))
finalData.mEnchant = recordData.mEnchant;
else
LOG_APPEND(Log::LOG_INFO, "-- Ignoring invalid enchantmentId %s", recordData.mEnchant.c_str());
}
if (record.baseOverrides.hasEnchantmentCharge) if (record.baseOverrides.hasEnchantmentCharge)
finalData.mData.mEnchant = recordData.mData.mEnchant; finalData.mData.mEnchant = recordData.mData.mEnchant;
@ -628,6 +674,11 @@ void RecordHelper::overrideClothingRecord(const mwmp::ClothingRecord& record)
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
{
LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
if (isExistingId) if (isExistingId)
world->updatePtrsWithRefId(recordData.mId); world->updatePtrsWithRefId(recordData.mId);
@ -639,7 +690,7 @@ void RecordHelper::overrideMiscellaneousRecord(const mwmp::MiscellaneousRecord&
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
@ -679,6 +730,11 @@ void RecordHelper::overrideMiscellaneousRecord(const mwmp::MiscellaneousRecord&
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
{
LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
if (isExistingId) if (isExistingId)
world->updatePtrsWithRefId(recordData.mId); world->updatePtrsWithRefId(recordData.mId);
@ -690,7 +746,7 @@ void RecordHelper::overrideWeaponRecord(const mwmp::WeaponRecord& record)
if (recordData.mId.empty()) if (recordData.mId.empty())
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring record override with no id provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with no id provided");
return; return;
} }
@ -701,7 +757,7 @@ void RecordHelper::overrideWeaponRecord(const mwmp::WeaponRecord& record)
{ {
if (!recordData.mEnchant.empty() && !doesEnchantmentRecordExist(recordData.mEnchant)) if (!recordData.mEnchant.empty() && !doesEnchantmentRecordExist(recordData.mEnchant))
{ {
LOG_MESSAGE_SIMPLE(Log::LOG_INFO, "Ignoring new weapon record with invalid enchantment provided"); LOG_APPEND(Log::LOG_INFO, "-- Ignoring new weapon record with invalid enchantmentId %s", recordData.mEnchant.c_str());
return; return;
} }
else else
@ -761,8 +817,13 @@ void RecordHelper::overrideWeaponRecord(const mwmp::WeaponRecord& record)
if (record.baseOverrides.hasFlags) if (record.baseOverrides.hasFlags)
finalData.mData.mFlags = recordData.mData.mFlags; finalData.mData.mFlags = recordData.mData.mFlags;
if (record.baseOverrides.hasEnchantmentId && doesEnchantmentRecordExist(recordData.mEnchant)) if (record.baseOverrides.hasEnchantmentId)
finalData.mEnchant = recordData.mEnchant; {
if (doesEnchantmentRecordExist(recordData.mEnchant))
finalData.mEnchant = recordData.mEnchant;
else
LOG_APPEND(Log::LOG_INFO, "-- Ignoring invalid enchantmentId %s", recordData.mEnchant.c_str());
}
if (record.baseOverrides.hasEnchantmentCharge) if (record.baseOverrides.hasEnchantmentCharge)
finalData.mData.mEnchant = recordData.mData.mEnchant; finalData.mData.mEnchant = recordData.mData.mEnchant;
@ -772,6 +833,11 @@ void RecordHelper::overrideWeaponRecord(const mwmp::WeaponRecord& record)
world->getModifiableStore().overrideRecord(finalData); world->getModifiableStore().overrideRecord(finalData);
} }
else
{
LOG_APPEND(Log::LOG_INFO, "-- Ignoring record override with invalid baseId %s", record.baseId.c_str());
return;
}
if (isExistingId) if (isExistingId)
world->updatePtrsWithRefId(recordData.mId); world->updatePtrsWithRefId(recordData.mId);

View file

@ -89,7 +89,7 @@
#include "WorldstateProcessor.hpp" #include "WorldstateProcessor.hpp"
#include "worldstate/ProcessorCellCreate.hpp" #include "worldstate/ProcessorCellCreate.hpp"
#include "worldstate/ProcessorCellReplace.hpp" #include "worldstate/ProcessorCellReset.hpp"
#include "worldstate/ProcessorRecordDynamic.hpp" #include "worldstate/ProcessorRecordDynamic.hpp"
#include "worldstate/ProcessorWorldCollisionOverride.hpp" #include "worldstate/ProcessorWorldCollisionOverride.hpp"
#include "worldstate/ProcessorWorldMap.hpp" #include "worldstate/ProcessorWorldMap.hpp"
@ -186,7 +186,7 @@ void ProcessorInitializer()
ActorProcessor::AddProcessor(new ProcessorActorTest()); ActorProcessor::AddProcessor(new ProcessorActorTest());
WorldstateProcessor::AddProcessor(new ProcessorCellCreate()); WorldstateProcessor::AddProcessor(new ProcessorCellCreate());
WorldstateProcessor::AddProcessor(new ProcessorCellReplace()); WorldstateProcessor::AddProcessor(new ProcessorCellReset());
WorldstateProcessor::AddProcessor(new ProcessorRecordDynamic()); WorldstateProcessor::AddProcessor(new ProcessorRecordDynamic());
WorldstateProcessor::AddProcessor(new ProcessorWorldCollisionOverride()); WorldstateProcessor::AddProcessor(new ProcessorWorldCollisionOverride());
WorldstateProcessor::AddProcessor(new ProcessorWorldMap()); WorldstateProcessor::AddProcessor(new ProcessorWorldMap());

View file

@ -1,23 +0,0 @@
#ifndef OPENMW_PROCESSORCELLREPLACE_HPP
#define OPENMW_PROCESSORCELLREPLACE_HPP
#include "../WorldstateProcessor.hpp"
namespace mwmp
{
class ProcessorCellReplace : public WorldstateProcessor
{
public:
ProcessorCellReplace()
{
BPP_INIT(ID_CELL_REPLACE)
}
virtual void Do(WorldstatePacket &packet, Worldstate &worldstate)
{
// Placeholder
}
};
}
#endif //OPENMW_PROCESSORCELLREPLACE_HPP

View file

@ -0,0 +1,23 @@
#ifndef OPENMW_PROCESSORCELLRESET_HPP
#define OPENMW_PROCESSORCELLRESET_HPP
#include "../WorldstateProcessor.hpp"
namespace mwmp
{
class ProcessorCellReset : public WorldstateProcessor
{
public:
ProcessorCellReset()
{
BPP_INIT(ID_CELL_RESET)
}
virtual void Do(WorldstatePacket &packet, Worldstate &worldstate)
{
// Placeholder
}
};
}
#endif //OPENMW_PROCESSORCELLRESET_HPP

View file

@ -613,13 +613,17 @@ namespace MWScript
Start of tes3mp addition Start of tes3mp addition
Send an ID_OBJECT_STATE packet whenever an object is enabled, as long as Send an ID_OBJECT_STATE packet whenever an object is enabled, as long as
the player is logged in on the server and the object wasn't already the player is logged in on the server, the object is still disabled, and our last
enabled previously packet regarding its state did not already attempt to enable it (to prevent
packet spam)
*/ */
if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) if (mwmp::Main::get().getLocalPlayer()->isLoggedIn())
{ {
if (ref.isInCell() && !ref.getRefData().isEnabled()) if (ref.isInCell() && !ref.getRefData().isEnabled() &&
ref.getRefData().getLastCommunicatedState() != MWWorld::RefData::StateCommunication::Enabled)
{ {
ref.getRefData().setLastCommunicatedState(MWWorld::RefData::StateCommunication::Enabled);
mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList();
objectList->reset(); objectList->reset();
objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(getContextType()); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(getContextType());
@ -650,14 +654,18 @@ namespace MWScript
/* /*
Start of tes3mp addition Start of tes3mp addition
Send an ID_OBJECT_STATE packet whenever an object is disabled, as long as Send an ID_OBJECT_STATE packet whenever an object should be disabled, as long as
the player is logged in on the server and the object wasn't already the player is logged in on the server, the object is still enabled, and our last
disabled previously packet regarding its state did not already attempt to disable it (to prevent
packet spam)
*/ */
if (mwmp::Main::get().getLocalPlayer()->isLoggedIn()) if (mwmp::Main::get().getLocalPlayer()->isLoggedIn())
{ {
if (ref.isInCell() && ref.getRefData().isEnabled()) if (ref.isInCell() && ref.getRefData().isEnabled() &&
ref.getRefData().getLastCommunicatedState() != MWWorld::RefData::StateCommunication::Disabled)
{ {
ref.getRefData().setLastCommunicatedState(MWWorld::RefData::StateCommunication::Disabled);
mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList(); mwmp::ObjectList *objectList = mwmp::Main::get().getNetworking()->getObjectList();
objectList->reset(); objectList->reset();
objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(getContextType()); objectList->packetOrigin = ScriptController::getPacketOriginFromContextType(getContextType());

View file

@ -13,6 +13,7 @@
Include additional headers for multiplayer purposes Include additional headers for multiplayer purposes
*/ */
#include <components/openmw-mp/Log.hpp>
#include "../mwmp/Main.hpp" #include "../mwmp/Main.hpp"
#include "../mwmp/CellController.hpp" #include "../mwmp/CellController.hpp"
#include "../mwmp/PlayerList.hpp" #include "../mwmp/PlayerList.hpp"
@ -251,7 +252,19 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::findSlot (int slot) con
{ {
// Object has been deleted // Object has been deleted
// This should no longer happen, since the new remove function will unequip first // This should no longer happen, since the new remove function will unequip first
throw std::runtime_error("Invalid slot, make sure you are not calling RefData::setCount for a container object");
/*
Start of tes3mp change (major)
Instead of throwing an error, display an error log message with information about
the item
*/
//throw std::runtime_error("Invalid slot, make sure you are not calling RefData::setCount for a container object");
LOG_MESSAGE_SIMPLE(Log::LOG_ERROR, "Invalid slot, make sure you are not calling RefData::setCount for a container object\n- item was %s",
mSlots[slot]->getCellRef().getRefId().c_str());
/*
End of tes3mp change (major)
*/
} }
return mSlots[slot]; return mSlots[slot];

View file

@ -138,6 +138,34 @@ namespace MWWorld
const ESM::AnimationState& getAnimationState() const; const ESM::AnimationState& getAnimationState() const;
ESM::AnimationState& getAnimationState(); ESM::AnimationState& getAnimationState();
/*
Start of tes3mp addition
Track the last state communicated to the server for this reference,
to avoid packet spam when the server denies our state change request or
is slow to reply
*/
enum StateCommunication
{
None = 0,
Enabled = 1,
Disabled = 2
};
private:
short mLastCommunicatedState = StateCommunication::None;
public:
short getLastCommunicatedState() { return mLastCommunicatedState; };
void setLastCommunicatedState(short communicationState) { mLastCommunicatedState = communicationState; };
/*
End of tes3mp addition
*/
}; };
} }

View file

@ -3258,10 +3258,11 @@ namespace MWWorld
If this actor is a LocalPlayer or LocalActor, get their Attack and prepare If this actor is a LocalPlayer or LocalActor, get their Attack and prepare
it for sending it for sending
Set the attack details before going through with the casting, in case it's
a one use item that would get removed through the casting (like a scroll)
*/ */
{ {
cast.cast(*inv.getSelectedEnchantItem());
mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(actor); mwmp::Attack *localAttack = MechanicsHelper::getLocalAttack(actor);
if (localAttack) if (localAttack)
@ -3271,6 +3272,8 @@ namespace MWWorld
localAttack->itemId = inv.getSelectedEnchantItem()->getCellRef().getRefId(); localAttack->itemId = inv.getSelectedEnchantItem()->getCellRef().getRefId();
localAttack->shouldSend = true; localAttack->shouldSend = true;
} }
cast.cast(*inv.getSelectedEnchantItem());
} }
/* /*
End of tes3mp addition End of tes3mp addition

View file

@ -202,7 +202,7 @@ add_component_dir (openmw-mp/Packets/Object
add_component_dir (openmw-mp/Packets/Worldstate add_component_dir (openmw-mp/Packets/Worldstate
WorldstatePacket WorldstatePacket
PacketCellCreate PacketCellReplace PacketRecordDynamic PacketWorldCollisionOverride PacketWorldMap PacketCellCreate PacketCellReset PacketRecordDynamic PacketWorldCollisionOverride PacketWorldMap
PacketWorldRegionAuthority PacketWorldTime PacketWorldWeather PacketWorldRegionAuthority PacketWorldTime PacketWorldWeather
) )

View file

@ -232,6 +232,8 @@ namespace mwmp
} }
RakNet::RakNetGUID guid; RakNet::RakNetGUID guid;
std::string serverPassword;
GUIMessageBox guiMessageBox; GUIMessageBox guiMessageBox;
// Track only the indexes of the attributes that have been changed, // Track only the indexes of the attributes that have been changed,
@ -295,7 +297,6 @@ namespace mwmp
std::string birthsign; std::string birthsign;
std::string chatMessage; std::string chatMessage;
CharGenState charGenState; CharGenState charGenState;
std::string passw;
std::string sound; std::string sound;
Animation animation; Animation animation;

View file

@ -1,5 +1,5 @@
#include "../Packets/Worldstate/PacketCellCreate.hpp" #include "../Packets/Worldstate/PacketCellCreate.hpp"
#include "../Packets/Worldstate/PacketCellReplace.hpp" #include "../Packets/Worldstate/PacketCellReset.hpp"
#include "../Packets/Worldstate/PacketRecordDynamic.hpp" #include "../Packets/Worldstate/PacketRecordDynamic.hpp"
#include "../Packets/Worldstate/PacketWorldCollisionOverride.hpp" #include "../Packets/Worldstate/PacketWorldCollisionOverride.hpp"
#include "../Packets/Worldstate/PacketWorldMap.hpp" #include "../Packets/Worldstate/PacketWorldMap.hpp"
@ -20,7 +20,7 @@ inline void AddPacket(mwmp::WorldstatePacketController::packets_t *packets, RakN
mwmp::WorldstatePacketController::WorldstatePacketController(RakNet::RakPeerInterface *peer) mwmp::WorldstatePacketController::WorldstatePacketController(RakNet::RakPeerInterface *peer)
{ {
AddPacket<PacketCellCreate>(&packets, peer); AddPacket<PacketCellCreate>(&packets, peer);
AddPacket<PacketCellReplace>(&packets, peer); AddPacket<PacketCellReset>(&packets, peer);
AddPacket<PacketRecordDynamic>(&packets, peer); AddPacket<PacketRecordDynamic>(&packets, peer);
AddPacket<PacketWorldCollisionOverride>(&packets, peer); AddPacket<PacketWorldCollisionOverride>(&packets, peer);
AddPacket<PacketWorldMap>(&packets, peer); AddPacket<PacketWorldMap>(&packets, peer);

View file

@ -104,7 +104,7 @@ enum GameMessages
ID_GAME_PREINIT, ID_GAME_PREINIT,
ID_CELL_CREATE, ID_CELL_CREATE,
ID_CELL_REPLACE, ID_CELL_RESET,
ID_RECORD_DYNAMIC, ID_RECORD_DYNAMIC,
ID_WORLD_COLLISION_OVERRIDE, ID_WORLD_COLLISION_OVERRIDE,
ID_WORLD_MAP, ID_WORLD_MAP,

View file

@ -1,7 +1,3 @@
//
// Created by koncord on 28.04.16.
//
#include <components/openmw-mp/NetworkMessages.hpp> #include <components/openmw-mp/NetworkMessages.hpp>
#include "PacketHandshake.hpp" #include "PacketHandshake.hpp"
@ -17,8 +13,8 @@ void PacketHandshake::Packet(RakNet::BitStream *bs, bool send)
{ {
PlayerPacket::Packet(bs, send); PlayerPacket::Packet(bs, send);
if (!RW(player->npc.mName, send, true, maxNameLen) || if (!RW(player->npc.mName, send, true, maxNameLength) ||
!RW(player->passw, send, true, maxPasswLen)) !RW(player->serverPassword, send, true, maxPasswordLength))
{ {
packetValid = false; packetValid = false;
return; return;

View file

@ -16,8 +16,8 @@ namespace mwmp
virtual void Packet(RakNet::BitStream *bs, bool send); virtual void Packet(RakNet::BitStream *bs, bool send);
const static uint32_t maxNameLen = 256; const static uint32_t maxNameLength = 256;
const static uint32_t maxPasswLen = 256; const static uint32_t maxPasswordLength = 256;
}; };
} }

View file

@ -1,17 +0,0 @@
#include "PacketCellReplace.hpp"
#include <components/openmw-mp/NetworkMessages.hpp>
using namespace mwmp;
PacketCellReplace::PacketCellReplace(RakNet::RakPeerInterface *peer) : WorldstatePacket(peer)
{
packetID = ID_CELL_REPLACE;
orderChannel = CHANNEL_SYSTEM;
}
void PacketCellReplace::Packet(RakNet::BitStream *bs, bool send)
{
WorldstatePacket::Packet(bs, send);
// Placeholder
}

View file

@ -0,0 +1,17 @@
#include "PacketCellReset.hpp"
#include <components/openmw-mp/NetworkMessages.hpp>
using namespace mwmp;
PacketCellReset::PacketCellReset(RakNet::RakPeerInterface *peer) : WorldstatePacket(peer)
{
packetID = ID_CELL_RESET;
orderChannel = CHANNEL_SYSTEM;
}
void PacketCellReset::Packet(RakNet::BitStream *bs, bool send)
{
WorldstatePacket::Packet(bs, send);
// Placeholder
}

View file

@ -1,18 +1,18 @@
#ifndef OPENMW_PACKETCELLREPLACE_HPP #ifndef OPENMW_PACKETCELLRESET_HPP
#define OPENMW_PACKETCELLREPLACE_HPP #define OPENMW_PACKETCELLRESET_HPP
#include <components/openmw-mp/Packets/Worldstate/WorldstatePacket.hpp> #include <components/openmw-mp/Packets/Worldstate/WorldstatePacket.hpp>
#include <components/openmw-mp/NetworkMessages.hpp> #include <components/openmw-mp/NetworkMessages.hpp>
namespace mwmp namespace mwmp
{ {
class PacketCellReplace: public WorldstatePacket class PacketCellReset: public WorldstatePacket
{ {
public: public:
PacketCellReplace(RakNet::RakPeerInterface *peer); PacketCellReset(RakNet::RakPeerInterface *peer);
virtual void Packet(RakNet::BitStream *bs, bool send); virtual void Packet(RakNet::BitStream *bs, bool send);
}; };
} }
#endif //OPENMW_PACKETCELLREPLACE_HPP #endif //OPENMW_PACKETCELLRESET_HPP

View file

@ -192,15 +192,16 @@ std::string Utils::getArchitectureType()
{ {
#if defined(__x86_64__) || defined(_M_X64) #if defined(__x86_64__) || defined(_M_X64)
return "64-bit"; return "64-bit";
#elif defined(__i386__) || defined(_M_I86) #elif defined(__i386__) || defined(_M_I86) || defined(_M_IX86)
return "32-bit"; return "32-bit";
#elif defined(__ARM_ARCH) #elif defined(__ARM_ARCH)
return "ARMv" + __ARM_ARCH; std::string architectureType = "ARMv" + __ARM_ARCH;
#ifdef __aarch64__ #ifdef __aarch64__
return "64-bit"; architectureType = architectureType + " 64-bit";
#else #else
return "32-bit"; architectureType = architectureType + " 32-bit";
#endif #endif
return architectureType;
#else #else
return "Unknown architecture"; return "Unknown architecture";
#endif #endif
@ -211,7 +212,7 @@ void Utils::printVersion(std::string appName, std::string version, std::string c
cout << appName << " " << version; cout << appName << " " << version;
cout << " (" << getOperatingSystemType() << " " << getArchitectureType() << ")" << endl; cout << " (" << getOperatingSystemType() << " " << getArchitectureType() << ")" << endl;
cout << "Protocol version: " << protocol << endl; cout << "Protocol version: " << protocol << endl;
cout << "Commit hash: " << commitHash.substr(0, 10) << endl; cout << "Oldest compatible commit hash: " << commitHash.substr(0, 10) << endl;
cout << "------------------------------------------------------------" << endl; cout << "------------------------------------------------------------" << endl;
} }

View file

@ -7,6 +7,7 @@
#define TES3MP_DEFAULT_PASSW "SuperPassword" #define TES3MP_DEFAULT_PASSW "SuperPassword"
#define TES3MP_MASTERSERVER_PASSW "12345" #define TES3MP_MASTERSERVER_PASSW "12345"
#define TES3MP_CREDITS_CHECKSUM "BC39D2E9" #define TES3MP_CREDITS_CHECKSUM "BAEFF920"
#endif //OPENMW_VERSION_HPP #endif //OPENMW_VERSION_HPP

View file

@ -4,14 +4,14 @@ tes3mp Credits
Programmers Programmers
---------------- ----------------
Stanislav Zhukov (Koncord) - Overall architecture, networking & scripting systems, player sync, server browser & master server Stanislav Zhukov (Koncord) - Architecture, networking & scripting systems, player sync, server browser & master server
David Cernat - World, NPC & quest sync, player sync improvements, state saving & loading, extensive bug fixes David Cernat - World, NPC & quest sync, player sync improvements, state saving & loading, extensive bug fixes
Additional programming Additional programming
---------------------- ----------------------
Grim Kriegor - Lua teleportation commands, early script fixes Grim Kriegor - Linux deployment scripts, Lua teleportation commands, early script fixes
Battlerax - Various small fixes Battlerax - Various small fixes
@ -26,6 +26,7 @@ Community administrators
Community moderators Community moderators
-------------------- --------------------
Lysol
Michael Fitzmayer (mupf) Michael Fitzmayer (mupf)
Nac Nac
NicholasAH NicholasAH
@ -53,6 +54,7 @@ Translation
Super special thanks Super special thanks
-------------------- --------------------
Alexander Ovsyannikov
Bret Curtis (psi29a) Bret Curtis (psi29a)
Gabriel Pascu (iGrebla) Gabriel Pascu (iGrebla)
greetasdf greetasdf
@ -73,8 +75,9 @@ Special thanks
Gluka Gluka
Goodevil Goodevil
Ignatious Ignatious
James Wards of Gore Corps LAN Club (www.gorecorps.co.nz) James Wards of Gore Corps LAN Club (gorecorps.co.nz)
Jeremiah Jeremiah
Kyle Willey of Loreshaper Games (steempeak.com/@loreshapergames)
Lewis Sadlier Lewis Sadlier
Luc Keating Luc Keating
Michael Zagar (Zoops) Michael Zagar (Zoops)
@ -84,6 +87,7 @@ Special thanks
Scorcio Scorcio
Simon Nemes Simon Nemes
Texafornian Texafornian
Thrud
Zaphida Zaphida
All the developers of OpenMW for creating an amazing open source project All the developers of OpenMW for creating an amazing open source project