refactor: Refactor multiplayer, optimize message exchange

This commit is contained in:
Vsevolod Kremianskii 2020-08-16 20:29:51 +07:00
parent 2e73eca295
commit 64b8ad60fd
23 changed files with 578 additions and 282 deletions

View file

@ -68,6 +68,7 @@ set(HEADERS
src/game/multiplayer/door.h
src/game/multiplayer/game.h
src/game/multiplayer/module.h
src/game/multiplayer/util.h
src/game/object/creature.h
src/game/object/door.h
src/game/object/object.h
@ -91,6 +92,7 @@ set(HEADERS
src/gui/types.h
src/net/client.h
src/net/connection.h
src/net/command.h
src/net/server.h
src/net/types.h
src/program.h
@ -170,6 +172,7 @@ set(SOURCES
src/game/multiplayer/door.cpp
src/game/multiplayer/game.cpp
src/game/multiplayer/module.cpp
src/game/multiplayer/util.cpp
src/game/navmesh.cpp
src/game/object/creature.cpp
src/game/object/door.cpp
@ -195,6 +198,7 @@ set(SOURCES
src/main.cpp
src/net/client.cpp
src/net/connection.cpp
src/net/command.cpp
src/net/server.cpp
src/program.cpp
src/render/aabb.cpp

View file

@ -85,11 +85,13 @@ void Area::load(const GffStruct &are, const GffStruct &git) {
shared_ptr<Creature> creature(makeCreature());
creature->load(gffs);
landObject(*creature);
creature->setSynchronize(true);
_objects[ObjectType::Creature].push_back(move(creature));
}
for (auto &gffs : git.getList("Door List")) {
shared_ptr<Door> door(makeDoor());
door->load(gffs);
door->setSynchronize(true);
_objects[ObjectType::Door].push_back(move(door));
}
for (auto &gffs : git.getList("Placeable List")) {
@ -118,8 +120,8 @@ void Area::load(const GffStruct &are, const GffStruct &git) {
});
}
shared_ptr<Creature> Area::makeCreature() {
return make_unique<Creature>(_idCounter++);
shared_ptr<Creature> Area::makeCreature(uint32_t id) {
return make_unique<Creature>(id > 0 ? id : _idCounter++);
}
shared_ptr<Door> Area::makeDoor() {
@ -204,15 +206,19 @@ void Area::landObject(Object &object) {
}
void Area::loadParty(const PartyConfiguration &party, const glm::vec3 &position, float heading) {
shared_ptr<Creature> partyLeader(makeCharacter(party.leader, kPartyLeaderTag, position, heading));
_objects[ObjectType::Creature].push_back(partyLeader);
landObject(*partyLeader);
_player = partyLeader;
_partyLeader = partyLeader;
if (party.memberCount > 0) {
shared_ptr<Creature> partyLeader(makeCharacter(party.leader, kPartyLeaderTag, position, heading));
_objects[ObjectType::Creature].push_back(partyLeader);
landObject(*partyLeader);
partyLeader->setSynchronize(true);
_player = partyLeader;
_partyLeader = partyLeader;
}
if (party.memberCount > 1) {
shared_ptr<Creature> partyMember(makeCharacter(party.member1, kPartyMember1Tag, position, heading));
landObject(*partyMember);
partyMember->setSynchronize(true);
_objects[ObjectType::Creature].push_back(partyMember);
_partyMember1 = partyMember;
@ -220,9 +226,10 @@ void Area::loadParty(const PartyConfiguration &party, const glm::vec3 &position,
partyMember->enqueueAction(move(action));
}
if (party.memberCount > 1) {
if (party.memberCount > 2) {
shared_ptr<Creature> partyMember(makeCharacter(party.member2, kPartyMember2Tag, position, heading));
landObject(*partyMember);
partyMember->setSynchronize(true);
_objects[ObjectType::Creature].push_back(partyMember);
_partyMember2 = partyMember;

View file

@ -114,7 +114,7 @@ protected:
void landObject(Object &object);
virtual std::shared_ptr<Creature> makeCreature();
virtual std::shared_ptr<Creature> makeCreature(uint32_t id = 0);
virtual std::shared_ptr<Door> makeDoor();
virtual std::shared_ptr<Placeable> makePlaceable();
virtual std::shared_ptr<Waypoint> makeWaypoint();

View file

@ -124,7 +124,7 @@ void Game::loadMainMenu() {
mainMenu->setOnExit([this]() { _quit = true; });
mainMenu->setOnModuleSelected([this](const string &name) {
PartyConfiguration party;
party.memberCount = 1;
party.memberCount = 2;
party.leader.equipment.push_back("g_a_clothes01");
party.member1.equipment.push_back("g_a_clothes01");
@ -170,6 +170,7 @@ void Game::loadPortraitsGui() {
string moduleName(_version == GameVersion::KotOR ? "end_m01aa" : "001ebo");
PartyConfiguration party;
party.memberCount = 1;
party.leader = character;
loadModule(moduleName, party);

View file

@ -168,12 +168,16 @@ void Module::getEntryPoint(const string &waypoint, glm::vec3 &position, float &h
}
void Module::update3rdPersonCameraTarget() {
Object *player = _area->player().get();
shared_ptr<Object> player(_area->player());
if (!player) return;
_thirdPersonCamera->setTargetPosition(player->position() + player->model()->getNodeAbsolutePosition("camerahook"));
}
void Module::update3rdPersonCameraHeading() {
Object *player = _area->player().get();
shared_ptr<Object> player(_area->player());
if (!player) return;
_thirdPersonCamera->setHeading(player->heading());
}
@ -364,7 +368,10 @@ void Module::updatePlayer(float dt) {
if (_cameraType != CameraType::ThirdPerson) return;
ThirdPersonCamera &camera = static_cast<ThirdPersonCamera &>(*_thirdPersonCamera);
Creature *player = static_cast<Creature *>(_area->player().get());
shared_ptr<Object> playerObject(_area->player());
if (!playerObject) return;
Creature &player = static_cast<Creature &>(*playerObject);
float heading = 0.0f;
bool movement = false;
@ -378,16 +385,16 @@ void Module::updatePlayer(float dt) {
}
if (movement) {
glm::vec3 target(player->position());
glm::vec3 target(player.position());
target.x -= 100.0f * glm::sin(heading);
target.y += 100.0f * glm::cos(heading);
if (_area->moveCreatureTowards(*player, target, dt)) {
player->setMovementType(MovementType::Run);
if (_area->moveCreatureTowards(player, target, dt)) {
player.setMovementType(MovementType::Run);
update3rdPersonCameraTarget();
}
} else {
player->setMovementType(MovementType::None);
player.setMovementType(MovementType::None);
}
}

View file

@ -22,6 +22,8 @@
using namespace std;
using namespace reone::net;
namespace reone {
namespace game {
@ -37,8 +39,8 @@ MultiplayerArea::MultiplayerArea(
_scriptsEnabled = mode == MultiplayerMode::Server;
}
shared_ptr<Creature> MultiplayerArea::makeCreature() {
return make_unique<MultiplayerCreature>(_idCounter++, _callbacks);
shared_ptr<Creature> MultiplayerArea::makeCreature(uint32_t id) {
return make_unique<MultiplayerCreature>(id > 0 ? id : _idCounter++, _callbacks);
}
shared_ptr<Door> MultiplayerArea::makeDoor() {
@ -76,7 +78,7 @@ void MultiplayerArea::execute(const Command &cmd) {
}
}
void MultiplayerArea::executeLoadCreature(const Command &cmd) {
shared_ptr<Creature> creature(makeCreature());
shared_ptr<Creature> creature(makeCreature(cmd.objectId()));
creature->setTag(cmd.tag());
for (auto &item : cmd.equipment()) {
@ -89,6 +91,7 @@ void MultiplayerArea::executeLoadCreature(const Command &cmd) {
creature->load(move(config));
creature->setPosition(cmd.position());
creature->setHeading(cmd.heading());
creature->setSynchronize(true);
creature->initGL();
switch (cmd.role()) {
@ -130,7 +133,7 @@ void MultiplayerArea::executeSetPlayerRole(const Command &cmd) {
}
void MultiplayerArea::executeSetObjectTransform(const Command &cmd) {
shared_ptr<Object> object(find(cmd.tag()));
shared_ptr<Object> object(find(cmd.objectId(), ObjectType::Creature));
if (object) {
object->setSynchronize(false);
object->setPosition(cmd.position());
@ -140,7 +143,7 @@ void MultiplayerArea::executeSetObjectTransform(const Command &cmd) {
}
void MultiplayerArea::executeSetObjectAnimation(const Command &cmd) {
shared_ptr<Object> object(find(cmd.tag()));
shared_ptr<Object> object(find(cmd.objectId(), ObjectType::Creature));
if (object) {
object->setSynchronize(false);
object->animate(cmd.animation(), cmd.animationFlags());
@ -149,7 +152,7 @@ void MultiplayerArea::executeSetObjectAnimation(const Command &cmd) {
}
void MultiplayerArea::executeSetCreatureMovementType(const Command &cmd) {
shared_ptr<Object> creature(find(cmd.tag(), ObjectType::Creature));
shared_ptr<Object> creature(find(cmd.objectId(), ObjectType::Creature));
if (creature) {
creature->setSynchronize(false);
static_cast<Creature &>(*creature).setMovementType(cmd.movementType());
@ -159,7 +162,7 @@ void MultiplayerArea::executeSetCreatureMovementType(const Command &cmd) {
void MultiplayerArea::executeSetDoorOpen(const Command &cmd) {
shared_ptr<Object> door(find(cmd.objectId(), ObjectType::Door));
shared_ptr<Object> trigerrer(find(cmd.trigerrer()));
shared_ptr<Object> trigerrer(find(cmd.triggerrer()));
if (door) {
door->setSynchronize(false);
static_cast<Door &>(*door).open(trigerrer);

View file

@ -40,7 +40,7 @@ public:
private:
IMultiplayerCallbacks *_callbacks { nullptr };
std::shared_ptr<Creature> makeCreature() override;
std::shared_ptr<Creature> makeCreature(uint32_t id) override;
std::shared_ptr<Door> makeDoor() override;
void updateCreature(Creature &creature, float dt) override;

View file

@ -44,6 +44,13 @@ static void putUint16(uint16_t val, ByteArray &arr) {
arr.push_back((val >> 8) & 0xff);
}
static void putUint32(uint32_t val, ByteArray &arr) {
arr.push_back(val & 0xff);
arr.push_back((val >> 8) & 0xff);
arr.push_back((val >> 16) & 0xff);
arr.push_back((val >> 24) & 0xff);
}
static void putFloat(float val, ByteArray &arr) {
uint32_t intVal = *reinterpret_cast<uint32_t *>(&val);
arr.push_back(intVal & 0xff);
@ -69,28 +76,37 @@ static uint16_t getUint16(const ByteArray &arr, int &offset) {
return val;
}
static uint32_t getUint32(const ByteArray &arr, int &offset) {
uint32_t val = *reinterpret_cast<const uint32_t *>(&arr[offset]);
offset += sizeof(uint32_t);
return val;
}
static float getFloat(const ByteArray &arr, int &offset) {
float val = *reinterpret_cast<const float *>(&arr[offset]);
offset += sizeof(float);
return val;
}
Command::Command(CommandType type) : _type(type) {
Command::Command(uint32_t id, net::CommandType type) : net::Command(id, type) {
}
void Command::load(const ByteArray &data) {
_type = static_cast<CommandType>(data[0]);
_type = static_cast<net::CommandType>(data[0]);
int offset = 1;
int equipmentCount = 0;
_id = getUint32(data, offset);
switch (_type) {
case CommandType::LoadModule:
case net::CommandType::LoadModule:
_module = getString(data, offset);
break;
case CommandType::LoadCreature:
case net::CommandType::LoadCreature:
_role = static_cast<CreatureRole>(getUint8(data, offset));
_objectId = getUint16(data, offset);
_tag = getString(data, offset);
_appearance = getUint16(data, offset);
_position.x = getFloat(data, offset);
@ -103,51 +119,53 @@ void Command::load(const ByteArray &data) {
}
break;
case CommandType::SetPlayerRole:
case net::CommandType::SetPlayerRole:
_role = static_cast<CreatureRole>(getUint8(data, offset));
break;
case CommandType::SetObjectTransform:
_tag = getString(data, offset);
case net::CommandType::SetObjectTransform:
_objectId = getUint16(data, offset);
_position.x = getFloat(data, offset);
_position.y = getFloat(data, offset);
_position.z = getFloat(data, offset);
_heading = getFloat(data, offset);
break;
case CommandType::SetObjectAnimation:
_tag = getString(data, offset);
case net::CommandType::SetObjectAnimation:
_objectId = getUint16(data, offset);
_animationFlags = getUint8(data, offset);
_animation = getString(data, offset);
break;
case CommandType::SetCreatureMovementType:
_tag = getString(data, offset);
case net::CommandType::SetCreatureMovementType:
_objectId = getUint16(data, offset);
_movementType = static_cast<MovementType>(getUint8(data, offset));
break;
case CommandType::SetDoorOpen:
case net::CommandType::SetDoorOpen:
_open = getUint8(data, offset);
_objectId = getUint16(data, offset);
_trigerrer = getString(data, offset);
_triggerrer = getUint16(data, offset);
break;
default:
throw runtime_error("Unsupported command type: " + to_string(static_cast<int>(_type)));
throw runtime_error("Command: unsupported type: " + to_string(static_cast<int>(_type)));
}
}
ByteArray Command::bytes() const {
ByteArray data;
putUint8(static_cast<uint8_t>(_type), data);
putUint32(_id, data);
switch (_type) {
case CommandType::LoadModule:
case net::CommandType::LoadModule:
putString(_module, data);
break;
case CommandType::LoadCreature:
case net::CommandType::LoadCreature:
putUint8(static_cast<uint8_t>(_role), data);
putUint16(_objectId, data);
putString(_tag, data);
putUint16(_appearance, data);
putFloat(_position.x, data);
@ -160,54 +178,46 @@ ByteArray Command::bytes() const {
}
break;
case CommandType::SetPlayerRole:
case net::CommandType::SetPlayerRole:
putUint8(static_cast<uint8_t>(_role), data);
break;
case CommandType::SetObjectTransform:
putString(_tag, data);
case net::CommandType::SetObjectTransform:
putUint16(_objectId, data);
putFloat(_position.x, data);
putFloat(_position.y, data);
putFloat(_position.z, data);
putFloat(_heading, data);
break;
case CommandType::SetObjectAnimation:
putString(_tag, data);
case net::CommandType::SetObjectAnimation:
putUint16(_objectId, data);
putUint8(_animationFlags, data);
putString(_animation, data);
break;
case CommandType::SetCreatureMovementType:
putString(_tag, data);
case net::CommandType::SetCreatureMovementType:
putUint16(_objectId, data);
putUint8(static_cast<uint8_t>(_movementType), data);
break;
case CommandType::SetDoorOpen:
case net::CommandType::SetDoorOpen:
putUint8(_open, data);
putUint16(_objectId, data);
putString(_trigerrer, data);
putUint16(_triggerrer, data);
break;
default:
throw runtime_error("Unsupported command type: " + to_string(static_cast<int>(_type)));
throw runtime_error("Command: unsupported type: " + to_string(static_cast<int>(_type)));
}
return move(data);
}
CommandType Command::type() const {
return _type;
}
const string &Command::module() const {
return _module;
}
uint32_t Command::objectId() const {
return _objectId;
}
const string &Command::tag() const {
return _tag;
}
@ -248,8 +258,8 @@ bool Command::open() const {
return _open;
}
const string &Command::trigerrer() const {
return _trigerrer;
uint32_t Command::triggerrer() const {
return _triggerrer;
}
} // namespace game

View file

@ -22,33 +22,23 @@
#include "../../core/types.h"
#include "../../game/types.h"
#include "../../net/command.h"
namespace reone {
namespace game {
enum class CommandType {
LoadModule,
LoadCreature,
SetPlayerRole,
SetObjectTransform,
SetObjectAnimation,
SetCreatureMovementType,
SetDoorOpen
};
class Command {
class Command : public net::Command {
public:
Command() = default;
Command(CommandType type);
Command(uint32_t id, net::CommandType type);
void load(const ByteArray &data);
ByteArray bytes() const;
ByteArray bytes() const override;
// Getters
CommandType type() const;
const std::string &module() const;
uint32_t objectId() const;
const std::string &tag() const;
CreatureRole role() const;
int appearance() const;
@ -59,12 +49,10 @@ public:
int animationFlags() const;
MovementType movementType() const;
bool open() const;
const std::string &trigerrer() const;
uint32_t triggerrer() const;
private:
CommandType _type { CommandType::LoadModule };
std::string _module;
uint32_t _objectId { 0 };
std::string _tag;
CreatureRole _role { CreatureRole::None };
int _appearance { 0 };
@ -76,7 +64,7 @@ private:
float _animationSpeed { 1.0f };
MovementType _movementType { MovementType::None };
bool _open { false };
std::string _trigerrer;
uint32_t _triggerrer { 0 };
friend class MultiplayerGame;
};

View file

@ -21,6 +21,7 @@
#include "area.h"
#include "module.h"
#include "util.h"
using namespace std;
using namespace std::placeholders;
@ -49,8 +50,9 @@ void MultiplayerGame::configure() {
_server = make_unique<Server>();
_server->setOnClientConnected(bind(&MultiplayerGame::onClientConnected, this, _1));
_server->setOnClientDisconnected(bind(&MultiplayerGame::onClientDisconnected, this, _1));
_server->setOnCommandReceived(bind(&MultiplayerGame::onCommandReceived, this, _1));
_server->setOnCommandReceived(bind(&MultiplayerGame::onCommandReceived, this, _2));
_server->start(_opts.network.port);
Game::configure();
break;
case MultiplayerMode::Client:
@ -60,28 +62,135 @@ void MultiplayerGame::configure() {
break;
default:
Game::configure();
break;
}
}
void MultiplayerGame::onClientConnected(const string tag) {
assert(_module && _module->loaded());
synchronizeClient(tag);
}
void MultiplayerGame::synchronizeClient(const string &tag) {
lock_guard<recursive_mutex> syncLock(_syncMutex);
sendLoadModule(tag, _module->name());
shared_ptr<Object> partyLeader(_module->area().partyLeader());
sendLoadCreature(tag, CreatureRole::PartyLeader, static_cast<Creature &>(*partyLeader));
shared_ptr<Object> partyMember1(_module->area().partyMember1());
if (partyMember1) {
Creature &creature = static_cast<Creature &>(*partyMember1);
sendLoadCreature(tag, CreatureRole::PartyMember1, creature);
}
shared_ptr<Object> partyMember2(_module->area().partyMember2());
if (partyMember2) {
Creature &creature = static_cast<Creature &>(*partyMember2);
sendLoadCreature(tag, CreatureRole::PartyMember2, creature);
}
bool control = false;
if (partyMember1) {
MultiplayerCreature &creature = static_cast<MultiplayerCreature &>(*partyMember1);
if (!creature.isControlled()) {
creature.setClientTag(tag);
sendSetPlayerRole(tag, CreatureRole::PartyMember1);
control = true;
}
}
if (!control && partyMember2) {
MultiplayerCreature &creature = static_cast<MultiplayerCreature &>(*partyMember2);
if (!creature.isControlled()) {
creature.setClientTag(tag);
sendSetPlayerRole(tag, CreatureRole::PartyMember2);
}
}
}
void MultiplayerGame::sendLoadModule(const string &client, const string &module) {
shared_ptr<net::Command> cmd(makeCommand(CommandType::LoadModule));
Command &cmd2 = static_cast<Command &>(*cmd);
cmd2._module = module;
sendCommand(client, cmd);
}
shared_ptr<net::Command> MultiplayerGame::makeCommand(net::CommandType type) {
assert(_cmdCounter <= UINT_MAX);
return shared_ptr<net::Command>(new Command(_cmdCounter++, type));
}
void MultiplayerGame::sendCommand(const string &client, const shared_ptr<net::Command> &command) {
assert(_mode == MultiplayerMode::Server);
_server->send(client, command);
}
void MultiplayerGame::sendLoadCreature(const string &client, CreatureRole role, const Creature &creature) {
shared_ptr<net::Command> cmd(makeCommand(CommandType::LoadCreature));
Command &cmd2 = static_cast<Command &>(*cmd);
cmd2._role = role;
cmd2._objectId = creature.id();
cmd2._tag = creature.tag();
cmd2._appearance = creature.appearance();
cmd2._position = creature.position();
cmd2._heading = creature.heading();
cmd2._equipment.clear();
for (auto &pair : creature.equipment()) {
cmd2._equipment.push_back(pair.second->resRef());
}
sendCommand(client, cmd);
}
void MultiplayerGame::sendSetPlayerRole(const string &client, CreatureRole role) {
shared_ptr<net::Command> cmd(makeCommand(CommandType::SetPlayerRole));
Command &cmd2 = static_cast<Command &>(*cmd);
cmd2._role = role;
sendCommand(client, cmd);
}
void MultiplayerGame::onClientDisconnected(const string tag) {
if (!_module || !_module->loaded()) return;
const MultiplayerArea &area = static_cast<MultiplayerArea &>(_module->area());
shared_ptr<Object> object(area.findCreatureByClientTag(tag));
if (object) {
static_cast<MultiplayerCreature &>(*object).setClientTag("");
}
}
void MultiplayerGame::onCommandReceived(const ByteArray &data) {
Command cmd;
cmd.load(data);
debug("Game: command received: " + describeCommand(cmd), 2);
lock_guard<recursive_mutex> lock(_commandsInMutex);
_commandsIn.push(cmd);
}
void MultiplayerGame::update() {
lock_guard<recursive_mutex> lock(_commandsMutex);
lock_guard<recursive_mutex> lock(_commandsInMutex);
bool skip = false;
while (!_commands.empty() && !skip) {
const Command &cmd = _commands.front();
while (!_commandsIn.empty() && !skip) {
const Command &cmd = _commandsIn.front();
switch (cmd.type()) {
case CommandType::LoadModule:
if (!_module || _module->name() != cmd.module()) {
_nextModule = cmd.module();
}
_commands.pop();
_commandsIn.pop();
break;
default:
if (_module && _nextModule.empty()) {
static_cast<MultiplayerArea &>(_module->area()).execute(cmd);
_commands.pop();
_commandsIn.pop();
} else {
skip = true;
}
@ -102,63 +211,13 @@ void MultiplayerGame::loadNextModule() {
}
}
const shared_ptr<Module> MultiplayerGame::makeModule(const string &name) {
return shared_ptr<Module>(new MultiplayerModule(name, _mode, _version, _opts.graphics, this));
}
void MultiplayerGame::onObjectTransformChanged(const Object &object, const glm::vec3 &position, float heading) {
if (shouldSendObjectUpdates(object.tag())) {
sendSetObjectTransformCommand(object.tag(), position, heading);
if (shouldSendObjectUpdates(object.id())) {
sendSetObjectTransform(object.id(), position, heading);
}
}
void MultiplayerGame::onObjectAnimationChanged(const Object &object, const string &anim, int flags, float speed) {
if (shouldSendObjectUpdates(object.tag())) {
sendSetObjectAnimationCommand(object.tag(), anim, flags, speed);
}
}
void MultiplayerGame::onCreatureMovementTypeChanged(const MultiplayerCreature &creature, MovementType type) {
if (shouldSendObjectUpdates(creature.tag())) {
sendSetCreatureMovementTypeCommand(creature.tag(), type);
}
}
void MultiplayerGame::onDoorOpen(const MultiplayerDoor &door, const shared_ptr<Object> &trigerrer) {
string trigerrerTag(trigerrer ? trigerrer->tag() : "");
if (shouldSendObjectUpdates(trigerrerTag)) {
sendSetDoorOpenCommand(door.id(), trigerrerTag);
}
}
void MultiplayerGame::onClientConnected(const string tag) {
if (!_module) return;
synchronizeClient(tag);
}
void MultiplayerGame::onClientDisconnected(const string tag) {
if (!_module) return;
const MultiplayerArea &area = static_cast<MultiplayerArea &>(_module->area());
shared_ptr<Object> object(area.findCreatureByClientTag(tag));
if (object) {
static_cast<MultiplayerCreature &>(*object).setClientTag("");
}
}
void MultiplayerGame::onCommandReceived(const ByteArray &data) {
Command cmd;
cmd.load(data);
debug("Command received: " + to_string(static_cast<int>(cmd.type())), 2);
lock_guard<recursive_mutex> lock(_commandsMutex);
_commands.push(cmd);
}
bool MultiplayerGame::shouldSendObjectUpdates(const string &tag) const {
bool MultiplayerGame::shouldSendObjectUpdates(uint32_t objectId) const {
if (!_module || !_module->loaded()) return false;
shared_ptr<Object> player(_module->area().player());
@ -168,7 +227,7 @@ bool MultiplayerGame::shouldSendObjectUpdates(const string &tag) const {
return true;
case MultiplayerMode::Client:
return player && tag == player->tag();
return player && objectId == player->id();
default:
break;
@ -177,124 +236,78 @@ bool MultiplayerGame::shouldSendObjectUpdates(const string &tag) const {
return false;
}
void MultiplayerGame::synchronizeClient(const string &tag) {
lock_guard<recursive_mutex> lock(_syncMutex);
void MultiplayerGame::sendSetObjectTransform(uint32_t objectId, const glm::vec3 &position, float heading) {
shared_ptr<net::Command> cmd(makeCommand(CommandType::SetObjectTransform));
Command &cmd2 = static_cast<Command &>(*cmd);
cmd2._objectId = objectId;
cmd2._position = position;
cmd2._heading = heading;
sendLoadModuleCommand(tag, _module->name());
sendCommand(cmd);
}
shared_ptr<Object> partyLeader(_module->area().partyLeader());
sendLoadCreatureCommand(tag, CreatureRole::PartyLeader, static_cast<Creature &>(*partyLeader));
shared_ptr<Object> partyMember1(_module->area().partyMember1());
if (partyMember1) {
Creature &creature = static_cast<Creature &>(*partyMember1);
sendLoadCreatureCommand(tag, CreatureRole::PartyMember1, creature);
}
shared_ptr<Object> partyMember2(_module->area().partyMember2());
if (partyMember2) {
Creature &creature = static_cast<Creature &>(*partyMember2);
sendLoadCreatureCommand(tag, CreatureRole::PartyMember2, creature);
}
bool control = false;
if (partyMember1) {
MultiplayerCreature &creature = static_cast<MultiplayerCreature &>(*partyMember1);
if (!creature.isControlled()) {
creature.setClientTag(tag);
sendSetPlayerRoleCommand(tag, CreatureRole::PartyMember1);
control = true;
}
}
if (!control && partyMember2) {
MultiplayerCreature &creature = static_cast<MultiplayerCreature &>(*partyMember2);
if (!creature.isControlled()) {
creature.setClientTag(tag);
sendSetPlayerRoleCommand(tag, CreatureRole::PartyMember2);
}
void MultiplayerGame::sendCommand(const shared_ptr<net::Command> &command) {
switch (_mode) {
case MultiplayerMode::Server:
_server->sendToAll(command);
break;
default:
_client->send(command);
break;
}
}
void MultiplayerGame::sendLoadModuleCommand(const string &client, const string &module) {
Command cmd(CommandType::LoadModule);
cmd._module = module;
_server->send(client, cmd.bytes());
}
void MultiplayerGame::sendLoadCreatureCommand(const string &client, CreatureRole role, const Creature &creature) {
Command cmd(CommandType::LoadCreature);
cmd._role = role;
cmd._tag = creature.tag();
cmd._appearance = creature.appearance();
cmd._position = creature.position();
cmd._heading = creature.heading();
cmd._equipment.clear();
for (auto &pair : creature.equipment()) {
cmd._equipment.push_back(pair.second->resRef());
}
_server->send(client, cmd.bytes());
}
void MultiplayerGame::sendSetPlayerRoleCommand(const string &client, CreatureRole role) {
Command cmd(CommandType::SetPlayerRole);
cmd._role = role;
_server->send(client, cmd.bytes());
}
void MultiplayerGame::sendSetObjectTransformCommand(const string &tag, const glm::vec3 &position, float heading) {
Command cmd(CommandType::SetObjectTransform);
cmd._tag = tag;
cmd._position = position;
cmd._heading = heading;
if (_mode == MultiplayerMode::Client) {
_client->send(cmd.bytes());
} else {
_server->sendToAll(cmd.bytes());
void MultiplayerGame::onObjectAnimationChanged(const Object &object, const string &anim, int flags, float speed) {
if (shouldSendObjectUpdates(object.id())) {
sendSetObjectAnimation(object.id(), anim, flags, speed);
}
}
void MultiplayerGame::sendSetObjectAnimationCommand(const string &tag, const string &animation, int flags, float speed) {
Command cmd(CommandType::SetObjectAnimation);
cmd._tag = tag;
cmd._animation = animation;
cmd._animationFlags = flags;
cmd._animationSpeed = speed;
void MultiplayerGame::sendSetObjectAnimation(uint32_t objectId, const string &animation, int flags, float speed) {
shared_ptr<net::Command> cmd(makeCommand(CommandType::SetObjectAnimation));
Command &cmd2 = static_cast<Command &>(*cmd);
cmd2._objectId = objectId;
cmd2._animation = animation;
cmd2._animationFlags = flags;
cmd2._animationSpeed = speed;
if (_mode == MultiplayerMode::Client) {
_client->send(cmd.bytes());
} else {
_server->sendToAll(cmd.bytes());
sendCommand(cmd);
}
void MultiplayerGame::onCreatureMovementTypeChanged(const MultiplayerCreature &creature, MovementType type) {
if (shouldSendObjectUpdates(creature.id())) {
sendSetCreatureMovementType(creature.id(), type);
}
}
void MultiplayerGame::sendSetCreatureMovementTypeCommand(const string &tag, MovementType type) {
Command cmd(CommandType::SetCreatureMovementType);
cmd._tag = tag;
cmd._movementType = type;
void MultiplayerGame::sendSetCreatureMovementType(uint32_t objectId, MovementType type) {
shared_ptr<net::Command> cmd(makeCommand(CommandType::SetCreatureMovementType));
Command &cmd2 = static_cast<Command &>(*cmd);
cmd2._objectId = objectId;
cmd2._movementType = type;
if (_mode == MultiplayerMode::Client) {
_client->send(cmd.bytes());
} else {
_server->sendToAll(cmd.bytes());
sendCommand(cmd);
}
void MultiplayerGame::onDoorOpen(const MultiplayerDoor &door, const shared_ptr<Object> &trigerrer) {
uint32_t triggerrerId(trigerrer ? trigerrer->id() : 0);
if (shouldSendObjectUpdates(triggerrerId)) {
sendSetDoorOpen(door.id(), triggerrerId);
}
}
void MultiplayerGame::sendSetDoorOpenCommand(uint32_t id, const string &trigerrer) {
Command cmd(CommandType::SetDoorOpen);
cmd._objectId = id;
cmd._trigerrer = trigerrer;
cmd._open = true;
void MultiplayerGame::sendSetDoorOpen(uint32_t objectId, uint32_t triggerrer) {
shared_ptr<net::Command> cmd(makeCommand(CommandType::SetDoorOpen));
Command &cmd2 = static_cast<Command &>(*cmd);
cmd2._objectId = objectId;
cmd2._triggerrer = triggerrer;
cmd2._open = true;
if (_mode == MultiplayerMode::Client) {
_client->send(cmd.bytes());
} else {
_server->sendToAll(cmd.bytes());
}
sendCommand(cmd);
}
const shared_ptr<Module> MultiplayerGame::makeModule(const string &name) {
return shared_ptr<Module>(new MultiplayerModule(name, _mode, _version, _opts.graphics, this));
}
} // namespace game

View file

@ -17,6 +17,8 @@
#pragma once
#include <list>
#include <map>
#include <mutex>
#include "../game.h"
@ -39,31 +41,41 @@ public:
const Options &opts);
private:
uint32_t _cmdCounter { 0 };
MultiplayerMode _mode { MultiplayerMode::Server };
std::recursive_mutex _syncMutex;
std::unique_ptr<net::Client> _client;
std::unique_ptr<net::Server> _server;
std::recursive_mutex _commandsMutex;
std::queue<Command> _commands;
std::recursive_mutex _syncMutex;
// Commands
std::queue<Command> _commandsIn;
std::recursive_mutex _commandsInMutex;
// Game overrides
void configure() override;
const std::shared_ptr<Module> makeModule(const std::string &name) override;
void update() override;
void loadNextModule() override;
const std::shared_ptr<Module> makeModule(const std::string &name) override;
// IMultiplayerCallbacks overrides
void onObjectTransformChanged(const Object &object, const glm::vec3 &position, float heading) override;
void onObjectAnimationChanged(const Object &object, const std::string &anim, int flags, float speed) override;
void onCreatureMovementTypeChanged(const MultiplayerCreature &creature, MovementType type) override;
bool shouldSendObjectUpdates(const std::string &tag) const;
std::shared_ptr<net::Command> makeCommand(net::CommandType type);
bool shouldSendObjectUpdates(uint32_t objectId) const;
void synchronizeClient(const std::string &tag);
void sendLoadModuleCommand(const std::string &client, const std::string &module);
void sendLoadCreatureCommand(const std::string &client, CreatureRole role, const Creature &creature);
void sendSetPlayerRoleCommand(const std::string &client, CreatureRole role);
void sendSetObjectTransformCommand(const std::string &tag, const glm::vec3 &position, float heading);
void sendSetObjectAnimationCommand(const std::string &tag, const std::string &animation, int flags, float speed);
void sendSetCreatureMovementTypeCommand(const std::string &tag, MovementType type);
void sendSetDoorOpenCommand(uint32_t objectId, const std::string &trigerrer);
void sendLoadModule(const std::string &client, const std::string &module);
void sendLoadCreature(const std::string &client, CreatureRole role, const Creature &creature);
void sendSetPlayerRole(const std::string &client, CreatureRole role);
void sendSetObjectTransform(uint32_t objectId, const glm::vec3 &position, float heading);
void sendSetObjectAnimation(uint32_t objectId, const std::string &animation, int flags, float speed);
void sendSetCreatureMovementType(uint32_t objectId, MovementType type);
void sendSetDoorOpen(uint32_t objectId, uint32_t triggerrer);
void sendCommand(const std::shared_ptr<net::Command> &command);
void sendCommand(const std::string &client, const std::shared_ptr<net::Command> &command);
void onClientConnected(const std::string tag);
void onClientDisconnected(const std::string tag);

View file

@ -0,0 +1,70 @@
/*
* Copyright © 2020 Vsevolod Kremianskii
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "util.h"
#include <map>
#include <boost/format.hpp>
using namespace std;
namespace reone {
namespace game {
static map<net::CommandType, string> g_cmdDesc = {
{ net::CommandType::LoadModule, "LoadModule" },
{ net::CommandType::LoadCreature, "LoadCreature" },
{ net::CommandType::SetPlayerRole, "SetPlayerRole" },
{ net::CommandType::SetObjectTransform, "SetObjectTransform" },
{ net::CommandType::SetObjectAnimation, "SetObjectAnimation" },
{ net::CommandType::SetCreatureMovementType, "SetCreatureMovementType" },
{ net::CommandType::SetDoorOpen, "SetDoorOpen" }
};
static const string &describeCommandType(net::CommandType type) {
auto desc = g_cmdDesc.find(type);
if (desc == g_cmdDesc.end()) {
auto pair = g_cmdDesc.insert(make_pair(type, to_string(static_cast<int>(type))));
return pair.first->second;
}
return desc->second;
}
string describeCommand(const Command &command) {
string desc(describeCommandType(command.type()));
switch (command.type()) {
case net::CommandType::SetObjectTransform:
case net::CommandType::SetObjectAnimation:
case net::CommandType::SetCreatureMovementType:
desc += str(boost::format(" {id=%d,objectId=%d}") % command.id() % command.objectId());
break;
default:
desc += str(boost::format(" {id=%d}") % command.id());
break;
}
return desc;
}
} // namespace game
} // namespace reone

View file

@ -0,0 +1,32 @@
/*
* Copyright © 2020 Vsevolod Kremianskii
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include "command.h"
namespace reone {
namespace game {
std::string describeCommand(const Command &cmd);
} // namespace game
} // namespace reone

View file

@ -66,7 +66,7 @@ protected:
glm::mat4 _transform { 1.0f };
std::shared_ptr<render::ModelInstance> _model;
std::shared_ptr<render::Walkmesh> _walkmesh;
bool _synchronize { true };
bool _synchronize { false };
Object(uint32_t id);
Object(Object &&) = default;

View file

@ -71,9 +71,9 @@ void Client::stop() {
}
}
void Client::send(const ByteArray &data) {
void Client::send(const shared_ptr<Command> &command) {
assert(_connection);
_connection->send(data);
_connection->send(command);
}
void Client::setOnCommandReceived(const function<void(const ByteArray &)> &fn) {

View file

@ -23,6 +23,7 @@
#include <boost/asio/io_service.hpp>
#include "connection.h"
#include "command.h"
namespace reone {
@ -36,7 +37,7 @@ public:
void start(const std::string &address, int port);
void stop();
void send(const ByteArray &data);
void send(const std::shared_ptr<Command> &command);
void setOnCommandReceived(const std::function<void(const ByteArray &)> &fn);

41
src/net/command.cpp Normal file
View file

@ -0,0 +1,41 @@
/*
* Copyright © 2020 Vsevolod Kremianskii
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "command.h"
namespace reone {
namespace net {
Command::Command(uint32_t id, CommandType type) : _id(id), _type(type) {
}
uint32_t Command::id() const {
return _id;
}
CommandType Command::type() const {
return _type;
}
uint32_t Command::objectId() const {
return _objectId;
}
} // namespace net
} // namespace reone

47
src/net/command.h Normal file
View file

@ -0,0 +1,47 @@
/*
* Copyright © 2020 Vsevolod Kremianskii
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "../core/types.h"
#include "types.h"
namespace reone {
namespace net {
class Command {
public:
Command() = default;
Command(uint32_t id, CommandType type);
virtual ByteArray bytes() const = 0;
uint32_t id() const;
CommandType type() const;
uint32_t objectId() const;
protected:
uint32_t _id { 0 };
CommandType _type { CommandType::LoadModule };
uint32_t _objectId { 0 };
};
} // namespace net
} // namespace reone

View file

@ -46,13 +46,13 @@ void Connection::open() {
void Connection::handleRead(size_t bytesRead, const boost::system::error_code &ec) {
if (ec) {
if (ec != boost::asio::error::eof) {
error("TCP: read failed: " + ec.message());
error("Connection: read failed: " + ec.message());
}
if (_onAbort) _onAbort(_tag);
return;
}
debug(boost::format("TCP: received %d bytes from %s") % bytesRead % _socket->remote_endpoint(), 3);
debug(boost::format("Connection: %d bytes received from %s") % bytesRead % _socket->remote_endpoint(), 3);
if (_cmdLength == 0) {
char buf[2];
@ -61,7 +61,7 @@ void Connection::handleRead(size_t bytesRead, const boost::system::error_code &e
_cmdLength = *reinterpret_cast<const uint16_t *>(buf);
if (_cmdLength == 0) {
error("TCP: invalid command length: " + to_string(_cmdLength));
error("Connection: invalid command length: " + to_string(_cmdLength));
if (_onAbort) _onAbort(_tag);
return;
}
@ -100,29 +100,69 @@ void Connection::close() {
}
}
void Connection::send(const ByteArray &data) {
ByteArray data2(data);
int cmdLength = static_cast<int>(data2.size());
data2.insert(data2.begin(), (cmdLength >> 8) & 0xff);
data2.insert(data2.begin(), cmdLength & 0xff);
void Connection::send(const shared_ptr<Command> &command) {
lock_guard<recursive_mutex> lock(_cmdOutMutex);
eraseSameCommands(*command);
_cmdOut.push_back(command);
if (_cmdOut.size() == 1) {
doSend(*command);
}
}
void Connection::eraseSameCommands(const Command &command) {
switch (command.type()) {
case net::CommandType::SetObjectTransform:
case net::CommandType::SetObjectAnimation:
case net::CommandType::SetCreatureMovementType:
break;
default:
return;
}
auto cmdToErase = remove_if(
_cmdOut.begin(),
_cmdOut.end(),
[&command](const shared_ptr<Command> &item) { return item->type() == command.type() && item->objectId() == command.objectId(); });
_cmdOut.erase(cmdToErase, _cmdOut.end());
}
void Connection::doSend(const Command &command) {
ByteArray data(command.bytes());
int cmdLength = static_cast<int>(data.size());
data.insert(data.begin(), (cmdLength >> 8) & 0xff);
data.insert(data.begin(), cmdLength & 0xff);
shared_ptr<boost::asio::streambuf> buffer(new boost::asio::streambuf());
ostream out(buffer.get());
out.write(&data2[0], data2.size());
out.write(&data[0], data.size());
boost::asio::async_write(
*_socket,
*buffer,
bind(&Connection::handleWrite, this, buffer, _1));
bind(&Connection::handleWrite, this, command.id(), buffer, _1));
}
void Connection::handleWrite(shared_ptr<boost::asio::streambuf> &buffer, const boost::system::error_code &ec) {
void Connection::handleWrite(uint32_t commandId, shared_ptr<boost::asio::streambuf> &buffer, const boost::system::error_code &ec) {
buffer.reset();
if (ec) {
error("TCP: write failed: " + ec.message());
error("Connection: write failed: " + ec.message());
if (_onAbort) _onAbort(_tag);
}
lock_guard<recursive_mutex> lock(_cmdOutMutex);
auto command = find_if(
_cmdOut.begin(),
_cmdOut.end(),
[&commandId](const shared_ptr<Command> &cmd) { return cmd->id() == commandId; });
if (command != _cmdOut.end()) {
_cmdOut.erase(command);
}
if (!_cmdOut.empty()) {
doSend(*_cmdOut.front());
}
}
void Connection::setTag(const string &tag) {

View file

@ -18,6 +18,8 @@
#pragma once
#include <functional>
#include <list>
#include <mutex>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/streambuf.hpp>
@ -25,6 +27,7 @@
#include "../core/types.h"
#include "command.h"
#include "types.h"
namespace reone {
@ -39,7 +42,7 @@ public:
void open();
void close();
void send(const ByteArray &data);
void send(const std::shared_ptr<Command> &command);
void setTag(const std::string &tag);
@ -53,14 +56,18 @@ private:
std::string _tag;
boost::asio::streambuf _readBuffer;
int _cmdLength { 0 };
std::list<std::shared_ptr<Command>> _cmdOut;
std::recursive_mutex _cmdOutMutex;
std::function<void(const std::string &)> _onAbort;
std::function<void(const ByteArray &)> _onCommandReceived;
Connection(const Connection &) = delete;
Connection &operator=(const Connection &) = delete;
void eraseSameCommands(const Command &command);
void doSend(const Command &command);
void handleRead(size_t bytesRead, const boost::system::error_code &ec);
void handleWrite(std::shared_ptr<boost::asio::streambuf> &buffer, const boost::system::error_code &ec);
void handleWrite(uint32_t commandId, std::shared_ptr<boost::asio::streambuf> &buffer, const boost::system::error_code &ec);
};
} // namespace net

View file

@ -55,7 +55,9 @@ void Server::handleAccept(shared_ptr<tcp::socket> &socket, const boost::system::
unique_ptr<Connection> client(new Connection(socket));
client->setTag(tag);
client->setOnAbort([this, &client](const string &tag) { stopClient(tag); });
client->setOnCommandReceived(_onCommandReceived);
client->setOnCommandReceived([this, tag](const ByteArray &data) {
_onCommandReceived(tag, data);
});
client->open();
_clients.insert(make_pair(tag, move(client)));
@ -101,18 +103,18 @@ void Server::stop() {
_clients.clear();
}
void Server::send(const string &tag, const ByteArray &data) {
void Server::send(const string &tag, const shared_ptr<Command> &command) {
auto it = _clients.find(tag);
if (it == _clients.end()) {
warn("TCP: invalid client: " + tag);
return;
}
it->second->send(data);
it->second->send(command);
}
void Server::sendToAll(const ByteArray &data) {
void Server::sendToAll(const shared_ptr<Command> &command) {
for (auto &client : _clients) {
client.second->send(data);
client.second->send(command);
}
}
@ -128,7 +130,7 @@ void Server::setOnClientDisconnected(const function<void(const string &)> &fn) {
_onClientDisconnected = fn;
}
void Server::setOnCommandReceived(const function<void(const ByteArray &)> &fn) {
void Server::setOnCommandReceived(const function<void(const string &, const ByteArray &)> &fn) {
_onCommandReceived = fn;
}

View file

@ -25,6 +25,7 @@
#include <boost/asio/ip/tcp.hpp>
#include "connection.h"
#include "command.h"
#include "types.h"
namespace reone {
@ -41,15 +42,15 @@ public:
void start(int port);
void stop();
void send(const std::string &client, const ByteArray &data);
void sendToAll(const ByteArray &data);
void send(const std::string &client, const std::shared_ptr<Command> &command);
void sendToAll(const std::shared_ptr<Command> &command);
const ServerClients &clients() const;
// Callbacks
void setOnClientConnected(const std::function<void(const std::string &)> &fn);
void setOnClientDisconnected(const std::function<void(const std::string &)> &fn);
void setOnCommandReceived(const std::function<void(const ByteArray &)> &fn);
void setOnCommandReceived(const std::function<void(const std::string &, const ByteArray &)> &fn);
private:
boost::asio::io_service _service;
@ -60,7 +61,7 @@ private:
// Callbacks
std::function<void(const std::string &)> _onClientConnected;
std::function<void(const std::string &)> _onClientDisconnected;
std::function<void(const ByteArray &)> _onCommandReceived;
std::function<void(const std::string &, const ByteArray &)> _onCommandReceived;
Server(const Server &) = delete;
Server &operator=(const Server &) = delete;

View file

@ -23,6 +23,16 @@ namespace reone {
namespace net {
enum class CommandType {
LoadModule,
LoadCreature,
SetPlayerRole,
SetObjectTransform,
SetObjectAnimation,
SetCreatureMovementType,
SetDoorOpen
};
struct NetworkOptions {
std::string host;
int port { 0 };