/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 1998-2000, Matthes Bender * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ * Copyright (c) 2009-2016, The OpenClonk Team and contributors * * Distributed under the terms of the ISC license; see accompanying file * "COPYING" for details. * * "Clonk" is a registered trademark of Matthes Bender, used with permission. * See accompanying file "TRADEMARK" for details. * * To redistribute this file separately, substitute the full license texts * for the above references. */ /* Control packets contain all player input in the message queue */ #include "C4Include.h" #include "control/C4Control.h" #include "script/C4AulExec.h" #include "object/C4Object.h" #include "control/C4GameSave.h" #include "gui/C4GameLobby.h" #include "network/C4Network2Dialogs.h" #include "lib/C4Random.h" #include "editor/C4Console.h" #include "lib/C4Log.h" #include "game/C4GraphicsSystem.h" #include "player/C4Player.h" #include "player/C4RankSystem.h" #include "control/C4RoundResults.h" #include "landscape/C4PXS.h" #include "landscape/C4MassMover.h" #include "gui/C4GameMessage.h" #include "landscape/C4Landscape.h" #include "game/C4Game.h" #include "game/C4GameScript.h" #include "player/C4PlayerList.h" #include "object/C4GameObjects.h" #include "control/C4GameControl.h" #include "gui/C4ScriptGuiWindow.h" #include "gui/C4MessageInput.h" #include "object/C4Def.h" #include "object/C4DefList.h" #ifndef NOAULDEBUG #include "script/C4AulDebug.h" #endif #include "script/C4AulExec.h" // *** C4ControlPacket C4ControlPacket::C4ControlPacket() : iByClient(::Control.ClientID()) { } C4ControlPacket::~C4ControlPacket() { } bool C4ControlPacket::LocalControl() const { return iByClient == ::Control.ClientID(); } void C4ControlPacket::SetByClient(int32_t inByClient) { iByClient = inByClient; } void C4ControlPacket::CompileFunc(StdCompiler *pComp) { // Section must be set by caller pComp->Value(mkNamingAdapt(mkIntPackAdapt(iByClient), "ByClient", -1)); } // *** C4Control C4Control::C4Control() { } C4Control::~C4Control() { Clear(); } void C4Control::Clear() { Pkts.Clear(); } bool C4Control::PreExecute() const { bool fReady = true; for (C4IDPacket *pPkt = firstPkt(); pPkt; pPkt = nextPkt(pPkt)) { // recheck packet type: Must be control if (pPkt->getPktType() & CID_First) { C4ControlPacket *pCtrlPkt = static_cast(pPkt->getPkt()); if (pCtrlPkt) fReady &= pCtrlPkt->PreExecute(); } else { LogF("C4Control::PreExecute: WARNING: Ignoring packet type %2x (not control.)", pPkt->getPktType()); } } return fReady; } void C4Control::Execute() const { for (C4IDPacket *pPkt = firstPkt(); pPkt; pPkt = nextPkt(pPkt)) { // recheck packet type: Must be control if (pPkt->getPktType() & CID_First) { C4ControlPacket *pCtrlPkt = static_cast(pPkt->getPkt()); if (pCtrlPkt) pCtrlPkt->Execute(); } else { LogF("C4Control::Execute: WARNING: Ignoring packet type %2x (not control.)", pPkt->getPktType()); } } } void C4Control::PreRec(C4Record *pRecord) const { for (C4IDPacket *pPkt = firstPkt(); pPkt; pPkt = nextPkt(pPkt)) { C4ControlPacket *pCtrlPkt = static_cast(pPkt->getPkt()); if (pCtrlPkt) pCtrlPkt->PreRec(pRecord); } } void C4Control::CompileFunc(StdCompiler *pComp) { pComp->Value(Pkts); } // *** C4ControlSet void C4ControlSet::Execute() const { switch (eValType) { case C4CVT_None: break; case C4CVT_ControlRate: // adjust control rate // host only if (iByClient != C4ClientIDHost) break; // adjust control rate ::Control.ControlRate += iData; ::Control.ControlRate = Clamp(::Control.ControlRate, 1, C4MaxControlRate); Game.Parameters.ControlRate = ::Control.ControlRate; // write back adjusted control rate to network settings if (::Control.isCtrlHost() && !::Control.isReplay() && ::Control.isNetwork()) Config.Network.ControlRate = ::Control.ControlRate; // always show msg ::GraphicsSystem.FlashMessage(FormatString(LoadResStr("IDS_NET_CONTROLRATE"),::Control.ControlRate,Game.FrameCounter).getData()); break; case C4CVT_DisableDebug: // force debug mode disabled { if (Game.DebugMode) { Game.DebugMode=false; ::GraphicsSystem.DeactivateDebugOutput(); } // save flag, log Game.Parameters.AllowDebug = false; C4Client *client = ::Game.Clients.getClientByID(iByClient); LogF("Debug mode forced disabled by %s", client ? client->getName() : ""); break; } break; case C4CVT_MaxPlayer: // host only if (iByClient != C4ClientIDHost) break; // not in league if (Game.Parameters.isLeague()) { Log("/set maxplayer disabled in league!"); C4GUI::GUISound("UI::Error"); break; } // set it Game.Parameters.MaxPlayers = iData; LogF("MaxPlayer = %d", (int)Game.Parameters.MaxPlayers); break; case C4CVT_TeamDistribution: // host only if (iByClient != C4ClientIDHost) break; // set new value Game.Teams.SetTeamDistribution(static_cast(iData)); break; case C4CVT_TeamColors: // host only if (!HostControl()) break; // set new value Game.Teams.SetTeamColors(!!iData); break; } } void C4ControlSet::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntAdapt(eValType), "Type", C4CVT_None)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(iData), "Data", 0)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlScript void C4ControlScript::Execute() const { const char *szScript = Script.getData(); // user script: from host only if ((iByClient != C4ClientIDHost) && !Console.Active) return; // only allow scripts when debug mode is not forbidden if (!Game.Parameters.AllowDebug) return; // execute C4PropList *pPropList = NULL; if (iTargetObj == SCOPE_Console) pPropList = ::GameScript.GetPropList(); else if (iTargetObj == SCOPE_Global) pPropList = ::ScriptEngine.GetPropList(); else if (!(pPropList = ::Objects.SafeObjectPointer(iTargetObj))) // default: Fallback to global context pPropList = ::ScriptEngine.GetPropList(); C4Value rVal(AulExec.DirectExec(pPropList, szScript, "console script", false, fUseVarsFromCallerContext ? AulExec.GetContext(AulExec.GetContextDepth()-1) : NULL)); #ifndef NOAULDEBUG C4AulDebug* pDebug; if ( (pDebug = C4AulDebug::GetDebugger()) ) { pDebug->ControlScriptEvaluated(szScript, rVal.GetDataString().getData()); } #endif // show messages // print script LogF("-> %s::%s", pPropList->GetName(), szScript); // print result if (!LocalControl()) { C4Network2Client *pClient = NULL; if (::Network.isEnabled()) pClient = ::Network.Clients.GetClientByID(iByClient); if (pClient) LogF(" = %s (by %s)", rVal.GetDataString().getData(), pClient->getName()); else LogF(" = %s (by client %d)", rVal.GetDataString().getData(), iByClient); } else LogF(" = %s", rVal.GetDataString().getData()); } void C4ControlScript::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(iTargetObj, "TargetObj", -1)); pComp->Value(mkNamingAdapt(fUseVarsFromCallerContext, "UseVarsFromCallerContext", false)); pComp->Value(mkNamingAdapt(Script, "Script", "")); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlMsgBoardReply void C4ControlMsgBoardReply::Execute() const { C4Object *target_object = ::Objects.SafeObjectPointer(target); C4Player *target_player = ::Players.Get(player); // remove query if (!target_player) return; if (!target_player->RemoveMessageBoardQuery(target_object)) return; // execute callback if answer present if (!reply) return; C4AulParSet pars(C4VString(reply), player); if (target_object) target_object->Call(PSF_InputCallback, &pars); else ::GameScript.Call(PSF_InputCallback, &pars); } void C4ControlMsgBoardReply::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(target, "TargetObj", -1)); pComp->Value(mkNamingAdapt(player, "Player", NO_OWNER)); pComp->Value(mkNamingAdapt(reply, "Reply", nullptr)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlMsgBoardCmd void C4ControlMsgBoardCmd::Execute() const { // don't handle this if the game isn't actually running if (!::Game.IsRunning) return; // fetch command script C4MessageBoardCommand *cmd = ::MessageInput.GetCommand(command.getData()); if (!cmd) return; StdCopyStrBuf script(cmd->Script); // interpolate parameters as required script.Replace("%player%", FormatString("%d", player).getData()); if (parameter) { script.Replace("%d", FormatString("%d", std::atoi(parameter.getData())).getData()); StdCopyStrBuf escaped_param(parameter); escaped_param.EscapeString(); script.Replace("%s", escaped_param.getData()); } // Run script C4Value rv(::AulExec.DirectExec(::ScriptEngine.GetPropList(), script.getData(), "message board command")); #ifndef NOAULDEBUG C4AulDebug* pDebug = C4AulDebug::GetDebugger(); if (pDebug) pDebug->ControlScriptEvaluated(script.getData(), rv.GetDataString().getData()); #endif } void C4ControlMsgBoardCmd::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(player, "Player", NO_OWNER)); pComp->Value(mkNamingAdapt(command, "Command")); pComp->Value(mkNamingAdapt(parameter, "Parameter")); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlPlayerSelect C4ControlPlayerSelect::C4ControlPlayerSelect(int32_t iPlr, const C4ObjectList &Objs, bool fIsAlt) : iPlr(iPlr), fIsAlt(fIsAlt), iObjCnt(Objs.ObjectCount()) { pObjNrs = new int32_t[iObjCnt]; int32_t i = 0; for (C4Object *obj : Objs) pObjNrs[i++] = obj->Number; assert(i == iObjCnt); } void C4ControlPlayerSelect::Execute() const { // get player C4Player *pPlr = ::Players.Get(iPlr); if (!pPlr) return; // Check object list C4Object *pObj; int32_t iControlChecksum = 0; for (int32_t i = 0; i < iObjCnt; i++) if ((pObj = ::Objects.SafeObjectPointer(pObjNrs[i]))) { iControlChecksum += pObj->Number * (iControlChecksum+4787821); // user defined object selection: callback to object if (pObj->Category & C4D_MouseSelect) { if (fIsAlt) pObj->Call(PSF_MouseSelectionAlt, &C4AulParSet(iPlr)); else pObj->Call(PSF_MouseSelection, &C4AulParSet(iPlr)); } } // count pPlr->CountControl(C4Player::PCID_Command, iControlChecksum); } void C4ControlPlayerSelect::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(iPlr, "Player", -1)); pComp->Value(mkNamingAdapt(fIsAlt, "IsAlt", false)); pComp->Value(mkNamingAdapt(iObjCnt, "ObjCnt", 0)); // Compile array if (pComp->isCompiler()) { delete[] pObjNrs; pObjNrs = new int32_t [iObjCnt]; } pComp->Value(mkNamingAdapt(mkArrayAdapt(pObjNrs, iObjCnt), "Objs", 0)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlPlayerControl void C4ControlPlayerControl::Execute() const { C4PlayerControl *pTargetCtrl = NULL; if (iPlr == -1) { // neutral control packet: Execute in global control } else { // player-based control: Execute on control owned by player C4Player *pPlr=::Players.Get(iPlr); if (pPlr) { pTargetCtrl = &(pPlr->Control); } } if (pTargetCtrl) pTargetCtrl->ExecuteControlPacket(this); } void C4ControlPlayerControl::ControlItem::CompileFunc(StdCompiler *pComp) { pComp->Value(iControl); pComp->Separator(); pComp->Value(iTriggerMode); } void C4ControlPlayerControl::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntPackAdapt(iPlr), "Player", -1)); pComp->Value(mkNamingAdapt(mkIntAdapt(state), "State", 0)); pComp->Value(mkNamingAdapt(ExtraData, "ExtraData", C4KeyEventData())); pComp->Value(mkNamingAdapt(mkSTLContainerAdapt(ControlItems), "Controls", ControlItemVec())); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlPlayerMouse C4ControlPlayerMouse *C4ControlPlayerMouse::Hover(const C4Player *player, const C4Object *target, const C4Object *old_target, const C4Object *drag) { assert(player != nullptr); if (!player) return nullptr; auto control = new C4ControlPlayerMouse(); control->action = CPM_Hover; control->player = player->Number; control->drag_obj = drag ? drag->Number : 0; control->target_obj = target ? target->Number : 0; control->old_obj = old_target ? old_target->Number : 0; return control; } C4ControlPlayerMouse *C4ControlPlayerMouse::DragDrop(const C4Player *player, const C4Object *target, const C4Object *drag) { assert(player != nullptr); if (!player) return nullptr; auto control = new C4ControlPlayerMouse(); control->action = CPM_Drop; control->player = player->Number; control->drag_obj = drag ? drag->Number : 0; control->target_obj = target ? target->Number : 0; return control; } void C4ControlPlayerMouse::Execute() const { const char *callback_name = nullptr; C4AulParSet pars(player); switch (action) { case CPM_NoAction: return; case CPM_Hover: // Mouse movement, object hover state changed callback_name = PSF_MouseHover; pars[1] = C4VObj(::Objects.SafeObjectPointer(old_obj)); pars[2] = C4VObj(::Objects.SafeObjectPointer(target_obj)); pars[3] = C4VObj(::Objects.SafeObjectPointer(drag_obj)); break; case CPM_Drop: // Drag/Drop operation callback_name = PSF_MouseDragDrop; pars[1] = C4VObj(::Objects.SafeObjectPointer(drag_obj)); pars[2] = C4VObj(::Objects.SafeObjectPointer(target_obj)); break; } // Do call if (!callback_name) return; ::ScriptEngine.Call(callback_name, &pars); } void C4ControlPlayerMouse::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(action, "Action")); pComp->Value(mkNamingAdapt(player, "Player", NO_OWNER)); pComp->Value(mkNamingAdapt(target_obj, "TargetObj")); pComp->Value(mkNamingAdapt(drag_obj, "DragObj")); pComp->Value(mkNamingAdapt(old_obj, "OldObj")); } // *** C4ControlMenuCommand C4ControlMenuCommand::C4ControlMenuCommand(int32_t actionID, int32_t player, int32_t menuID, int32_t subwindowID, C4Object *target, int32_t actionType) : actionID(actionID), player(player), menuID(menuID), subwindowID(subwindowID), target(target ? target->Number : 0), actionType(actionType) { } void C4ControlMenuCommand::Execute() const { // invalid action? The action needs to be in bounds! if (actionType < 0 || actionType >= C4ScriptGuiWindowPropertyName::_lastProp) { // this could only come from a malicious attempt to crash the engine! Log("Warning: invalid action type for C4ControlMenuCommand!"); return; } C4ScriptGuiWindow *menu = ::Game.ScriptGuiRoot->GetChildByID(menuID); // menu was closed? if (!menu) return; C4Object *obj = target ? ::Objects.ObjectPointer(target) : 0; // target has been removed in the meantime? abort now if (target && !obj) return; menu->ExecuteCommand(actionID, player, subwindowID, actionType, obj); } void C4ControlMenuCommand::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntPackAdapt(actionID), "ID", -1)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(player), "Player", -1)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(menuID), "Menu", 0)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(subwindowID), "Window", 0)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(actionType), "Action", 0)); pComp->Value(mkNamingAdapt(target, "Target", 0)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlPlayerAction C4ControlPlayerAction::C4ControlPlayerAction(const C4Player *source) : action(CPA_NoAction), source(source ? source->Number : NO_OWNER), target(NO_OWNER), param_int(0) { } C4ControlPlayerAction *C4ControlPlayerAction::Surrender(const C4Player *source) { assert(source); C4ControlPlayerAction *control = new C4ControlPlayerAction(source); control->action = CPA_Surrender; return control; } C4ControlPlayerAction *C4ControlPlayerAction::Eliminate(const C4Player *source) { assert(source); C4ControlPlayerAction *control = new C4ControlPlayerAction(source); control->action = CPA_Eliminate; return control; } C4ControlPlayerAction *C4ControlPlayerAction::ActivateGoal(const C4Player *source, const C4Object *goal) { assert(source); assert(goal); C4ControlPlayerAction *control = new C4ControlPlayerAction(source); control->action = CPA_ActivateGoal; control->target = goal->Number; return control; } C4ControlPlayerAction *C4ControlPlayerAction::ActivateGoalMenu(const C4Player *source) { assert(source); C4ControlPlayerAction *control = new C4ControlPlayerAction(source); control->action = CPA_ActivateGoalMenu; return control; } C4ControlPlayerAction *C4ControlPlayerAction::SetHostility(const C4Player *source, const C4Player *target, bool hostile) { assert(source); assert(target); C4ControlPlayerAction *control = new C4ControlPlayerAction(source); control->action = CPA_SetHostility; control->target = target ? target->Number : NO_OWNER; control->param_int = hostile; return control; } C4ControlPlayerAction *C4ControlPlayerAction::SetTeam(const C4Player *source, int32_t team) { assert(source); C4ControlPlayerAction *control = new C4ControlPlayerAction(source); control->action = CPA_SetTeam; control->target = team; return control; } C4ControlPlayerAction *C4ControlPlayerAction::InitScenarioPlayer(const C4Player *source, int32_t team) { assert(source); C4ControlPlayerAction *control = new C4ControlPlayerAction(source); control->action = CPA_InitScenarioPlayer; control->target = team; return control; } C4ControlPlayerAction *C4ControlPlayerAction::InitPlayerControl(const C4Player *source, const C4PlayerControlAssignmentSet *ctrl_set) { assert(source); C4ControlPlayerAction *control = new C4ControlPlayerAction(source); control->action = CPA_InitPlayerControl; if (ctrl_set) { control->param_str = ctrl_set->GetName(); if (ctrl_set->HasKeyboard()) control->param_int |= CPA_IPC_HasKeyboard; if (ctrl_set->HasMouse()) control->param_int |= CPA_IPC_HasMouse; if (ctrl_set->HasGamepad()) control->param_int |= CPA_IPC_HasGamepad; } return control; } void C4ControlPlayerAction::Execute() const { // The originating player must exist C4Player *source_player = ::Players.Get(source); if (!source_player) return; switch (action) { case CPA_Surrender: source_player->Surrender(); break; case CPA_Eliminate: source_player->Eliminate(); break; case CPA_ActivateGoal: { // Make sure the object actually exists C4Object *goal = ::Objects.SafeObjectPointer(target); if (!goal) return; // Call it C4AulParSet pars(source_player->Number); goal->Call("Activate", &pars); break; } case CPA_ActivateGoalMenu: // open menu source_player->Menu.ActivateGoals(source_player->Number, source_player->LocalControl && !::Control.isReplay()); break; case CPA_SetHostility: { // Can only set hostility towards a player that exists C4Player *target_player = ::Players.Get(target); if (!target_player) return; // Proxy the hostility change through C4Aul, in case a script wants to capture it C4AulParSet pars(source_player->Number, target_player->Number, param_int != 0); ::ScriptEngine.Call("SetHostility", &pars); break; } case CPA_SetTeam: { // Make sure team switching is allowed in the first place if (!::Game.Teams.IsTeamSwitchAllowed()) return; // We can't change teams to one that doesn't exist C4Team *team = ::Game.Teams.GetTeamByID(target); if (!team && target != TEAMID_New) return; // Proxy the team switch through C4Aul, in case a script wants to capture it C4AulParSet pars(source_player->Number, target); ::ScriptEngine.Call("SetPlayerTeam", &pars); break; } case CPA_InitScenarioPlayer: { // Proxy the call through C4Aul, in case a script wants to capture it C4AulParSet pars(source_player->Number, target); ::ScriptEngine.Call("InitScenarioPlayer", &pars); break; } case CPA_InitPlayerControl: { // Notify scripts about player control selection const char *callback_name = PSF_InitializePlayerControl; C4AulParSet pars(source_player->Number); // If the player is using a control set, its name is stored in param_str if (param_str) { pars[1] = C4VString(param_str); pars[2] = C4VBool(CPA_IPC_HasKeyboard == (param_int & CPA_IPC_HasKeyboard)); pars[3] = C4VBool(CPA_IPC_HasMouse == (param_int & CPA_IPC_HasMouse)); pars[4] = C4VBool(CPA_IPC_HasGamepad == (param_int & CPA_IPC_HasGamepad)); } ::ScriptEngine.Call(callback_name, &pars); break; } case CPA_NoAction: break; } } void C4ControlPlayerAction::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(source, "Player", NO_OWNER)); const StdEnumEntry ActionNames[] = { { "Surrender", CPA_Surrender }, { "ActivateGoal", CPA_ActivateGoal }, { "ActivateGoalMenu", CPA_ActivateGoalMenu }, { "Eliminate", CPA_Eliminate }, { "SetHostility", CPA_SetHostility }, { "SetTeam", CPA_SetTeam }, { "InitScenarioPlayer", CPA_InitScenarioPlayer }, { "InitPlayerControl", CPA_InitPlayerControl }, { NULL, CPA_NoAction } }; pComp->Value(mkNamingAdapt(mkEnumAdapt(action, ActionNames), "Action", CPA_NoAction)); pComp->Value(mkNamingAdapt(target, "Target", NO_OWNER)); pComp->Value(mkNamingAdapt(param_int, "DataI", 0)); pComp->Value(mkNamingAdapt(param_str, "DataS", nullptr)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlSyncCheck C4ControlSyncCheck::C4ControlSyncCheck() { } void C4ControlSyncCheck::Set() { Frame = Game.FrameCounter; ControlTick = ::Control.ControlTick; RandomCount = ::RandomCount; AllCrewPosX = GetAllCrewPosX(); PXSCount = ::PXS.Count; MassMoverIndex = ::MassMover.CreatePtr; ObjectCount = ::Objects.ObjectCount(); ObjectEnumerationIndex = C4PropListNumbered::GetEnumerationIndex(); SectShapeSum = ::Objects.Sectors.getShapeSum(); } int32_t C4ControlSyncCheck::GetAllCrewPosX() { int32_t cpx=0; for (C4Player *pPlr=::Players.First; pPlr; pPlr=pPlr->Next) for (C4Object *member : pPlr->Crew) cpx += fixtoi(member->fix_x, 100); return cpx; } void C4ControlSyncCheck::Execute() const { // control host? if (::Control.isCtrlHost()) return; // get the saved sync check data C4ControlSyncCheck* pSyncCheck = ::Control.GetSyncCheck(Frame), &SyncCheck = *pSyncCheck; if (!pSyncCheck) { ::Control.SyncChecks.Add(CID_SyncCheck, new C4ControlSyncCheck(*this)); return; } // Not equal if ( Frame != pSyncCheck->Frame ||(ControlTick != pSyncCheck->ControlTick && !::Control.isReplay()) || RandomCount != pSyncCheck->RandomCount || AllCrewPosX != pSyncCheck->AllCrewPosX || PXSCount != pSyncCheck->PXSCount || MassMoverIndex != pSyncCheck->MassMoverIndex || ObjectCount != pSyncCheck->ObjectCount || ObjectEnumerationIndex != pSyncCheck->ObjectEnumerationIndex || SectShapeSum != pSyncCheck->SectShapeSum) { const char *szThis = "Client", *szOther = ::Control.isReplay() ? "Rec ":"Host"; if (iByClient != ::Control.ClientID()) { const char *szTemp = szThis; szThis = szOther; szOther = szTemp; } // Message LogFatal("Network: Synchronization loss!"); LogFatal(FormatString("Network: %s Frm %i Ctrl %i Rnc %i Cpx %i PXS %i MMi %i Obc %i Oei %i Sct %i", szThis, Frame,ControlTick,RandomCount,AllCrewPosX,PXSCount,MassMoverIndex,ObjectCount,ObjectEnumerationIndex, SectShapeSum).getData()); LogFatal(FormatString("Network: %s Frm %i Ctrl %i Rnc %i Cpx %i PXS %i MMi %i Obc %i Oei %i Sct %i", szOther, SyncCheck.Frame,SyncCheck.ControlTick,SyncCheck.RandomCount,SyncCheck.AllCrewPosX,SyncCheck.PXSCount,SyncCheck.MassMoverIndex,SyncCheck.ObjectCount,SyncCheck.ObjectEnumerationIndex, SyncCheck.SectShapeSum).getData()); StartSoundEffect("UI::SyncError"); #ifdef _DEBUG // Debug safe C4GameSaveNetwork SaveGame(false); SaveGame.Save(Config.AtExePath("Desync.ocs")); #endif // league: Notify regular client disconnect within the game ::Network.LeagueNotifyDisconnect(C4ClientIDHost, C4LDR_Desync); // Deactivate / end if (::Control.isReplay()) Game.DoGameOver(); else if (::Control.isNetwork()) { Game.RoundResults.EvaluateNetwork(C4RoundResults::NR_NetError, "Network: Synchronization loss!"); ::Network.Clear(); } } } void C4ControlSyncCheck::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntPackAdapt(Frame), "Frame", -1)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(ControlTick), "ControlTick", 0)); pComp->Value(mkNamingAdapt(RandomCount, "RandomCount", 0)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(AllCrewPosX), "AllCrewPosX", 0)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(PXSCount), "PXSCount", 0)); pComp->Value(mkNamingAdapt(MassMoverIndex, "MassMoverIndex", 0)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(ObjectCount), "ObjectCount", 0)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(ObjectEnumerationIndex), "ObjectEnumerationIndex", 0)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(SectShapeSum), "SectShapeSum", 0)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlSynchronize void C4ControlSynchronize::Execute() const { Game.Synchronize(fSavePlrFiles); if (fSyncClearance) Game.SyncClearance(); } void C4ControlSynchronize::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(fSavePlrFiles, "SavePlrs", false)); pComp->Value(mkNamingAdapt(fSyncClearance, "SyncClear", false)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlClientJoin void C4ControlClientJoin::Execute() const { // host only if (iByClient != C4ClientIDHost) return; // add client C4Client *pClient = Game.Clients.Add(Core); if (!pClient) return; // log LogF(LoadResStr("IDS_NET_CLIENT_JOIN"), Core.getName()); // lobby callback C4GameLobby::MainDlg *pLobby = ::Network.GetLobby(); if (pLobby) pLobby->OnClientJoin(pClient); // console callback if (Console.Active) Console.UpdateMenus(); } void C4ControlClientJoin::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(Core, "ClientCore")); C4ControlPacket::CompileFunc(pComp); } // *** C4Control void C4ControlClientUpdate::Execute() const { // host only if (iByClient != C4ClientIDHost && eType != CUT_SetReady) return; // find client C4Client *pClient = Game.Clients.getClientByID(iID); if (!pClient) return; StdCopyStrBuf strClient(LoadResStr(pClient->isLocal() ? "IDS_NET_LOCAL_CLIENT" : "IDS_NET_CLIENT")); // do whatever specified switch (eType) { case CUT_None: break; case CUT_Activate: // nothing to do? if (pClient->isActivated() == !!iData) break; // log LogF(LoadResStr(iData ? "IDS_NET_CLIENT_ACTIVATED" : "IDS_NET_CLIENT_DEACTIVATED"), strClient.getData(), pClient->getName()); // activate/deactivate pClient->SetActivated(!!iData); // local? if (pClient->isLocal()) ::Control.SetActivated(!!iData); break; case CUT_SetObserver: // nothing to do? if (pClient->isObserver()) break; // log LogF(LoadResStr("IDS_NET_CLIENT_OBSERVE"), strClient.getData(), pClient->getName()); // set observer (will deactivate) pClient->SetObserver(); // local? if (pClient->isLocal()) ::Control.SetActivated(false); // remove all players ("soft kick") ::Players.RemoveAtClient(iID, true); break; case CUT_SetReady: { // nothing to do? if (pClient->isLobbyReady() == !!iData) break; // ready/unready (while keeping track of time) time_t last_change_time = MinReadyAnnouncementDelay; pClient->SetLobbyReady(!!iData, &last_change_time); // log to others, but don't spam if (last_change_time >= MinReadyAnnouncementDelay) { if (!pClient->isLocal()) { LogF(LoadResStr(iData ? "IDS_NET_CLIENT_READY" : "IDS_NET_CLIENT_UNREADY"), strClient.getData(), pClient->getName()); } // Also update icons C4GameLobby::MainDlg *lobby = ::Network.GetLobby(); if (lobby) lobby->OnClientReadyStateChange(); } break; } } } void C4ControlClientUpdate::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntAdaptT(eType), "Type", CUT_None)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(iID), "ClientID", C4ClientIDUnknown)); if (eType == CUT_Activate) pComp->Value(mkNamingAdapt(mkIntPackAdapt(iData), "Data", 0)); if (eType == CUT_SetReady) pComp->Value(mkNamingAdapt(mkIntPackAdapt(iData), "Data", 0)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlClientRemove void C4ControlClientRemove::Execute() const { // host only if (iByClient != C4ClientIDHost) return; if (iID == C4ClientIDHost) return; // find client C4Client *pClient = Game.Clients.getClientByID(iID); if (!pClient) { // TODO: in replays, client list is not yet synchronized // remove players anyway if (::Control.isReplay()) ::Players.RemoveAtClient(iID, true); return; } StdCopyStrBuf strClient(LoadResStr(pClient->isLocal() ? "IDS_NET_LOCAL_CLIENT" : "IDS_NET_CLIENT")); // local? if (pClient->isLocal()) { StdStrBuf sMsg; sMsg.Format(LoadResStr("IDS_NET_CLIENT_REMOVED"), strClient.getData(), pClient->getName(), strReason.getData()); Log(sMsg.getData()); Game.RoundResults.EvaluateNetwork(C4RoundResults::NR_NetError, sMsg.getData()); ::Control.ChangeToLocal(); return; } // remove client if (!Game.Clients.Remove(pClient)) return; // log LogF(LoadResStr("IDS_NET_CLIENT_REMOVED"), strClient.getData(), pClient->getName(), strReason.getData()); // remove all players ::Players.RemoveAtClient(iID, true); // remove all resources if (::Network.isEnabled()) ::Network.ResList.RemoveAtClient(iID); // lobby callback C4GameLobby::MainDlg *pLobby = ::Network.GetLobby(); if (pLobby && ::pGUI) pLobby->OnClientPart(pClient); // player list callback ::Network.Players.OnClientPart(pClient); // console callback if (Console.Active) Console.UpdateMenus(); // delete delete pClient; } void C4ControlClientRemove::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntPackAdapt(iID), "ClientID", CUT_None)); pComp->Value(mkNamingAdapt(strReason, "Reason", "")); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlJoinPlayer C4ControlJoinPlayer::C4ControlJoinPlayer(const char *szFilename, int32_t iAtClient, int32_t iIDInfo, const C4Network2ResCore &ResCore) : Filename(szFilename, true), iAtClient(iAtClient), idInfo(iIDInfo), fByRes(true), ResCore(ResCore) { } C4ControlJoinPlayer::C4ControlJoinPlayer(const char *szFilename, int32_t iAtClient, int32_t iIDInfo) : Filename(szFilename, true), iAtClient(iAtClient), idInfo(iIDInfo), fByRes(false) { // load from file if filename is given - which may not be the case for script players StdStrBuf filename; if (szFilename && Reloc.LocateItem(szFilename, filename)) { bool file_is_temp = false; if (DirectoryExists(filename.getData())) { // the player file is unpacked - temp pack and read StdStrBuf filename_buf; filename_buf.Copy(Config.AtTempPath(GetFilenameOnly(filename.getData()))); MakeTempFilename(&filename_buf); if (C4Group_PackDirectoryTo(filename.getData(), filename_buf.getData())) { filename.Take(filename_buf); file_is_temp = true; } else { // pack failed LogF("[!]Error packing player file %s to %s for join: Pack failed.", filename.getData(), filename_buf.getData()); assert(false); } } bool fSuccess = PlrData.LoadFromFile(filename.getData()); if (!fSuccess) { LogF("[!]Error loading player file from %s.", filename.getData()); assert(false); } if (file_is_temp) EraseFile(filename.getData()); } else if(szFilename) { LogF("[!]Error loading player file from %s.", szFilename); assert(false); } } void C4ControlJoinPlayer::Execute() const { const char *szFilename = Filename.getData(); // get client C4Client *pClient = Game.Clients.getClientByID(iAtClient); if (!pClient) return; // get info C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(idInfo); if (!pInfo) { LogF("ERROR: Ghost player join: No info for %d", idInfo); assert(false); return; } else if (LocalControl()) { // Local player: Just join from local file Game.JoinPlayer(szFilename, iAtClient, pClient->getName(), pInfo); } else if (!fByRes) { if (PlrData.getSize()) { // create temp file StdStrBuf PlayerFilename; PlayerFilename.Format("%s-%s",pClient->getName(),GetFilename(szFilename)); PlayerFilename = Config.AtTempPath(PlayerFilename.getData()); // copy to it if (PlrData.SaveToFile(PlayerFilename.getData())) { Game.JoinPlayer(PlayerFilename.getData(), iAtClient, pClient->getName(), pInfo); EraseFile(PlayerFilename.getData()); } } else if (pInfo->GetType() == C4PT_Script) { // script players may join without data Game.JoinPlayer(NULL, iAtClient, pClient->getName(), pInfo); } else { // no player data for user player present: Must not happen LogF("ERROR: Ghost player join: No player data for %s", (const char*)pInfo->GetName()); assert(false); return; } } else if (::Control.isNetwork()) { // Find resource C4Network2Res::Ref pRes = ::Network.ResList.getRefRes(ResCore.getID()); if (pRes && pRes->isComplete()) Game.JoinPlayer(pRes->getFile(), iAtClient, pClient->getName(), pInfo); } else if (::Control.isReplay()) { // Expect player in scenario file StdStrBuf PlayerFilename; PlayerFilename.Format("%s" DirSep "%d-%s", Game.ScenarioFilename, ResCore.getID(), GetFilename(ResCore.getFileName())); Game.JoinPlayer(PlayerFilename.getData(), iAtClient, pClient ? pClient->getName() : "Unknown", pInfo); } else // Shouldn't happen assert(false); } void C4ControlJoinPlayer::Strip() { // By resource? Can't touch player file, then. if (fByRes) return; // create temp file StdStrBuf PlayerFilename; PlayerFilename = GetFilename(Filename.getData()); PlayerFilename = Config.AtTempPath(PlayerFilename.getData()); // Copy to it if (PlrData.SaveToFile(PlayerFilename.getData())) { // open as group C4Group Grp; if (!Grp.Open(PlayerFilename.getData())) { EraseFile(PlayerFilename.getData()); return; } // remove bigicon, if the file size is too large size_t iBigIconSize=0; if (Grp.FindEntry(C4CFN_BigIcon, NULL, &iBigIconSize)) if (iBigIconSize > C4NetResMaxBigicon*1024) Grp.Delete(C4CFN_BigIcon); Grp.Close(); // Set new data StdBuf NewPlrData; if (!NewPlrData.LoadFromFile(PlayerFilename.getData())) { EraseFile(PlayerFilename.getData()); return; } PlrData = std::move(NewPlrData); // Done EraseFile(PlayerFilename.getData()); } } bool C4ControlJoinPlayer::PreExecute() const { // all data included in control packet? if (!fByRes) return true; // client lost? if (!Game.Clients.getClientByID(iAtClient)) return true; // network only if (!::Control.isNetwork()) return true; // search resource C4Network2Res::Ref pRes = ::Network.ResList.getRefRes(ResCore.getID()); // doesn't exist? start loading if (!pRes) { pRes = ::Network.ResList.AddByCore(ResCore, true); } if (!pRes) return true; // is loading or removed? return !pRes->isLoading(); } void C4ControlJoinPlayer::PreRec(C4Record *pRecord) { if (!pRecord) return; if (fByRes) { // get local file by id C4Network2Res::Ref pRes = ::Network.ResList.getRefRes(ResCore.getID()); if (!pRes || pRes->isRemoved()) return; // create a copy of the resource StdStrBuf szTemp; szTemp.Copy(pRes->getFile()); MakeTempFilename(&szTemp); if (C4Group_CopyItem(pRes->getFile(), szTemp.getData())) { // add to record StdStrBuf szTarget = FormatString("%d-%s", ResCore.getID(), GetFilename(ResCore.getFileName())); pRecord->AddFile(szTemp.getData(), szTarget.getData(), true); } } else { // player data raw within control: Will be used directly in record } } void C4ControlJoinPlayer::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkNetFilenameAdapt(Filename), "Filename", "")); pComp->Value(mkNamingAdapt(mkIntPackAdapt(iAtClient), "AtClient", -1)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(idInfo), "InfoID", -1)); pComp->Value(mkNamingAdapt(fByRes, "ByRes", false)); if (fByRes) pComp->Value(mkNamingAdapt(ResCore, "ResCore")); else pComp->Value(mkNamingAdapt(PlrData, "PlrData")); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlEMMoveObject C4ControlEMMoveObject::C4ControlEMMoveObject(C4ControlEMObjectAction eAction, C4Real tx, C4Real ty, C4Object *pTargetObj, int32_t iObjectNum, int32_t *pObjects, const char *szScript) : eAction(eAction), tx(tx), ty(ty), iTargetObj(pTargetObj ? pTargetObj->Number : 0), iObjectNum(iObjectNum), pObjects(pObjects), StringParam(szScript, true) { } C4ControlEMMoveObject *C4ControlEMMoveObject::CreateObject(const C4ID &id, C4Real x, C4Real y) { auto ctl = new C4ControlEMMoveObject(EMMO_Create, x, y, nullptr); ctl->StringParam = id.ToString(); return ctl; } C4ControlEMMoveObject::~C4ControlEMMoveObject() { delete [] pObjects; pObjects = NULL; } void C4ControlEMMoveObject::MoveObject(C4Object *moved_object) const { // move given object by this->tx/ty and do callbacks if (!moved_object || !moved_object->Status) return; int32_t old_x = moved_object->GetX(), old_y = moved_object->GetY(); C4Real tx = this->tx; if (moved_object->Def->NoHorizontalMove) tx = Fix0; moved_object->ForcePosition(moved_object->fix_x + tx, moved_object->fix_y + ty); moved_object->xdir = moved_object->ydir = 0; moved_object->Mobile = false; C4AulParSet pars(C4VInt(old_x), C4VInt(old_y)); moved_object->Call(PSF_EditCursorMoved, &pars); } void C4ControlEMMoveObject::Execute() const { bool fLocalCall = LocalControl(); switch (eAction) { case EMMO_Move: { if (!pObjects) break; // move all given objects C4Object *pObj; for (int i=0; iStatus) { MoveObject(pObj); // attached objects: Also move attachment target while (pObj->GetProcedure() == DFA_ATTACH) { pObj = pObj->Action.Target; if (!pObj) break; // leftover action cancelled next frame for (int j = 0; j < iObjectNum; ++j) if (pObjects[j] == pObj->Number) { pObj = NULL; break; } // ensure we aren't moving twice if (!pObj) break; MoveObject(pObj); } } } break; case EMMO_Enter: { if (!pObjects) break; // enter all given objects into target C4Object *pObj, *pTarget = ::Objects.SafeObjectPointer(iTargetObj); if (pTarget) for (int i=0; iEnter(pTarget); } break; case EMMO_Duplicate: { if (!pObjects) break; // perform duplication C4Object *pOldObj, *pObj; for (int i=0; iGetPrototype(), pOldObj, pOldObj->Owner, pOldObj->GetX(), pOldObj->GetY()); if (pObj && pObj->Status) { // local call? adjust selection then // do callbacks for all clients for sync reasons if (fLocalCall) Console.EditCursor.GetSelection().Add(pObj, C4ObjectList::stNone); C4AulParSet pars(C4VObj(pObj)); if (pOldObj->Status) pOldObj->Call(PSF_EditCursorDeselection, &pars); if (pObj->Status) pObj->Call(PSF_EditCursorSelection); } } // update status if (fLocalCall) { if (fLocalCall) for (int i = 0; iAssignRemoval(); } break; case EMMO_Exit: { if (!pObjects) return; // exit all objects C4Object *pObj; for (int i=0; iExit(pObj->GetX(), pObj->GetY(), pObj->GetR()); } break; case EMMO_Select: { // callback to script C4Object *target = ::Objects.SafeObjectPointer(iTargetObj); if (!target) return; target->Call(PSF_EditCursorSelection); } break; case EMMO_Deselect: { // callback to script C4Object *target = ::Objects.SafeObjectPointer(iTargetObj), *next_selection = NULL; if (!target) return; // next selection may be passed to EditCursorSelection if (iObjectNum) next_selection = ::Objects.SafeObjectPointer(pObjects[0]); C4AulParSet pars(C4VObj(next_selection)); target->Call(PSF_EditCursorDeselection, &pars); } break; case EMMO_Create: { ::Game.CreateObject(C4ID(StringParam), nullptr, NO_OWNER, fixtoi(tx), fixtoi(ty)); } break; } // update property dlg & status bar if (fLocalCall) Console.EditCursor.OnSelectionChanged(); } void C4ControlEMMoveObject::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntAdaptT(eAction), "Action")); pComp->Value(mkNamingAdapt(tx, "tx", 0)); pComp->Value(mkNamingAdapt(ty, "ty", 0)); pComp->Value(mkNamingAdapt(iTargetObj, "TargetObj", -1)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(iObjectNum), "ObjectNum", 0)); if (pComp->isCompiler()) { delete [] pObjects; pObjects = new int32_t [iObjectNum]; } pComp->Value(mkNamingAdapt(mkArrayAdapt(pObjects, iObjectNum), "Objs", -1)); if (eAction == EMMO_Script) pComp->Value(mkNamingAdapt(StringParam, "Script", "")); else if (eAction == EMMO_Create) pComp->Value(mkNamingAdapt(StringParam, "ID", "")); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlEMDrawTool C4ControlEMDrawTool::C4ControlEMDrawTool(C4ControlEMDrawAction eAction, LandscapeMode iMode, int32_t iX, int32_t iY, int32_t iX2, int32_t iY2, int32_t iGrade, const char *szMaterial, const char *szTexture, const char *szBackMaterial, const char *szBackTexture) : eAction(eAction), iMode(iMode), iX(iX), iY(iY), iX2(iX2), iY2(iY2), iGrade(iGrade), Material(szMaterial, true), Texture(szTexture, true), BackMaterial(szBackMaterial, true), BackTexture(szBackTexture, true) { } void C4ControlEMDrawTool::Execute() const { // set new mode if (eAction == EMDT_SetMode) { Console.ToolsDlg.SetLandscapeMode(iMode, true); return; } // check current mode assert(::Landscape.GetMode() == iMode); if (::Landscape.GetMode() != iMode) return; // assert validity of parameters if (!Material.getSize()) return; const char *szMaterial = Material.getData(), *szTexture = Texture.getData(); const char *szBackMaterial = BackMaterial.getData(), *szBackTexture = BackTexture.getData(); // perform action switch (eAction) { case EMDT_Brush: // brush tool if (!Texture.getSize()) break; ::Landscape.DrawBrush(iX, iY, iGrade, szMaterial, szTexture, szBackMaterial, szBackTexture); break; case EMDT_Line: // line tool if (!Texture.getSize()) break; ::Landscape.DrawLine(iX,iY,iX2,iY2, iGrade, szMaterial, szTexture, szBackMaterial, szBackTexture); break; case EMDT_Rect: // rect tool if (!Texture.getSize()) break; ::Landscape.DrawBox(iX,iY,iX2,iY2, iGrade, szMaterial, szTexture, szBackMaterial, szBackTexture); break; case EMDT_Fill: // fill tool { int iMat = ::MaterialMap.Get(szMaterial); if (!MatValid(iMat)) return; for (int cnt=0; cntValue(mkNamingAdapt(mkIntAdaptT(eAction), "Action")); pComp->Value(mkNamingAdapt(mkIntAdaptT(iMode), "Mode", LandscapeMode::Undefined)); pComp->Value(mkNamingAdapt(iX, "X", 0)); pComp->Value(mkNamingAdapt(iY, "Y", 0)); pComp->Value(mkNamingAdapt(iX2, "X2", 0)); pComp->Value(mkNamingAdapt(iY2, "Y2", 0)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(iGrade), "Grade", 0)); pComp->Value(mkNamingAdapt(Material, "Material", "")); pComp->Value(mkNamingAdapt(Texture, "Texture", "")); pComp->Value(mkNamingAdapt(BackMaterial, "BackMaterial", "")); pComp->Value(mkNamingAdapt(BackTexture, "BackTexture", "")); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlMessage void C4ControlMessage::Execute() const { const char *szMessage = Message.getData(); // get player C4Player *pPlr = (iPlayer < 0 ? NULL : ::Players.Get(iPlayer)); // security if (pPlr && pPlr->AtClient != iByClient) return; // do not record message as control, because it is not synced! // get lobby to forward to C4GameLobby::MainDlg *pLobby = ::Network.GetLobby(); StdStrBuf str; switch (eType) { case C4CMT_Normal: case C4CMT_Me: // log it if (pPlr) { if (pPlr->AtClient != iByClient) break; str.Format((eType == C4CMT_Normal ? "<%s> %s" : " * %s %s"), pPlr->ColorDw, pPlr->GetName(), szMessage); } else { C4Client *pClient = Game.Clients.getClientByID(iByClient); str.Format((eType == C4CMT_Normal ? "<%s> %s" : " * %s %s"), pClient ? pClient->getNick() : "???", szMessage); } // 2 lobby if (pLobby) pLobby->OnMessage(Game.Clients.getClientByID(iByClient), str.getData()); // or 2 log else Log(str.getData()); break; case C4CMT_Say: // show as game message above player cursor if (pPlr && pPlr->Cursor) { if (Game.C4S.Head.Film == C4SFilm_Cinematic) { StdStrBuf sMessage; sMessage.Format("<%s> %s", pPlr->Cursor->GetName(), szMessage); uint32_t dwClr = pPlr->Cursor->Color; if (!dwClr) dwClr = 0xff; GameMsgObjectDw(sMessage.getData(), pPlr->Cursor, dwClr|0xff000000); } else GameMsgObjectDw(szMessage, pPlr->Cursor, pPlr->ColorDw|0xff000000); } break; case C4CMT_Team: { // show only if sending player is allied with a local one if (pPlr) { // for running game mode, check actual hostility C4Player *pLocalPlr; for (int cnt = 0; (pLocalPlr = ::Players.GetLocalByIndex(cnt)); cnt++) if (!Hostile(pLocalPlr->Number, iPlayer)) break; if (pLocalPlr) Log(FormatString("{%s} %s", pPlr->ColorDw, pPlr->GetName(), szMessage).getData()); } else if (pLobby) { // in lobby mode, no player has joined yet - check teams of unjoined players if (!Game.PlayerInfos.HasSameTeamPlayers(iByClient, Game.Clients.getLocalID())) break; // OK - permit message C4Client *pClient = Game.Clients.getClientByID(iByClient); pLobby->OnMessage(Game.Clients.getClientByID(iByClient), FormatString("{%s} %s", pClient ? pClient->getNick() : "???", szMessage).getData()); } } break; case C4CMT_Private: { if (!pPlr) break; // show only if the target player is local C4Player *pLocalPlr; for (int cnt = 0; (pLocalPlr = ::Players.GetLocalByIndex(cnt)); cnt++) if (pLocalPlr->ID == iToPlayer) break; if (pLocalPlr) { Log(FormatString("[%s] %s", pPlr->ColorDw, pPlr->GetName(), szMessage).getData()); } } break; case C4CMT_Sound: { // tehehe, sound! C4Client *singer = Game.Clients.getClientByID(iByClient); if (!singer || !singer->IsIgnored()) if (!StartSoundEffect(szMessage, false, 100, NULL)) // probably wrong sound file name break; // Sound icon even if someone you ignored just tried. So you know you still need to ignore. if (pLobby) pLobby->OnClientSound(singer); if (C4Network2ClientListDlg::GetInstance()) C4Network2ClientListDlg::GetInstance()->OnSound(singer); break; } case C4CMT_Alert: // notify inactive users Application.NotifyUserIfInactive(); break; case C4CMT_System: // sender must be host if (!HostControl()) break; // show LogF("Network: %s", szMessage); break; } } void C4ControlMessage::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntAdaptT(eType), "Type", C4CMT_Normal)); pComp->Value(mkNamingAdapt(mkIntPackAdapt(iPlayer), "Player", -1)); if (eType == C4CMT_Private) pComp->Value(mkNamingAdapt(mkIntPackAdapt(iToPlayer), "ToPlayer", -1)); pComp->Value(mkNamingAdapt(Message, "Message", "")); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlPlayerInfo void C4ControlPlayerInfo::Execute() const { // join to player info list // replay and local control: direct join if (::Control.isReplay() || !::Control.isNetwork()) { // add info directly Game.PlayerInfos.AddInfo(new C4ClientPlayerInfos(PlrInfo)); // make sure team list reflects teams set in player infos Game.Teams.RecheckPlayers(); // replay: actual player join packet will follow // offline game: Issue the join if (::Control.isLocal()) Game.PlayerInfos.LocalJoinUnjoinedPlayersInQueue(); } else // network: ::Network.Players.HandlePlayerInfo(PlrInfo); } void C4ControlPlayerInfo::CompileFunc(StdCompiler *pComp) { pComp->Value(PlrInfo); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlRemovePlr void C4ControlRemovePlr::Execute() const { // host only if (iByClient != C4ClientIDHost) return; // remove ::Players.Remove(iPlr, fDisconnected, false); } void C4ControlRemovePlr::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntPackAdapt(iPlr), "Plr", -1)); pComp->Value(mkNamingAdapt(fDisconnected, "Disconnected", false)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlDebugRec void C4ControlDebugRec::Execute() const { } void C4ControlDebugRec::CompileFunc(StdCompiler *pComp) { pComp->Value(Data); } // *** C4ControlVote StdStrBuf C4ControlVote::getDesc() const { // Describe action StdStrBuf Action; switch (eType) { case VT_Cancel: Action = LoadResStr("IDS_VOTE_CANCELTHEROUND"); break; case VT_Kick: if (iData == iByClient) Action = LoadResStr("IDS_VOTE_LEAVETHEGAME"); else { C4Client *pTargetClient = Game.Clients.getClientByID(iData); Action.Format(LoadResStr("IDS_VOTE_KICKCLIENT"), pTargetClient ? pTargetClient->getName() : "???"); } break; case VT_Pause: if (iData) Action = LoadResStr("IDS_TEXT_PAUSETHEGAME"); else Action = LoadResStr("IDS_TEXT_UNPAUSETHEGAME"); break; default: Action = "perform some mysterious action"; break; } return Action; } StdStrBuf C4ControlVote::getDescWarning() const { StdStrBuf Warning; switch (eType) { case VT_Cancel: Warning = LoadResStr("IDS_TEXT_WARNINGIFTHEGAMEISCANCELL"); break; case VT_Kick: Warning = LoadResStr("IDS_TEXT_WARNINGNOLEAGUEPOINTSWILL"); break; default: Warning = ""; break; } return Warning; } void C4ControlVote::Execute() const { // Log C4Client *pClient = Game.Clients.getClientByID(iByClient); if (fApprove) LogF(LoadResStr("IDS_VOTE_WANTSTO"), pClient->getName(), getDesc().getData()); else LogF(LoadResStr("IDS_VOTE_DOESNOTWANTTO"), pClient->getName(), getDesc().getData()); // Save vote back if (::Network.isEnabled()) ::Network.AddVote(*this); // Vote done? if (::Control.isCtrlHost()) { // Count votes int32_t iPositive = 0, iNegative = 0, iVotes = 0; // If there are no teams, count as if all were in the same team // (which happens to be equivalent to "everyone is in his own team" here) for (int32_t i = 0; i < std::max(Game.Teams.GetTeamCount(), 1); i++) { C4Team *pTeam = Game.Teams.GetTeamByIndex(i); // Votes for this team int32_t iPositiveTeam = 0, iNegativeTeam = 0, iVotesTeam = 0; // Check each player for (int32_t j = 0; j < (pTeam ? pTeam->GetPlayerCount() : Game.PlayerInfos.GetPlayerCount()); j++) { int32_t iClientID = C4ClientIDUnknown; C4PlayerInfo *pNfo; if (!pTeam) { pNfo = Game.PlayerInfos.GetPlayerInfoByIndex(j); if (!pNfo) continue; // shouldn't happen iClientID = Game.PlayerInfos.GetClientInfoByPlayerID(pNfo->GetID())->GetClientID(); } else { pNfo = Game.PlayerInfos.GetPlayerInfoByID(pTeam->GetIndexedPlayer(j), &iClientID); if (!pNfo) continue; // shouldn't happen } if (iClientID < 0) continue; // Client disconnected? if (!Game.Clients.getClientByID(iClientID)) continue; // Player eliminated or never joined? if (!pNfo->IsJoined()) continue; // Okay, this player can vote iVotesTeam++; // Search vote of this client on the subject C4IDPacket *pPkt; C4ControlVote *pVote; if ((pPkt = ::Network.GetVote(iClientID, eType, iData))) if ((pVote = static_cast(pPkt->getPkt()))) { if (pVote->isApprove()) iPositiveTeam++; else iNegativeTeam++; } } // Any votes available? if (iVotesTeam) { iVotes++; // Approval by team? More then 50% needed if (iPositiveTeam * 2 > iVotesTeam) iPositive++; // Disapproval by team? More then 50% needed else if (iNegativeTeam * 2 >= iVotesTeam) iNegative++; } } // Approval? More then 50% needed if (iPositive * 2 > iVotes) ::Control.DoInput(CID_VoteEnd, new C4ControlVoteEnd(eType, true, iData), CDT_Sync); // Disapproval? else if (iNegative * 2 >= iVotes) ::Control.DoInput(CID_VoteEnd, new C4ControlVoteEnd(eType, false, iData), CDT_Sync); } } void C4ControlVote::CompileFunc(StdCompiler *pComp) { pComp->Value(mkNamingAdapt(mkIntAdaptT(eType), "Type", VT_None)); pComp->Value(mkNamingAdapt(fApprove, "Approve", true)); pComp->Value(mkNamingAdapt(iData, "Data", 0)); C4ControlPacket::CompileFunc(pComp); } // *** C4ControlVoteEnd void C4ControlVoteEnd::Execute() const { // End the voting process if (!HostControl()) return; if (::Network.isEnabled()) ::Network.EndVote(getType(), isApprove(), getData()); // Log StdStrBuf sMsg; if (isApprove()) sMsg.Format(LoadResStr("IDS_TEXT_ITWASDECIDEDTO"), getDesc().getData()); else sMsg.Format(LoadResStr("IDS_TEXT_ITWASDECIDEDNOTTO"), getDesc().getData()); Log(sMsg.getData()); // Approved? if (!isApprove()) return; // Do it C4ClientPlayerInfos *pInfos; C4PlayerInfo *pInfo; int iClient, iInfo; switch (getType()) { case VT_Cancel: // Flag players if (!Game.GameOver) for (iClient = 0; (pInfos = Game.PlayerInfos.GetIndexedInfo(iClient)); iClient++) for (iInfo = 0; (pInfo = pInfos->GetPlayerInfo(iInfo)); iInfo++) if (!pInfo->IsRemoved()) pInfo->SetVotedOut(); // Abort the game Game.Abort(true); break; case VT_Kick: // Flag players pInfos = Game.PlayerInfos.GetInfoByClientID(getData()); if (!Game.GameOver) if (pInfos) for (iInfo = 0; (pInfo = pInfos->GetPlayerInfo(iInfo)); iInfo++) if (!pInfo->IsRemoved()) pInfo->SetVotedOut(); // Remove the client if (::Control.isCtrlHost()) { C4Client *pClient = Game.Clients.getClientByID(getData()); if (pClient) Game.Clients.CtrlRemove(pClient, LoadResStr("IDS_VOTE_VOTEDOUT")); } // It is ourselves that have been voted out? if (getData() == Game.Clients.getLocalID()) { // otherwise, we have been kicked by the host. // Do a regular disconnect and display reason in game over dialog, so the client knows what has happened! Game.RoundResults.EvaluateNetwork(C4RoundResults::NR_NetError, FormatString(LoadResStr("IDS_ERR_YOUHAVEBEENREMOVEDBYVOTIN"), sMsg.getData()).getData()); ::Network.Clear(); // Game over immediately, so poor player won't continue game alone Game.DoGameOver(); } break; default: // TODO break; } } void C4ControlVoteEnd::CompileFunc(StdCompiler *pComp) { C4ControlVote::CompileFunc(pComp); }