Store voting data in lobby protocol so it is available on client
and server. Voting behaviour is now to start the race as soon as all votes are in.
This commit is contained in:
34
data/gui/screens/online/vote_overview.stkgui
Normal file
34
data/gui/screens/online/vote_overview.stkgui
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<stkgui>
|
||||
<icon-button id="back" x="0" y="0" height="8%" icon="gui/icons/back.png"/>
|
||||
|
||||
<div id="all-track" x="1%" y="1%" width="98%" height="96%" layout="vertical-row" >
|
||||
<header width="80%" I18N="In the track selection screen" text="Votes"
|
||||
align="center" text_align="center" />
|
||||
|
||||
<div id="rect-box" width="100%" height="80%" padding="15" layout="vertical-row">
|
||||
<div proportion="1" width="100%" layout="horizontal-row" padding="1">
|
||||
<box id="rect-box0" width="24%" height="100%" padding="15" layout="vertical-row">
|
||||
</box>
|
||||
<box id="rect-box1" width="24%" height="100%" padding="15" layout="vertical-row">
|
||||
</box>
|
||||
<box id="rect-box2" width="24%" height="100%" padding="15" layout="vertical-row">
|
||||
</box>
|
||||
<box id="rect-box3" width="24%" height="100%" padding="15" layout="vertical-row">
|
||||
</box>
|
||||
</div>
|
||||
|
||||
<div proportion="1" width="100%" layout="horizontal-row" padding="1">
|
||||
<box id="rect-box4" width="24%" height="100%" padding="15" layout="vertical-row">
|
||||
</box>
|
||||
<box id="rect-box5" width="24%" height="100%" padding="15" layout="vertical-row">
|
||||
</box>
|
||||
<box id="rect-box6" width="24%" height="100%" padding="15" layout="vertical-row">
|
||||
</box>
|
||||
<box id="rect-box7" width="24%" height="100%" padding="15" layout="vertical-row">
|
||||
</box>
|
||||
</div>
|
||||
<progressbar id="timer" height="4%" width="100%"></progressbar>
|
||||
</div>
|
||||
</div>
|
||||
</stkgui>
|
||||
@@ -44,6 +44,7 @@
|
||||
#include "network/stk_peer.hpp"
|
||||
#include "states_screens/online/networking_lobby.hpp"
|
||||
#include "states_screens/online/network_kart_selection.hpp"
|
||||
#include "states_screens/online/vote_overview.hpp"
|
||||
#include "states_screens/race_result_gui.hpp"
|
||||
#include "states_screens/state_manager.hpp"
|
||||
#include "states_screens/online/tracks_screen.hpp"
|
||||
@@ -121,7 +122,7 @@ void ClientLobby::setup()
|
||||
{
|
||||
clearPlayers();
|
||||
m_received_server_result = false;
|
||||
TracksScreen::getInstance()->resetVote();
|
||||
VoteOverview::getInstance()->resetVote();
|
||||
LobbyProtocol::setup();
|
||||
m_state.store(NONE);
|
||||
} // setup
|
||||
@@ -162,7 +163,7 @@ bool ClientLobby::notifyEvent(Event* event)
|
||||
case LE_SERVER_INFO: handleServerInfo(event); break;
|
||||
case LE_PLAYER_DISCONNECTED : disconnectedPlayer(event); break;
|
||||
case LE_CONNECTION_REFUSED: connectionRefused(event); break;
|
||||
case LE_VOTE: displayPlayerVote(event); break;
|
||||
case LE_VOTE: receivePlayerVote(event); break;
|
||||
case LE_SERVER_OWNERSHIP: becomingServerOwner(); break;
|
||||
case LE_BAD_TEAM: handleBadTeam(); break;
|
||||
case LE_BAD_CONNECTION: handleBadConnection(); break;
|
||||
@@ -435,68 +436,32 @@ void ClientLobby::finalizeConnectionRequest(NetworkString* header,
|
||||
} // finalizeConnectionRequest
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
void ClientLobby::displayPlayerVote(Event* event)
|
||||
void ClientLobby::receivePlayerVote(Event* event)
|
||||
{
|
||||
if (!checkDataSize(event, 4)) return;
|
||||
// Get the player name who voted
|
||||
NetworkString& data = event->data();
|
||||
|
||||
|
||||
uint32_t host_id2 = data.getUInt32();
|
||||
std::shared_ptr<STKPeer> peer = STKHost::get()->findPeerByHostId(host_id2);
|
||||
|
||||
std::string player_name;
|
||||
data.decodeString(&player_name);
|
||||
// if (host_id2 != STKHost::get()->getMyHostId())
|
||||
{
|
||||
// std::string local_name = StringUtils::wideToUtf8(peer->getPlayerProfiles()[0]->getName());
|
||||
}
|
||||
|
||||
uint32_t host_id = data.getUInt32();
|
||||
player_name += ": ";
|
||||
std::string track_name;
|
||||
data.decodeString(&track_name);
|
||||
Track* track = track_manager->getTrack(track_name);
|
||||
PeerVote vote(data);
|
||||
m_peers_votes[event->getPeer()->getHostId()] = vote;
|
||||
Track* track = track_manager->getTrack(vote.m_track_name);
|
||||
if (!track)
|
||||
Log::fatal("ClientLobby", "Missing track %s", track_name.c_str());
|
||||
core::stringw track_readable = track->getName();
|
||||
int lap = data.getUInt8();
|
||||
int rev = data.getUInt8();
|
||||
core::stringw yes = _("Yes");
|
||||
core::stringw no = _("No");
|
||||
core::stringw vote_msg;
|
||||
if (race_manager->getMinorMode() == RaceManager::MINOR_MODE_BATTLE &&
|
||||
race_manager->getMajorMode() == RaceManager::MAJOR_MODE_FREE_FOR_ALL)
|
||||
{
|
||||
//I18N: Vote message in network game from a player
|
||||
vote_msg = _("Track: %s,\nrandom item location: %s",
|
||||
track_readable, rev == 1 ? yes : no);
|
||||
}
|
||||
else if (race_manager->getMinorMode() == RaceManager::MINOR_MODE_BATTLE &&
|
||||
race_manager->getMajorMode() ==
|
||||
RaceManager::MAJOR_MODE_CAPTURE_THE_FLAG)
|
||||
{
|
||||
//I18N: Vote message in network game from a player
|
||||
vote_msg = _("Track: %s", track_readable);
|
||||
}
|
||||
else if (race_manager->getMinorMode() == RaceManager::MINOR_MODE_SOCCER)
|
||||
{
|
||||
if (m_game_setup->isSoccerGoalTarget())
|
||||
{
|
||||
//I18N: Vote message in network game from a player
|
||||
vote_msg = _("Track: %s,\n"
|
||||
"number of goals to win: %d,\nrandom item location: %s",
|
||||
track_readable, lap, rev == 1 ? yes : no);
|
||||
}
|
||||
else
|
||||
{
|
||||
//I18N: Vote message in network game from a player
|
||||
vote_msg = _("Track: %s,\n"
|
||||
"maximum time: %d,\nrandom item location: %s",
|
||||
track_readable, lap, rev == 1 ? yes : no);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//I18N: Vote message in network game from a player
|
||||
vote_msg = _("Track: %s,\nlaps: %d, reversed: %s",
|
||||
track_readable, lap, rev == 1 ? yes : no);
|
||||
}
|
||||
vote_msg = StringUtils::utf8ToWide(player_name) + vote_msg;
|
||||
TracksScreen::getInstance()->addVoteMessage(player_name +
|
||||
StringUtils::toString(host_id), vote_msg);
|
||||
} // displayPlayerVote
|
||||
Log::fatal("ClientLobby", "Missing track %s", vote.m_track_name.c_str());
|
||||
|
||||
} // receivePlayerVote
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/*! \brief Called when a new player is disconnected
|
||||
|
||||
@@ -44,7 +44,7 @@ private:
|
||||
void raceFinished(Event* event);
|
||||
void exitResultScreen(Event *event);
|
||||
// race votes
|
||||
void displayPlayerVote(Event* event);
|
||||
void receivePlayerVote(Event* event);
|
||||
void updatePlayerList(Event* event);
|
||||
void handleChat(Event* event);
|
||||
void handleServerInfo(Event* event);
|
||||
|
||||
@@ -21,11 +21,14 @@
|
||||
|
||||
#include "network/protocol.hpp"
|
||||
|
||||
#include "network/network_string.hpp"
|
||||
|
||||
class GameSetup;
|
||||
class NetworkPlayerProfile;
|
||||
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
@@ -83,6 +86,43 @@ public:
|
||||
uint64_t m_max_voting_time;
|
||||
|
||||
protected:
|
||||
/** A simple structure to store a vote from a client:
|
||||
* track name, number of laps and reverse or not. */
|
||||
class PeerVote
|
||||
{
|
||||
public:
|
||||
std::string m_track_name;
|
||||
uint8_t m_num_laps;
|
||||
bool m_reverse;
|
||||
|
||||
// ------------------------------------------------------
|
||||
PeerVote() : m_track_name(""), m_num_laps(1),
|
||||
m_reverse(false)
|
||||
{}
|
||||
// ------------------------------------------------------
|
||||
/** Initialised this object from a data in a network string. */
|
||||
PeerVote(NetworkString &ns)
|
||||
{
|
||||
ns.decodeString(&m_track_name);
|
||||
m_num_laps = ns.getUInt8();
|
||||
m_reverse = ns.getUInt8();
|
||||
|
||||
} // PeerVote
|
||||
// ------------------------------------------------------
|
||||
/** Encodes this vote object into a network string. */
|
||||
void encode(NetworkString *ns)
|
||||
{
|
||||
ns->encodeString(m_track_name)
|
||||
.addUInt8(m_num_laps)
|
||||
.addUInt8(m_reverse);
|
||||
} // encode
|
||||
}; // class PeerVote
|
||||
|
||||
/** Vote from each peer. The host id is used as a key. Note that
|
||||
* host ids can be non-consecutive, so we cannot use std::vector. */
|
||||
std::map<int, PeerVote> m_peers_votes;
|
||||
|
||||
|
||||
std::thread m_start_game_thread;
|
||||
|
||||
static std::weak_ptr<LobbyProtocol> m_lobby;
|
||||
|
||||
@@ -334,7 +334,7 @@ bool ServerLobby::notifyEventAsynchronous(Event* event)
|
||||
case LE_CONNECTION_REQUESTED: connectionRequested(event); break;
|
||||
case LE_KART_SELECTION: kartSelectionRequested(event); break;
|
||||
case LE_CLIENT_LOADED_WORLD: finishedLoadingWorldClient(event); break;
|
||||
case LE_VOTE: playerVote(event); break;
|
||||
case LE_VOTE: handlePlayerVote(event); break;
|
||||
case LE_KICK_HOST: kickHost(event); break;
|
||||
case LE_CHANGE_TEAM: changeTeam(event); break;
|
||||
case LE_REQUEST_BEGIN: startSelection(event); break;
|
||||
@@ -489,13 +489,12 @@ void ServerLobby::asynchronousUpdate()
|
||||
}
|
||||
case SELECTING:
|
||||
{
|
||||
std::string track_name;
|
||||
int num_laps;
|
||||
bool reverse;
|
||||
bool all_votes_in = handleAllVotes(&track_name, &num_laps, &reverse);
|
||||
PeerVote winner;
|
||||
bool all_votes_in = handleAllVotes(&winner);
|
||||
if (isVotingOver() || all_votes_in)
|
||||
{
|
||||
m_game_setup->setRace(track_name, num_laps, reverse);
|
||||
m_game_setup->setRace(winner.m_track_name, winner.m_num_laps,
|
||||
winner.m_reverse );
|
||||
// Remove disconnected player (if any) one last time
|
||||
m_game_setup->update(true);
|
||||
m_game_setup->sortPlayersForGrandPrix();
|
||||
@@ -505,9 +504,10 @@ void ServerLobby::asynchronousUpdate()
|
||||
player->getPeer()->clearAvailableKartIDs();
|
||||
NetworkString* load_world = getNetworkString();
|
||||
load_world->setSynchronous(true);
|
||||
load_world->addUInt8(LE_LOAD_WORLD).encodeString(track_name)
|
||||
.addUInt8(num_laps).addUInt8(reverse)
|
||||
.addUInt8((uint8_t)players.size());
|
||||
load_world->addUInt8(LE_LOAD_WORLD)
|
||||
.encodeString(winner.m_track_name)
|
||||
.addUInt8(winner.m_num_laps).addUInt8(winner.m_reverse)
|
||||
.addUInt8((uint8_t)players.size());
|
||||
for (unsigned i = 0; i < players.size(); i++)
|
||||
{
|
||||
std::shared_ptr<NetworkPlayerProfile>& player = players[i];
|
||||
@@ -1832,7 +1832,7 @@ void ServerLobby::kartSelectionRequested(Event* event)
|
||||
/*! \brief Called when a player votes for track(s).
|
||||
* \param event : Event providing the information.
|
||||
*/
|
||||
void ServerLobby::playerVote(Event* event)
|
||||
void ServerLobby::handlePlayerVote(Event* event)
|
||||
{
|
||||
if (m_state != SELECTING)
|
||||
{
|
||||
@@ -1849,145 +1849,97 @@ void ServerLobby::playerVote(Event* event)
|
||||
if (isVotingOver()) return;
|
||||
|
||||
NetworkString& data = event->data();
|
||||
std::string track_name;
|
||||
data.decodeString(&track_name);
|
||||
uint8_t lap = data.getUInt8();
|
||||
uint8_t reverse = data.getUInt8();
|
||||
PeerVote vote(data);
|
||||
|
||||
if (race_manager->modeHasLaps())
|
||||
{
|
||||
if (ServerConfig::m_auto_lap_ratio > 0.0f)
|
||||
{
|
||||
Track* t = track_manager->getTrack(track_name);
|
||||
Track* t = track_manager->getTrack(vote.m_track_name);
|
||||
if (t)
|
||||
{
|
||||
lap = (uint8_t)(fmaxf(1.0f,
|
||||
(float)t->getDefaultNumberOfLaps() *
|
||||
ServerConfig::m_auto_lap_ratio));
|
||||
vote.m_num_laps =
|
||||
(uint8_t)(fmaxf(1.0f,
|
||||
(float)t->getDefaultNumberOfLaps()
|
||||
*ServerConfig::m_auto_lap_ratio ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prevent someone send invalid vote
|
||||
track_name = *m_available_kts.second.begin();
|
||||
lap = (uint8_t)3;
|
||||
vote.m_track_name = *m_available_kts.second.begin();
|
||||
vote.m_num_laps = (uint8_t)3;
|
||||
}
|
||||
}
|
||||
else if (lap == 0)
|
||||
lap = (uint8_t)3;
|
||||
else if (vote.m_num_laps == 0)
|
||||
vote.m_num_laps = (uint8_t)3;
|
||||
}
|
||||
|
||||
NetworkString other = NetworkString(PROTOCOL_LOBBY_ROOM);
|
||||
std::string name = StringUtils::wideToUtf8(event->getPeer()
|
||||
->getPlayerProfiles()[0]->getName());
|
||||
other.setSynchronous(true);
|
||||
other.addUInt8(LE_VOTE)
|
||||
.encodeString(name).addUInt32(event->getPeer()->getHostId())
|
||||
.encodeString(track_name).addUInt8(lap).addUInt8(reverse);
|
||||
// Store vote:
|
||||
m_peers_votes[event->getPeer()->getHostId()] = vote;
|
||||
|
||||
// Now inform all clients about the vote
|
||||
NetworkString other = NetworkString(PROTOCOL_LOBBY_ROOM);
|
||||
|
||||
std::string name =
|
||||
StringUtils::wideToUtf8(event->getPeer()
|
||||
->getPlayerProfiles()[0]->getName());
|
||||
other.setSynchronous(true);
|
||||
other.addUInt8(LE_VOTE);
|
||||
|
||||
other.addUInt32(event->getPeer()->getHostId()) .encodeString(name)
|
||||
.addUInt32(event->getPeer()->getHostId());
|
||||
vote.encode(&other);
|
||||
|
||||
m_peers_votes[event->getPeerSP()] =
|
||||
std::make_tuple(track_name, lap, reverse == 1);
|
||||
sendMessageToPeers(&other);
|
||||
|
||||
} // playerVote
|
||||
} // handlePlayerVote
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
/** Select the track to be used based on all votes being received.
|
||||
* \param track_name Name of the track voted for.
|
||||
* \param num_laps Number of laps.
|
||||
* \param reverse If reverse track is to be used.
|
||||
* \result True if a vote from each player has been received, false otherwise.
|
||||
* \param winner The PeerVote that was picked.
|
||||
*/
|
||||
bool ServerLobby::handleAllVotes(std::string *track_name, int *num_laps,
|
||||
bool *reverse)
|
||||
bool ServerLobby::handleAllVotes(PeerVote *winner)
|
||||
{
|
||||
// Default settings if no votes at all
|
||||
RandomGenerator rg;
|
||||
std::set<std::string>::iterator it = m_available_kts.second.begin();
|
||||
std::advance(it, rg.get((int)m_available_kts.second.size()));
|
||||
*track_name = *it;
|
||||
*num_laps = UserConfigParams::m_num_laps;
|
||||
*reverse = track_name->size() % 2 == 0;
|
||||
// First remove all votes from disconnected hosts
|
||||
auto it = m_peers_votes.begin();
|
||||
while (it != m_peers_votes.end())
|
||||
{
|
||||
auto peer = STKHost::get()->findPeerByHostId(it->first);
|
||||
if (peer == nullptr)
|
||||
{
|
||||
it = m_peers_votes.erase(it);
|
||||
}
|
||||
else
|
||||
it++;
|
||||
}
|
||||
|
||||
|
||||
int cur_players = 0;
|
||||
// Count number of players
|
||||
unsigned int cur_players = 0;
|
||||
auto peers = STKHost::get()->getPeers();
|
||||
for (auto peer : peers)
|
||||
{
|
||||
if (peer->hasPlayerProfiles() && !peer->isWaitingForGame())
|
||||
cur_players ++;
|
||||
}
|
||||
if (cur_players == 0) return false;
|
||||
|
||||
std::map<std::string, unsigned> tracks;
|
||||
std::map<unsigned, unsigned> laps;
|
||||
std::map<bool, unsigned> reverses;
|
||||
|
||||
for (auto p : m_peers_votes)
|
||||
if (cur_players == 0 || m_peers_votes.size() < cur_players)
|
||||
{
|
||||
if (p.first.expired())
|
||||
continue;
|
||||
auto track_vote = tracks.find(std::get<0>(p.second));
|
||||
if (track_vote == tracks.end())
|
||||
tracks[std::get<0>(p.second)] = 1;
|
||||
else
|
||||
track_vote->second++;
|
||||
auto lap_vote = laps.find(std::get<1>(p.second));
|
||||
if (lap_vote == laps.end())
|
||||
laps[std::get<1>(p.second)] = 1;
|
||||
else
|
||||
lap_vote->second++;
|
||||
auto reverse_vote = reverses.find(std::get<2>(p.second));
|
||||
if (reverse_vote == reverses.end())
|
||||
reverses[std::get<2>(p.second)] = 1;
|
||||
else
|
||||
reverse_vote->second++;
|
||||
} // for p in m_peers_votes
|
||||
|
||||
unsigned vote = 0;
|
||||
auto track_vote = tracks.begin();
|
||||
for (auto c_vote = tracks.begin(); c_vote != tracks.end(); c_vote++)
|
||||
{
|
||||
if (c_vote->second > vote)
|
||||
{
|
||||
vote = c_vote->second;
|
||||
track_vote = c_vote;
|
||||
}
|
||||
}
|
||||
if (track_vote != tracks.end())
|
||||
{
|
||||
*track_name = track_vote->first;
|
||||
// Default settings if no votes at all
|
||||
RandomGenerator rg;
|
||||
std::set<std::string>::iterator it = m_available_kts.second.begin();
|
||||
std::advance(it, rg.get((int)m_available_kts.second.size()));
|
||||
winner->m_track_name = *it;
|
||||
winner->m_num_laps = UserConfigParams::m_num_laps;
|
||||
winner->m_reverse = winner->m_track_name.size() % 2 == 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
vote = 0;
|
||||
auto lap_vote = laps.begin();
|
||||
for (auto c_vote = laps.begin(); c_vote != laps.end(); c_vote++)
|
||||
{
|
||||
if (c_vote->second > vote)
|
||||
{
|
||||
vote = c_vote->second;
|
||||
lap_vote = c_vote;
|
||||
}
|
||||
}
|
||||
if (lap_vote != laps.end())
|
||||
{
|
||||
*num_laps = lap_vote->first;
|
||||
}
|
||||
RandomGenerator r;
|
||||
auto vote = m_peers_votes.begin();
|
||||
std::advance(vote, r.get(m_peers_votes.size()) );
|
||||
|
||||
vote = 0;
|
||||
auto reverse_vote = reverses.begin();
|
||||
for (auto c_vote = reverses.begin(); c_vote != reverses.end(); c_vote++)
|
||||
{
|
||||
if (c_vote->second > vote)
|
||||
{
|
||||
vote = c_vote->second;
|
||||
reverse_vote = c_vote;
|
||||
}
|
||||
}
|
||||
if (reverse_vote != reverses.end())
|
||||
{
|
||||
*reverse = reverse_vote->first;
|
||||
}
|
||||
*winner = vote->second;
|
||||
|
||||
return false;
|
||||
return m_peers_votes.size() == cur_players;
|
||||
} // handleAllVotes
|
||||
|
||||
|
||||
@@ -86,10 +86,6 @@ private:
|
||||
std::map<std::weak_ptr<STKPeer>, bool,
|
||||
std::owner_less<std::weak_ptr<STKPeer> > > m_peers_ready;
|
||||
|
||||
/** Vote from each peer. */
|
||||
std::map<std::weak_ptr<STKPeer>, std::tuple<std::string, uint8_t, bool>,
|
||||
std::owner_less<std::weak_ptr<STKPeer> > > m_peers_votes;
|
||||
|
||||
bool m_has_created_server_id_file;
|
||||
|
||||
/** It indicates if this server is unregistered with the stk server. */
|
||||
@@ -166,7 +162,7 @@ private:
|
||||
// kart selection
|
||||
void kartSelectionRequested(Event* event);
|
||||
// Track(s) votes
|
||||
void playerVote(Event *event);
|
||||
void handlePlayerVote(Event *event);
|
||||
void playerFinishedResult(Event *event);
|
||||
bool registerServer(bool now);
|
||||
void finishedLoadingWorldClient(Event *event);
|
||||
@@ -238,8 +234,7 @@ private:
|
||||
const std::string& iv,
|
||||
uint32_t online_id,
|
||||
const irr::core::stringw& online_name);
|
||||
bool handleAllVotes(std::string *track_name, int *num_laps,
|
||||
bool *reverse);
|
||||
bool handleAllVotes(PeerVote *winner);
|
||||
void getRankingForPlayer(std::shared_ptr<NetworkPlayerProfile> p);
|
||||
void submitRankingsToAddons();
|
||||
void computeNewRankings();
|
||||
|
||||
0
src/states_screens/online/network_kart_selection.cpp
Executable file → Normal file
0
src/states_screens/online/network_kart_selection.cpp
Executable file → Normal file
@@ -57,8 +57,6 @@ private:
|
||||
|
||||
int m_bottom_box_height;
|
||||
|
||||
std::map<std::string, core::stringw> m_vote_messages;
|
||||
|
||||
std::deque<std::string> m_random_track_list;
|
||||
|
||||
/** adds the tracks from the current track group into the tracks ribbon */
|
||||
@@ -104,17 +102,6 @@ public:
|
||||
void setNetworkTracks() { m_network_tracks = true; }
|
||||
// ------------------------------------------------------------------------
|
||||
void setQuitServer() { m_quit_server = true; }
|
||||
// ------------------------------------------------------------------------
|
||||
void resetVote()
|
||||
{
|
||||
m_vote_messages.clear();
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
void addVoteMessage(const std::string& user,
|
||||
const irr::core::stringw& message)
|
||||
{
|
||||
m_vote_messages[user] = message;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user