Merge pull request #413 from TES3MP/0.6.3 while resolving conflicts

Conflicts:
	apps/openmw-mp/processors/player/ProcessorPlayerCellChange.hpp
	components/openmw-mp/Base/BasePlayer.hpp
	components/openmw-mp/Packets/Player/PacketPlayerEquipment.hpp
	components/openmw-mp/Packets/Player/PacketPlayerSkill.cpp
sol2-server-rewrite
David Cernat 7 years ago
commit 502751cae0

@ -103,6 +103,10 @@ To be able to merge PRs, commit priviledges are required. If you do not have the
The person to merge the PR may either use github's Merge button or if using the command line, use the ```--no-ff``` flag (so a merge commit is created, just like with Github's merge button) and include the pull request number in the commit description. The person to merge the PR may either use github's Merge button or if using the command line, use the ```--no-ff``` flag (so a merge commit is created, just like with Github's merge button) and include the pull request number in the commit description.
Dealing with regressions
========================
The master branch should always be in a working state that is not worse than the previous release in any way. If a regression is found, the first and foremost priority should be to get the regression fixed quickly, either by reverting the change that caused it or finding a better solution. Please avoid leaving the project in the 'broken' state for an extensive period of time while proper solutions are found. If the solution takes more than a day or so then it is usually better to revert the offending change first and reapply it later when fixed.
Other resources Other resources
=============== ===============

@ -29,10 +29,14 @@ namespace mwmp
{ {
LOG_APPEND(Log::LOG_INFO, "- Moved to %s", player->cell.getDescription().c_str()); LOG_APPEND(Log::LOG_INFO, "- Moved to %s", player->cell.getDescription().c_str());
player->exchangeFullInfo = true;
player->forEachLoaded([this](Player *pl, Player *other) { player->forEachLoaded([this](Player *pl, Player *other) {
LOG_APPEND(Log::LOG_INFO, "- Started information exchange with %s", other->npc.mName.c_str()); LOG_APPEND(Log::LOG_INFO, "- Started information exchange with %s", other->npc.mName.c_str());
other->exchangeFullInfo = true;
playerController->GetPacket(ID_PLAYER_STATS_DYNAMIC)->setPlayer(other); playerController->GetPacket(ID_PLAYER_STATS_DYNAMIC)->setPlayer(other);
playerController->GetPacket(ID_PLAYER_ATTRIBUTE)->setPlayer(other); playerController->GetPacket(ID_PLAYER_ATTRIBUTE)->setPlayer(other);
playerController->GetPacket(ID_PLAYER_POSITION)->setPlayer(other); playerController->GetPacket(ID_PLAYER_POSITION)->setPlayer(other);
@ -63,6 +67,8 @@ namespace mwmp
playerController->GetPacket(ID_PLAYER_ANIM_FLAGS)->Send(other->guid); playerController->GetPacket(ID_PLAYER_ANIM_FLAGS)->Send(other->guid);
playerController->GetPacket(ID_PLAYER_SHAPESHIFT)->Send(other->guid); playerController->GetPacket(ID_PLAYER_SHAPESHIFT)->Send(other->guid);
other->exchangeFullInfo = false;
LOG_APPEND(Log::LOG_INFO, "- Finished information exchange with %s", other->npc.mName.c_str()); LOG_APPEND(Log::LOG_INFO, "- Finished information exchange with %s", other->npc.mName.c_str());
}); });
@ -74,6 +80,8 @@ namespace mwmp
Networking::get().getState().getEventCtrl().Call<CoreEvent::ON_PLAYER_CELLCHANGE>(player.get()); Networking::get().getState().getEventCtrl().Call<CoreEvent::ON_PLAYER_CELLCHANGE>(player.get());
LOG_APPEND(Log::LOG_INFO, "- Finished processing ID_PLAYER_CELL_CHANGE"); LOG_APPEND(Log::LOG_INFO, "- Finished processing ID_PLAYER_CELL_CHANGE");
player->exchangeFullInfo = false;
} }
else else
LOG_APPEND(Log::LOG_INFO, "- Ignored because %s is dead", player->npc.mName.c_str()); LOG_APPEND(Log::LOG_INFO, "- Ignored because %s is dead", player->npc.mName.c_str());

@ -404,7 +404,12 @@ namespace MWGui
MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player);
MWMechanics::Spells& spells = stats.getSpells(); MWMechanics::Spells& spells = stats.getSpells();
if (!spells.hasSpell(spellId)) if (!spells.hasSpell(spellId))
{
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().find(spellId);
MWBase::Environment::get().getWindowManager()->messageBox (
"#{sQuickMenu5} " + spell->mName);
return; return;
}
store.setSelectedEnchantItem(store.end()); store.setSelectedEnchantItem(store.end());
MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player)));
MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell);

@ -302,7 +302,7 @@ void CharacterController::refreshHitRecoilAnims()
} }
else if (recovery) else if (recovery)
{ {
std::string anim = isSwimming ? chooseRandomGroup("swimhit") : chooseRandomGroup("hit"); std::string anim = chooseRandomGroup("swimhit");
if (isSwimming && mAnimation->hasAnimation(anim)) if (isSwimming && mAnimation->hasAnimation(anim))
{ {
mHitState = CharState_SwimHit; mHitState = CharState_SwimHit;

@ -194,6 +194,9 @@ bool LocalPlayer::hasFinishedCharGen()
void LocalPlayer::updateStatsDynamic(bool forceUpdate) void LocalPlayer::updateStatsDynamic(bool forceUpdate)
{ {
if (statsDynamicIndexChanges.size() > 0)
statsDynamicIndexChanges.clear();
MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::Ptr ptrPlayer = getPlayerPtr();
MWMechanics::CreatureStats *ptrCreatureStats = &ptrPlayer.getClass().getCreatureStats(ptrPlayer); MWMechanics::CreatureStats *ptrCreatureStats = &ptrPlayer.getClass().getCreatureStats(ptrPlayer);
@ -231,9 +234,9 @@ void LocalPlayer::updateStatsDynamic(bool forceUpdate)
magicka.writeState(creatureStats.mDynamic[1]); magicka.writeState(creatureStats.mDynamic[1]);
fatigue.writeState(creatureStats.mDynamic[2]); fatigue.writeState(creatureStats.mDynamic[2]);
exchangeFullInfo = false;
getNetworking()->getPlayerPacket(ID_PLAYER_STATS_DYNAMIC)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_STATS_DYNAMIC)->setPlayer(this);
getNetworking()->getPlayerPacket(ID_PLAYER_STATS_DYNAMIC)->Send(); getNetworking()->getPlayerPacket(ID_PLAYER_STATS_DYNAMIC)->Send();
statsDynamicIndexChanges.clear();
} }
} }
@ -243,6 +246,9 @@ void LocalPlayer::updateAttributes(bool forceUpdate)
// overwritten by the werewolf ones // overwritten by the werewolf ones
if (isWerewolf) return; if (isWerewolf) return;
if (attributeIndexChanges.size() > 0)
attributeIndexChanges.clear();
MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::Ptr ptrPlayer = getPlayerPtr();
const MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); const MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer);
@ -261,9 +267,9 @@ void LocalPlayer::updateAttributes(bool forceUpdate)
if (attributeIndexChanges.size() > 0) if (attributeIndexChanges.size() > 0)
{ {
exchangeFullInfo = false;
getNetworking()->getPlayerPacket(ID_PLAYER_ATTRIBUTE)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_ATTRIBUTE)->setPlayer(this);
getNetworking()->getPlayerPacket(ID_PLAYER_ATTRIBUTE)->Send(); getNetworking()->getPlayerPacket(ID_PLAYER_ATTRIBUTE)->Send();
attributeIndexChanges.clear();
} }
} }
@ -273,6 +279,9 @@ void LocalPlayer::updateSkills(bool forceUpdate)
// overwritten by the werewolf ones // overwritten by the werewolf ones
if (isWerewolf) return; if (isWerewolf) return;
if (skillIndexChanges.size() > 0)
skillIndexChanges.clear();
MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::Ptr ptrPlayer = getPlayerPtr();
const MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer); const MWMechanics::NpcStats &ptrNpcStats = ptrPlayer.getClass().getNpcStats(ptrPlayer);
@ -291,9 +300,9 @@ void LocalPlayer::updateSkills(bool forceUpdate)
if (skillIndexChanges.size() > 0) if (skillIndexChanges.size() > 0)
{ {
exchangeFullInfo = false;
getNetworking()->getPlayerPacket(ID_PLAYER_SKILL)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_SKILL)->setPlayer(this);
getNetworking()->getPlayerPacket(ID_PLAYER_SKILL)->Send(); getNetworking()->getPlayerPacket(ID_PLAYER_SKILL)->Send();
skillIndexChanges.clear();
} }
} }
@ -432,6 +441,9 @@ void LocalPlayer::updateCell(bool forceUpdate)
void LocalPlayer::updateEquipment(bool forceUpdate) void LocalPlayer::updateEquipment(bool forceUpdate)
{ {
if (equipmentIndexChanges.size() > 0)
equipmentIndexChanges.clear();
MWWorld::Ptr ptrPlayer = getPlayerPtr(); MWWorld::Ptr ptrPlayer = getPlayerPtr();
MWWorld::InventoryStore &invStore = ptrPlayer.getClass().getInventoryStore(ptrPlayer); MWWorld::InventoryStore &invStore = ptrPlayer.getClass().getInventoryStore(ptrPlayer);
@ -442,7 +454,11 @@ void LocalPlayer::updateEquipment(bool forceUpdate)
if (it != invStore.end()) if (it != invStore.end())
{ {
if (!::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), equipmentItems[slot].refId) || forceUpdate) if (!::Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), equipmentItems[slot].refId) ||
it->getCellRef().getCharge() != item.charge ||
it->getCellRef().getEnchantmentCharge() != item.enchantmentCharge ||
it->getRefData().getCount() != item.count ||
forceUpdate)
{ {
equipmentIndexChanges.push_back(slot); equipmentIndexChanges.push_back(slot);
item.refId = it->getCellRef().getRefId(); item.refId = it->getCellRef().getRefId();
@ -463,9 +479,9 @@ void LocalPlayer::updateEquipment(bool forceUpdate)
if (!equipmentIndexChanges.empty()) if (!equipmentIndexChanges.empty())
{ {
exchangeFullInfo = false;
getNetworking()->getPlayerPacket(ID_PLAYER_EQUIPMENT)->setPlayer(this); getNetworking()->getPlayerPacket(ID_PLAYER_EQUIPMENT)->setPlayer(this);
getNetworking()->getPlayerPacket(ID_PLAYER_EQUIPMENT)->Send(); getNetworking()->getPlayerPacket(ID_PLAYER_EQUIPMENT)->Send();
equipmentIndexChanges.clear();
} }
} }

@ -574,6 +574,7 @@ namespace MWScript
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), itemID, 1); MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), itemID, 1);
MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), actor, actor.getCell(), direction, distance); MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), actor, actor.getCell(), direction, distance);
MWBase::Environment::get().getWorld()->scaleObject(ptr, actor.getCellRef().getScale());
/* /*
Start of tes3mp addition Start of tes3mp addition

@ -393,32 +393,8 @@ namespace MWSound
void SoundManager::startRandomTitle() void SoundManager::startRandomTitle()
{ {
std::vector<std::string> filelist; const std::vector<std::string> &filelist = mMusicFiles[mCurrentPlaylist];
auto &tracklist = mMusicToPlay[mCurrentPlaylist]; auto &tracklist = mMusicToPlay[mCurrentPlaylist];
if (mMusicFiles.find(mCurrentPlaylist) == mMusicFiles.end())
{
const std::map<std::string, VFS::File*>& index = mVFS->getIndex();
std::string pattern = "Music/" + mCurrentPlaylist;
mVFS->normalizeFilename(pattern);
std::map<std::string, VFS::File*>::const_iterator found = index.lower_bound(pattern);
while (found != index.end())
{
if (found->first.size() >= pattern.size() && found->first.substr(0, pattern.size()) == pattern)
filelist.push_back(found->first);
else
break;
++found;
}
mMusicFiles[mCurrentPlaylist] = filelist;
}
else
filelist = mMusicFiles[mCurrentPlaylist];
if(filelist.empty())
return;
// Do a Fisher-Yates shuffle // Do a Fisher-Yates shuffle
@ -454,6 +430,33 @@ namespace MWSound
void SoundManager::playPlaylist(const std::string &playlist) void SoundManager::playPlaylist(const std::string &playlist)
{ {
if (mCurrentPlaylist == playlist)
return;
if (mMusicFiles.find(playlist) == mMusicFiles.end())
{
std::vector<std::string> filelist;
const std::map<std::string, VFS::File*>& index = mVFS->getIndex();
std::string pattern = "Music/" + playlist;
mVFS->normalizeFilename(pattern);
std::map<std::string, VFS::File*>::const_iterator found = index.lower_bound(pattern);
while (found != index.end())
{
if (found->first.size() >= pattern.size() && found->first.substr(0, pattern.size()) == pattern)
filelist.push_back(found->first);
else
break;
++found;
}
mMusicFiles[playlist] = filelist;
}
if (mMusicFiles[playlist].empty())
return;
mCurrentPlaylist = playlist; mCurrentPlaylist = playlist;
startRandomTitle(); startRandomTitle();
} }

@ -1,7 +1,5 @@
#include "fallback.hpp" #include "fallback.hpp"
#include <iostream>
#include <boost/lexical_cast.hpp> #include <boost/lexical_cast.hpp>
@ -19,7 +17,6 @@ namespace Fallback
std::map<std::string,std::string>::const_iterator it; std::map<std::string,std::string>::const_iterator it;
if((it = mFallbackMap.find(fall)) == mFallbackMap.end()) if((it = mFallbackMap.find(fall)) == mFallbackMap.end())
{ {
std::cerr << "Warning: fallback value " << fall << " not found." << std::endl;
return ""; return "";
} }
return it->second; return it->second;

@ -63,28 +63,16 @@ std::string Misc::ResourceHelpers::correctResourcePath(const std::string &topLev
std::string origExt = correctedPath; std::string origExt = correctedPath;
// since we know all (GOTY edition or less) textures end if (vfs->exists(origExt)
// in .dds, we change the extension || (changeExtensionToDds(correctedPath) && vfs->exists(correctedPath)))
bool changedToDds = changeExtensionToDds(correctedPath);
if (vfs->exists(correctedPath))
return correctedPath; return correctedPath;
// if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods)
// verify, and revert if false (this call succeeds quickly, but fails slowly)
if (changedToDds && vfs->exists(origExt))
return origExt;
// fall back to a resource in the top level directory if it exists // fall back to a resource in the top level directory if it exists
std::string fallback = topLevelDirectory + "\\" + getBasename(correctedPath); std::string fallback = topLevelDirectory + "\\" + getBasename(origExt);
if (vfs->exists(fallback)) if (vfs->exists(fallback)
|| (changeExtensionToDds(fallback) && vfs->exists(fallback)))
return fallback; return fallback;
if (changedToDds)
{
fallback = topLevelDirectory + "\\" + getBasename(origExt);
if (vfs->exists(fallback))
return fallback;
}
return correctedPath; return correctedPath;
} }

@ -1,28 +1,31 @@
#include "rng.hpp" #include "rng.hpp"
#include <cstdlib>
#include <ctime> #include <chrono>
#include <random>
namespace Misc namespace Misc
{ {
std::mt19937 Rng::generator = std::mt19937();
void Rng::init() void Rng::init()
{ {
std::srand(static_cast<unsigned int>(std::time(NULL))); generator.seed(static_cast<unsigned int>(std::chrono::high_resolution_clock::now().time_since_epoch().count()));
} }
float Rng::rollProbability() float Rng::rollProbability()
{ {
return static_cast<float>(std::rand() / (static_cast<double>(RAND_MAX)+1.0)); return std::uniform_real_distribution<float>(0, 1 - std::numeric_limits<float>::epsilon())(generator);
} }
float Rng::rollClosedProbability() float Rng::rollClosedProbability()
{ {
return static_cast<float>(std::rand() / static_cast<double>(RAND_MAX)); return std::uniform_real_distribution<float>(0, 1)(generator);
} }
int Rng::rollDice(int max) int Rng::rollDice(int max)
{ {
return static_cast<int>((std::rand() / (static_cast<double>(RAND_MAX)+1.0)) * (max)); return max > 0 ? std::uniform_int_distribution<int>(0, max - 1)(generator) : 0;
} }
} }

@ -2,6 +2,7 @@
#define OPENMW_COMPONENTS_MISC_RNG_H #define OPENMW_COMPONENTS_MISC_RNG_H
#include <cassert> #include <cassert>
#include <random>
namespace Misc namespace Misc
{ {
@ -13,6 +14,9 @@ class Rng
{ {
public: public:
/// create a RNG
static std::mt19937 generator;
/// seed the RNG /// seed the RNG
static void init(); static void init();

@ -256,6 +256,7 @@ namespace mwmp
{ {
spellbookChanges.action = SpellbookChanges::Type::None; spellbookChanges.action = SpellbookChanges::Type::None;
isWerewolf = false; isWerewolf = false;
exchangeFullInfo = false;
displayCreatureName = false; displayCreatureName = false;
resetStats = false; resetStats = false;
enforcedLogLevel = -1; enforcedLogLevel = -1;
@ -286,6 +287,8 @@ namespace mwmp
// with the skill values themselves being stored in npcStats.mSkills // with the skill values themselves being stored in npcStats.mSkills
std::vector<int> skillIndexChanges; std::vector<int> skillIndexChanges;
bool exchangeFullInfo;
SpellbookChanges spellbookChanges; SpellbookChanges spellbookChanges;
QuickKeyChanges quickKeyChanges; QuickKeyChanges quickKeyChanges;
JournalChanges journalChanges; JournalChanges journalChanges;

@ -13,7 +13,17 @@ void PacketPlayerAttribute::Packet(RakNet::BitStream *bs, bool send)
{ {
PlayerPacket::Packet(bs, send); PlayerPacket::Packet(bs, send);
RW(player->exchangeFullInfo, send);
if (player->exchangeFullInfo)
{
RW(player->creatureStats.mAttributes, send);
RW(player->npcStats.mSkillIncrease, send);
}
else
{
uint32_t count; uint32_t count;
if (send) if (send)
count = static_cast<uint32_t>(player->attributeIndexChanges.size()); count = static_cast<uint32_t>(player->attributeIndexChanges.size());
@ -33,3 +43,4 @@ void PacketPlayerAttribute::Packet(RakNet::BitStream *bs, bool send)
RW(player->npcStats.mSkillIncrease[attributeIndex], send); RW(player->npcStats.mSkillIncrease[attributeIndex], send);
} }
} }
}

@ -1,4 +1,5 @@
#include "PacketPlayerEquipment.hpp" #include "PacketPlayerEquipment.hpp"
#include <components/openmw-mp/NetworkMessages.hpp> #include <components/openmw-mp/NetworkMessages.hpp>
using namespace mwmp; using namespace mwmp;
@ -12,6 +13,17 @@ void PacketPlayerEquipment::Packet(RakNet::BitStream *bs, bool send)
{ {
PlayerPacket::Packet(bs, send); PlayerPacket::Packet(bs, send);
RW(player->exchangeFullInfo, send);
if (player->exchangeFullInfo)
{
for (auto &&equipmentItem : player->equipmentItems)
{
ExchangeItemInformation(equipmentItem, send);
}
}
else
{
uint32_t count; uint32_t count;
if (send) if (send)
count = static_cast<uint32_t>(player->equipmentIndexChanges.size()); count = static_cast<uint32_t>(player->equipmentIndexChanges.size());
@ -27,10 +39,16 @@ void PacketPlayerEquipment::Packet(RakNet::BitStream *bs, bool send)
for (auto &&equipmentIndex : player->equipmentIndexChanges) for (auto &&equipmentIndex : player->equipmentIndexChanges)
{ {
RW(equipmentIndex, send); RW(equipmentIndex, send);
ExchangeItemInformation(player->equipmentItems[equipmentIndex], send);
RW(player->equipmentItems[equipmentIndex].refId, send); }
RW(player->equipmentItems[equipmentIndex].count, send); }
RW(player->equipmentItems[equipmentIndex].charge, send);
RW(player->equipmentItems[equipmentIndex].enchantmentCharge, send);
} }
void PacketPlayerEquipment::ExchangeItemInformation(Item &item, bool send)
{
RW(item.refId, send);
RW(item.count, send);
RW(item.charge, send);
RW(item.enchantmentCharge, send);
} }

@ -15,6 +15,7 @@ namespace mwmp
PacketPlayerEquipment(RakNet::RakPeerInterface *peer); PacketPlayerEquipment(RakNet::RakPeerInterface *peer);
void Packet(RakNet::BitStream *bs, bool send) override; void Packet(RakNet::BitStream *bs, bool send) override;
void ExchangeItemInformation(Item &item, bool send);
}; };
} }

@ -14,6 +14,14 @@ void PacketPlayerSkill::Packet(RakNet::BitStream *bs, bool send)
{ {
PlayerPacket::Packet(bs, send); PlayerPacket::Packet(bs, send);
RW(player->exchangeFullInfo, send);
if (player->exchangeFullInfo)
{
RW(player->npcStats.mSkills, send);
}
else
{
uint32_t count; uint32_t count;
if (send) if (send)
@ -33,3 +41,4 @@ void PacketPlayerSkill::Packet(RakNet::BitStream *bs, bool send)
RW(player->npcStats.mSkills[skillId], send); RW(player->npcStats.mSkills[skillId], send);
} }
} }
}

@ -1,4 +1,5 @@
#include "PacketPlayerStatsDynamic.hpp" #include "PacketPlayerStatsDynamic.hpp"
#include <components/openmw-mp/NetworkMessages.hpp> #include <components/openmw-mp/NetworkMessages.hpp>
using namespace mwmp; using namespace mwmp;
@ -12,7 +13,16 @@ void PacketPlayerStatsDynamic::Packet(RakNet::BitStream *bs, bool send)
{ {
PlayerPacket::Packet(bs, send); PlayerPacket::Packet(bs, send);
RW(player->exchangeFullInfo, send);
if (player->exchangeFullInfo)
{
RW(player->creatureStats.mDynamic, send);
}
else
{
uint32_t count; uint32_t count;
if (send) if (send)
count = static_cast<uint32_t>(player->statsDynamicIndexChanges.size()); count = static_cast<uint32_t>(player->statsDynamicIndexChanges.size());
@ -31,3 +41,4 @@ void PacketPlayerStatsDynamic::Packet(RakNet::BitStream *bs, bool send)
RW(player->creatureStats.mDynamic[statsDynamicIndex], send); RW(player->creatureStats.mDynamic[statsDynamicIndex], send);
} }
} }
}

@ -3,7 +3,7 @@
Copyright 2015 Alexandre Moine <nobrakal@gmail.com> Copyright 2015 Alexandre Moine <nobrakal@gmail.com>
Copyright 2017 Bret Curtis <psi29a@gmail.com> Copyright 2017 Bret Curtis <psi29a@gmail.com>
--> -->
<component> <component type="desktop">
<id>org.openmw.desktop</id> <id>org.openmw.desktop</id>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0 and MIT</project_license> <project_license>GPL-3.0 and MIT</project_license>
@ -20,7 +20,6 @@ Copyright 2017 Bret Curtis <psi29a@gmail.com>
You will still need the original game data to play OpenMW. You will still need the original game data to play OpenMW.
</p> </p>
</description> </description>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://wiki.openmw.org/images/b/b2/Openmw_0.11.1_launcher_1.png</image> <image>https://wiki.openmw.org/images/b/b2/Openmw_0.11.1_launcher_1.png</image>
@ -34,7 +33,18 @@ Copyright 2017 Bret Curtis <psi29a@gmail.com>
<image>https://wiki.openmw.org/images/5/5b/Screenshot_Vivec_seen_from_Ebonheart_0.35.png</image> <image>https://wiki.openmw.org/images/5/5b/Screenshot_Vivec_seen_from_Ebonheart_0.35.png</image>
<caption>Vivec seen from Ebonheart on OpenMW</caption> <caption>Vivec seen from Ebonheart on OpenMW</caption>
</screenshot> </screenshot>
<screenshot>
<image>http://wiki.openmw.org/images/a/a3/0.40_Screenshot-Balmora_3.png</image>
<caption>Balmora at morning on OpenMW</caption>
</screenshot>
</screenshots> </screenshots>
<categories>
<category>Game</category>
<category>RolePlaying</category>
</categories>
<releases>
<release version="0.43.0" date="2017-12-05"/>
</releases>
<url type="homepage">https://openmw.org</url> <url type="homepage">https://openmw.org</url>
<url type="bugtracker">https://bugs.openmw.org/</url> <url type="bugtracker">https://bugs.openmw.org/</url>
<url type="faq">https://openmw.org/faq/</url> <url type="faq">https://openmw.org/faq/</url>

Loading…
Cancel
Save