From d605cb39ecab4cef887bc4360dbdf0ae57674036 Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Wed, 31 May 2006 05:27:02 +0000 Subject: [PATCH] # Atlas editor: Control over 'random' actor variations. Actor variation selection (though not saved to maps, so not very useful). Added more levels of complexity to the waiting-for-game-to-respond message pump, to fix reentrancy problems. Use number keys to assign player to selected unit. This was SVN commit r3913. --- .../actors/props/units/heads/head_lime.xml | 10 +- source/graphics/ObjectBase.cpp | 70 +++++++ source/graphics/ObjectBase.h | 11 ++ source/graphics/ObjectEntry.h | 1 + source/graphics/ObjectManager.cpp | 4 +- source/graphics/Unit.cpp | 6 + source/graphics/Unit.h | 4 + .../atlas/AtlasUI/General/AtlasEventLoop.cpp | 38 ++++ .../atlas/AtlasUI/General/AtlasEventLoop.h | 24 +++ .../tools/atlas/AtlasUI/General/Observable.h | 26 ++- .../tools/atlas/AtlasUI/Misc/DLLInterface.cpp | 16 ++ .../AtlasUI/ScenarioEditor/ScenarioEditor.cpp | 66 ++++++- .../ScenarioEditor/Sections/Object/Object.cpp | 171 +++++++++++++++++- .../ScenarioEditor/Tools/Common/MiscState.h | 3 - .../Tools/Common/ObjectSettings.cpp | 78 +++++++- .../Tools/Common/ObjectSettings.h | 18 +- .../ScenarioEditor/Tools/TransformObject.cpp | 28 ++- .../GameInterface/Handlers/ObjectHandlers.cpp | 59 +++++- source/tools/atlas/GameInterface/Messages.h | 5 + .../tools/atlas/GameInterface/SharedTypes.h | 14 +- 20 files changed, 619 insertions(+), 33 deletions(-) create mode 100644 source/tools/atlas/AtlasUI/General/AtlasEventLoop.cpp create mode 100644 source/tools/atlas/AtlasUI/General/AtlasEventLoop.h diff --git a/binaries/data/mods/official/art/actors/props/units/heads/head_lime.xml b/binaries/data/mods/official/art/actors/props/units/heads/head_lime.xml index da7d72dd00..0030946bae 100644 --- a/binaries/data/mods/official/art/actors/props/units/heads/head_lime.xml +++ b/binaries/data/mods/official/art/actors/props/units/heads/head_lime.xml @@ -4,22 +4,22 @@ - + props/head_lime.pmd - + props/head/celt_e.dds - + props/head/celt_f.dds - + props/head/celt_g.dds - + props/head/celt_h.dds diff --git a/source/graphics/ObjectBase.cpp b/source/graphics/ObjectBase.cpp index 4b8c8a5fb0..782487fc95 100644 --- a/source/graphics/ObjectBase.cpp +++ b/source/graphics/ObjectBase.cpp @@ -486,3 +486,73 @@ std::set CObjectBase::CalculateRandomVariation(const std::set& ini return selections; } + +std::vector > CObjectBase::GetVariantGroups() const +{ + std::vector > groups; + + // Queue of objects (main actor plus props (recursively)) to be processed + std::queue objectsQueue; + objectsQueue.push(this); + + // Set of objects already processed, so we don't do them more than once + std::set objectsProcessed; + + while (objectsQueue.size()) + { + const CObjectBase* obj = objectsQueue.front(); + objectsQueue.pop(); + // Ignore repeated objects (likely to be props) + if (objectsProcessed.find(obj) != objectsProcessed.end()) + continue; + + objectsProcessed.insert(obj); + + // Iterate through the list of groups + for (size_t i = 0; i < obj->m_VariantGroups.size(); ++i) + { + // Copy the group's variant names into a new vector + std::vector group; + group.reserve(obj->m_VariantGroups[i].size()); + for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j) + group.push_back(obj->m_VariantGroups[i][j].m_VariantName); + + // If this group is identical to one elsewhere, don't bother listing + // it twice. + // Linear search is theoretically not very efficient, but hopefully + // we don't have enough props for that to matter... + bool dupe = false; + for (size_t j = 0; j < groups.size(); ++j) + { + if (groups[j] == group) + { + dupe = true; + break; + } + } + if (dupe) + continue; + + // Add non-trivial groups (i.e. not just one entry) to the returned list + if (obj->m_VariantGroups[i].size() > 1) + groups.push_back(group); + + // Add all props onto the queue to be considered + for (size_t j = 0; j < obj->m_VariantGroups[i].size(); ++j) + { + const std::vector& props = obj->m_VariantGroups[i][j].m_Props; + for (size_t k = 0; k < props.size(); ++k) + { + if (props[k].m_ModelName.Length()) + { + CObjectBase* prop = g_ObjMan.FindObjectBase(props[k].m_ModelName); + if (prop) + objectsQueue.push(prop); + } + } + } + } + } + + return groups; +} diff --git a/source/graphics/ObjectBase.h b/source/graphics/ObjectBase.h index cfcf8cf019..36d4e34ced 100644 --- a/source/graphics/ObjectBase.h +++ b/source/graphics/ObjectBase.h @@ -60,12 +60,23 @@ public: CObjectBase(); + // Get the variation key (indices of chosen variants from each group) + // based on the selection strings std::vector CalculateVariationKey(const std::vector >& selections); + // Get the final actor data, combining all selected variants const Variation BuildVariation(const std::vector& variationKey); + // Get a set of selection strings that are complete enough to specify an + // exact variation of the actor, using the initial selections wherever possible + // and choosing randomly where a choice is necessary. std::set CalculateRandomVariation(const std::set& initialSelections); + // Get a list of variant groups for this object, plus for all possible + // props. Duplicated groups are removed, if several props share the same + // variant names. + std::vector > GetVariantGroups() const; + bool Load(const char* filename); // object name diff --git a/source/graphics/ObjectEntry.h b/source/graphics/ObjectEntry.h index 654072e418..10b0e33100 100644 --- a/source/graphics/ObjectEntry.h +++ b/source/graphics/ObjectEntry.h @@ -18,6 +18,7 @@ public: CObjectEntry(int type, CObjectBase* base); ~CObjectEntry(); + // Construct this actor, using the specified variation selections bool BuildVariation(const std::vector >& selections, const std::vector& variationKey); // Base actor. Contains all the things that don't change between diff --git a/source/graphics/ObjectManager.cpp b/source/graphics/ObjectManager.cpp index 736e7fa9f2..c3388958aa 100644 --- a/source/graphics/ObjectManager.cpp +++ b/source/graphics/ObjectManager.cpp @@ -47,7 +47,9 @@ CObjectManager::~CObjectManager() CObjectBase* CObjectManager::FindObjectBase(const char* objectname) { - // See if the base type has been loaded yet + debug_assert(strcmp(objectname, "") != 0); + + // See if the base type has been loaded yet: for (uint k = 0; k < m_ObjectTypes.size(); k++) { diff --git a/source/graphics/Unit.cpp b/source/graphics/Unit.cpp index d8be8ad60e..395755d21d 100644 --- a/source/graphics/Unit.cpp +++ b/source/graphics/Unit.cpp @@ -130,6 +130,12 @@ void CUnit::SetEntitySelection(const CStrW& selection) ReloadObject(); } +void CUnit::SetActorSelections(const std::set& selections) +{ + m_ActorSelections = selections; + ReloadObject(); +} + void CUnit::ReloadObject() { std::vector > selections; diff --git a/source/graphics/Unit.h b/source/graphics/Unit.h index e437881dcd..bd66522a11 100644 --- a/source/graphics/Unit.h +++ b/source/graphics/Unit.h @@ -57,6 +57,10 @@ public: int GetID() const { return m_ID; } void SetID(int id) { m_ID = id; } + const std::set& GetActorSelections() const { return m_ActorSelections; } + + void SetActorSelections(const std::set& selections); + private: // object from which unit was created CObjectEntry* m_Object; diff --git a/source/tools/atlas/AtlasUI/General/AtlasEventLoop.cpp b/source/tools/atlas/AtlasUI/General/AtlasEventLoop.cpp new file mode 100644 index 0000000000..8fed7731c1 --- /dev/null +++ b/source/tools/atlas/AtlasUI/General/AtlasEventLoop.cpp @@ -0,0 +1,38 @@ +#include "stdafx.h" + +#include "AtlasEventLoop.h" + +AtlasEventLoop::AtlasEventLoop() +: m_NeedsPaint(false) +{ +} + +void AtlasEventLoop::AddMessage(MSG* msg) +{ + m_Messages.push_back(msg); +} + +void AtlasEventLoop::NeedsPaint() +{ + m_NeedsPaint = true; +} + +bool AtlasEventLoop::Dispatch() +{ + // Process the messages that QueryCallback collected + for (size_t i = 0; i < m_Messages.size(); ++i) + { + MSG* pMsg = m_Messages[i]; + wxEventLoop::GetActive()->ProcessMessage(pMsg); + delete pMsg; + } + m_Messages.clear(); + + if (m_NeedsPaint && wxTheApp && wxTheApp->GetTopWindow()) + { + wxTheApp->GetTopWindow()->Refresh(); + m_NeedsPaint = false; + } + + return wxEventLoop::Dispatch(); +} diff --git a/source/tools/atlas/AtlasUI/General/AtlasEventLoop.h b/source/tools/atlas/AtlasUI/General/AtlasEventLoop.h new file mode 100644 index 0000000000..6750acdf77 --- /dev/null +++ b/source/tools/atlas/AtlasUI/General/AtlasEventLoop.h @@ -0,0 +1,24 @@ +#ifndef ATLASEVENTLOOP_H__ +#define ATLASEVENTLOOP_H__ + +#include "wx/evtloop.h" + +struct tagMSG; + +// See ScenarioEditor.cpp's QueryCallback for explanation + +class AtlasEventLoop : public wxEventLoop +{ + std::vector m_Messages; + bool m_NeedsPaint; + +public: + AtlasEventLoop(); + + void AddMessage(tagMSG* msg); + void NeedsPaint(); + + virtual bool Dispatch(); +}; + +#endif // ATLASEVENTLOOP_H__ diff --git a/source/tools/atlas/AtlasUI/General/Observable.h b/source/tools/atlas/AtlasUI/General/Observable.h index a2cd227ee7..96a6993103 100644 --- a/source/tools/atlas/AtlasUI/General/Observable.h +++ b/source/tools/atlas/AtlasUI/General/Observable.h @@ -13,14 +13,36 @@ public: { return m_Signal.connect(order, boost::bind(std::mem_fun(callback), obj, _1)); } - void RemoveObserver(ObservableConnection handle) + + void RemoveObserver(const ObservableConnection& conn) { - handle.disconnect(); + conn.disconnect(); } + void NotifyObservers() { m_Signal(*this); } + + // Use when an object is changing something that it's also observing, + // because it already knows about the change and doesn't need to be notified + // again (particularly since that may cause infinite loops). + void NotifyObserversExcept(ObservableConnection& conn) + { + if (conn.blocked()) + { + // conn is already blocked and won't see anything + NotifyObservers(); + } + else + { + // Temporarily disable conn + conn.block(); + NotifyObservers(); + conn.unblock(); + } + } + private: boost::signal m_Signal; }; diff --git a/source/tools/atlas/AtlasUI/Misc/DLLInterface.cpp b/source/tools/atlas/AtlasUI/Misc/DLLInterface.cpp index f612ad3c63..ae3fe4a750 100644 --- a/source/tools/atlas/AtlasUI/Misc/DLLInterface.cpp +++ b/source/tools/atlas/AtlasUI/Misc/DLLInterface.cpp @@ -2,6 +2,8 @@ #include "DLLInterface.h" +#include "General/AtlasEventLoop.h" + #include "General/Datafile.h" #include "ActorEditor/ActorEditor.h" #include "ColourTester/ColourTester.h" @@ -184,6 +186,20 @@ public: OpenDirectory(dir); } } + + virtual int MainLoop() + { + // Override the default MainLoop so that we can provide our own event loop + + wxEventLoop* old = m_mainLoop; + m_mainLoop = new AtlasEventLoop; + + int ret = m_mainLoop->Run(); + + delete m_mainLoop; + m_mainLoop = old; + return ret; + } }; IMPLEMENT_APP_NO_MAIN(wxDLLApp) diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp b/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp index ed658f21b4..91db64b834 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp @@ -6,6 +6,8 @@ #include "wx/evtloop.h" #include "wx/tooltip.h" +#include "General/AtlasEventLoop.h" + #include "SnapSplitterWindow/SnapSplitterWindow.h" #include "HighResTimer/HighResTimer.h" #include "Buttons/ToolButton.h" @@ -532,6 +534,9 @@ AtlasMessage::Position::Position(const wxPoint& pt) type1.y = pt.y; } +////////////////////////////////////////////////////////////////////////// + + static void QueryCallback() { // If this thread completely blocked on the semaphore inside Query, it would @@ -544,9 +549,66 @@ static void QueryCallback() // This is kind of like wxYield, but without the ProcessPendingEvents - // it's enough to make Windows happy and stop deadlocking, without actually // calling the event handlers (which could lead to nasty recursion) - while (wxEventLoop::GetActive()->Pending()) - wxEventLoop::GetActive()->Dispatch(); + +// while (wxEventLoop::GetActive()->Pending()) +// wxEventLoop::GetActive()->Dispatch(); + + // Oh dear, we can't use that either - it (at least in wx 2.6.3) still + // processes messages, which causes reentry into various things that we + // don't want to be reentrant. So do it all manually, accepting Windows + // messages and sticking them on a list for later processing (in a custom + // event loop class): + + // (TODO: Rethink this entire process on Linux) + // (Alt TODO: Could we make the game never pop up windows (or use the Win32 + // GUI in any other way) when it's running under Atlas, so we wouldn't need + // to do any message processing here at all?) + + AtlasEventLoop* evtLoop = (AtlasEventLoop*)wxEventLoop::GetActive(); + + while (evtLoop->Pending()) + { + // Based on src/msw/evtloop.cpp's wxEventLoop::Dispatch() + + MSG msg; + BOOL rc = ::GetMessage(&msg, (HWND) NULL, 0, 0); + + if (rc == 0) + { + // got WM_QUIT + return; + } + + if (rc == -1) + { + wxLogLastError(wxT("GetMessage")); + return; + } + + // Our special bits: + + if (msg.message == WM_PAINT) + { + // "GetMessage does not remove WM_PAINT messages from the queue. + // The messages remain in the queue until processed." + // So let's process them, to avoid infinite loops... + PAINTSTRUCT paint; + ::BeginPaint(msg.hwnd, &paint); + ::EndPaint(msg.hwnd, &paint); + // Remember that some painting was needed - we'll just repaint + // the whole screen when this is finished. + evtLoop->NeedsPaint(); + } + else + { + // Add this message to a queue for later processing. (That's + // probably kind of valid, at least in most cases.) + MSG* pMsg = new MSG(msg); + evtLoop->AddMessage(pMsg); + } + } } + void AtlasMessage::QueryMessage::Post() { g_MessagePasser->Query(this, &QueryCallback); diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Object/Object.cpp b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Object/Object.cpp index 1be39e1c17..349be02959 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Object/Object.cpp +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Object/Object.cpp @@ -20,9 +20,11 @@ public: void OnSelect(wxCommandEvent& evt) { + // On selecting an object, enable the PlaceObject tool with this object wxString id = static_cast(evt.GetClientObject())->GetData(); SetCurrentTool(_T("PlaceObject"), &id); } + private: DECLARE_EVENT_TABLE(); }; @@ -43,10 +45,13 @@ public: void OnSelect(wxCommandEvent& evt) { + // Switch between displayed lists of objects (e.g. entities vs actors) m_Sidebar.SetObjectFilter(evt.GetSelection()); } + private: ObjectSidebar& m_Sidebar; + DECLARE_EVENT_TABLE(); }; BEGIN_EVENT_TABLE(ObjectChoiceCtrl, wxChoice) @@ -84,9 +89,11 @@ ObjectSidebar::~ObjectSidebar() void ObjectSidebar::OnFirstDisplay() { + // Get the list of objects from the game AtlasMessage::qGetObjectsList qry; qry.Post(); p->m_Objects = *qry.objects; + // Display first group of objects SetObjectFilter(0); } @@ -114,25 +121,27 @@ public: PlayerComboBox(wxWindow* parent, wxArrayString& choices) : wxComboBox(parent, -1, choices[g_ObjectSettings.GetPlayerID()], wxDefaultPosition, wxDefaultSize, choices, wxCB_READONLY) { - m_Conn = g_SelectedObjects.RegisterObserver(1, &PlayerComboBox::OnSelectionChange, this); + m_Conn = g_ObjectSettings.RegisterObserver(1, &PlayerComboBox::OnObjectSettingsChange, this); } + ~PlayerComboBox() { - g_SelectedObjects.RemoveObserver(m_Conn); + g_ObjectSettings.RemoveObserver(m_Conn); } private: ObservableConnection m_Conn; - void OnSelectionChange(const std::vector& selection) + void OnObjectSettingsChange(const ObjectSettings& settings) { - SetSelection(g_ObjectSettings.GetPlayerID()); + SetSelection(settings.GetPlayerID()); } void OnSelect(wxCommandEvent& evt) { g_ObjectSettings.SetPlayerID(evt.GetInt()); + g_ObjectSettings.NotifyObserversExcept(m_Conn); } DECLARE_EVENT_TABLE(); @@ -142,6 +151,153 @@ BEGIN_EVENT_TABLE(PlayerComboBox, wxComboBox) END_EVENT_TABLE(); +class VariationDisplay : public wxScrolledWindow +{ +public: + VariationDisplay(wxWindow* parent) + : wxScrolledWindow(parent, -1) + { + m_Conn = g_ObjectSettings.RegisterObserver(1, &VariationDisplay::OnObjectSettingsChange, this); + + SetMinSize(wxSize(160, wxDefaultCoord)); + + SetScrollRate(0, 5); + + m_Sizer = new wxBoxSizer(wxVERTICAL); + SetSizer(m_Sizer); + } + + ~VariationDisplay() + { + g_ObjectSettings.RemoveObserver(m_Conn); + } + +private: + + ObservableConnection m_Conn; + + std::vector m_ComboBoxes; + wxSizer* m_Sizer; + + // Event handler shared by all the combo boxes created by this window + void OnSelect(wxCommandEvent& evt) + { + std::set selections; + + // It's possible for a variant name to appear in multiple groups. + // If so, assume that all the names in each group are the same, so + // we don't have to worry about some impossible combinations (e.g. + // one group "a,b", a second "b,c", and a third "c,a", where's there's + // no set of selections that matches one (and only one) of each group). + // + // So... When a combo box is changed from 'a' to 'b', add 'b' to the new + // selections and make sure any other combo boxes containing both 'a' and + // 'b' no longer contain 'a'. + + wxComboBox* thisComboBox = wxDynamicCast(evt.GetEventObject(), wxComboBox); + wxString newValue = thisComboBox->GetValue(); + + selections.insert(newValue); + + for (size_t i = 0; i < m_ComboBoxes.size(); ++i) + { + wxComboBox* comboBox = wxDynamicCast(m_ComboBoxes[i], wxComboBox); + wxCHECK(comboBox != NULL, ); + // If our newly selected value is used in another combobox, we want + // that combobox to use the new value, so don't add its old value + // to the list of selections + if (comboBox->FindString(newValue) == wxNOT_FOUND) + selections.insert(comboBox->GetValue()); + } + + g_ObjectSettings.SetActorSelections(selections); + g_ObjectSettings.NotifyObserversExcept(m_Conn); + RefreshObjectSettings(); + } + + void OnObjectSettingsChange(const ObjectSettings& settings) + { + Freeze(); + + const std::vector& variation = settings.GetActorVariation(); + + // Creating combo boxes seems to be pretty expensive - so we create as + // few as possible, by never deleting any. + + size_t oldCount = m_ComboBoxes.size(); + size_t newCount = variation.size(); + + // If we have too many combo boxes, hide the excess ones + for (size_t i = newCount; i < oldCount; ++i) + { + m_ComboBoxes[i]->Show(false); + } + + for (size_t i = 0; i < variation.size(); ++i) + { + const ObjectSettings::Group& group = variation[i]; + + if (i < oldCount) + { + // Already got enough boxes available, so use an old one + wxComboBox* comboBox = wxDynamicCast(m_ComboBoxes[i], wxComboBox); + wxCHECK(comboBox != NULL, ); + // Replace the contents of the old combobox with the new data + comboBox->Freeze(); + comboBox->Clear(); + comboBox->Append(group.variants); + comboBox->SetValue(group.chosen); + comboBox->Show(true); + comboBox->Thaw(); + } + else + { + // Create an initially empty combobox, because we can fill it + // quicker than the default constructor can + wxComboBox* combo = new wxComboBox(this, -1, wxEmptyString, wxDefaultPosition, + wxSize(130, wxDefaultCoord), wxArrayString(), wxCB_READONLY); + // Freeze it before adding all the values + combo->Freeze(); + combo->Append(group.variants); + combo->SetValue(group.chosen); + combo->Thaw(); + // Add the on-select event handler + combo->Connect(wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, + wxCommandEventHandler(VariationDisplay::OnSelect), NULL, this); + // Add box to sizer and list + m_Sizer->Add(combo); + m_ComboBoxes.push_back(combo); + } + } + + Layout(); + + // Make the scrollbars appear when appropriate + FitInside(); + + Thaw(); + } + + void RefreshObjectSettings() + { + const std::vector& variation = g_ObjectSettings.GetActorVariation(); + + // For each group, set the corresponding combobox's value to the chosen one + size_t i = 0; + for (std::vector::const_iterator group = variation.begin(); + group != variation.end() && i < m_ComboBoxes.size(); + ++group, ++i) + { + wxComboBox* comboBox = wxDynamicCast(m_ComboBoxes[i], wxComboBox); + wxCHECK(comboBox != NULL, ); + + comboBox->SetValue(group->chosen); + } + } +}; + +////////////////////////////////////////////////////////////////////////// + ObjectBottomBar::ObjectBottomBar(wxWindow* parent) : wxPanel(parent, wxID_ANY) { @@ -159,7 +315,12 @@ ObjectBottomBar::ObjectBottomBar(wxWindow* parent) players.Add(_("Player 7")); players.Add(_("Player 8")); wxComboBox* playerSelect = new PlayerComboBox(this, players); - sizer->Add(playerSelect); + + wxWindow* variationSelect = new VariationDisplay(this); + wxSizer* variationSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Variation")); + variationSizer->Add(variationSelect, wxSizerFlags().Proportion(1).Expand()); + sizer->Add(variationSizer, wxSizerFlags().Proportion(1)); + SetSizer(sizer); } diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/MiscState.h b/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/MiscState.h index 88d6393b69..44d009d643 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/MiscState.h +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/MiscState.h @@ -10,9 +10,6 @@ namespace AtlasMessage extern wxString g_SelectedTexture; -// Observer order: -// 0 = g_UnitSettings -// 1 = things that want to access g_UnitSettings extern Observable > g_SelectedObjects; #endif // MISCSTATE_H__ diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/ObjectSettings.cpp b/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/ObjectSettings.cpp index d332ffe3df..5413f09757 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/ObjectSettings.cpp +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/ObjectSettings.cpp @@ -5,7 +5,7 @@ #include "GameInterface/Messages.h" #include "ScenarioEditor/Tools/Common/Tools.h" -ObjectSettings g_ObjectSettings; +Observable g_ObjectSettings; ObjectSettings::ObjectSettings() : m_PlayerID(0) @@ -18,7 +18,7 @@ ObjectSettings::~ObjectSettings() m_Conn.disconnect(); } -int ObjectSettings::GetPlayerID() +int ObjectSettings::GetPlayerID() const { return m_PlayerID; } @@ -29,21 +29,62 @@ void ObjectSettings::SetPlayerID(int playerID) PostToGame(); } +const std::set& ObjectSettings::GetActorSelections() const +{ + return m_ActorSelections; +} + void ObjectSettings::SetActorSelections(const std::set& selections) { m_ActorSelections = selections; PostToGame(); } +const std::vector ObjectSettings::GetActorVariation() const +{ + std::vector variation; + + for (std::vector::const_iterator grp = m_VariantGroups.begin(); + grp != m_VariantGroups.end(); + ++grp) + { + Group group; + group.variants = *grp; + + // Variant choice method, as used by the game: Choose the first variant + // which matches any of the selections + + size_t chosen = 0; // default to first + for (size_t i = 0; i < grp->GetCount(); ++i) + { + if (m_ActorSelections.find(grp->Item(i)) != m_ActorSelections.end()) + { + chosen = i; + break; + } + } + group.chosen = grp->Item(chosen); + + variation.push_back(group); + } + + return variation; +} + AtlasMessage::sObjectSettings ObjectSettings::GetSettings() const { AtlasMessage::sObjectSettings settings; settings.player = m_PlayerID; + // Copy selections from set into vector std::vector selections; - for (std::set::const_iterator it = m_ActorSelections.begin(); it != m_ActorSelections.end(); ++it) + for (std::set::const_iterator it = m_ActorSelections.begin(); + it != m_ActorSelections.end(); + ++it) + { selections.push_back(it->c_str()); + } settings.selections = selections; return settings; @@ -61,11 +102,36 @@ void ObjectSettings::OnSelectionChange(const std::vector qry.Post(); m_PlayerID = qry.settings->player; - std::vector selections = *qry.settings->selections; m_ActorSelections.clear(); - for (std::vector::iterator it = selections.begin(); it != selections.end(); ++it) - m_ActorSelections.insert(it->c_str()); + m_VariantGroups.clear(); + + std::vector > variation = *qry.settings->variantgroups; + for (std::vector >::iterator grp = variation.begin(); + grp != variation.end(); + ++grp) + { + wxArrayString variants; + + for (std::vector::iterator it = grp->begin(); + it != grp->end(); + ++it) + { + variants.Add(it->c_str()); + } + + m_VariantGroups.push_back(variants); + } + + std::vector selections = *qry.settings->selections; + for (std::vector::iterator sel = selections.begin(); + sel != selections.end(); + ++sel) + { + m_ActorSelections.insert(sel->c_str()); + } + + static_cast*>(this)->NotifyObservers(); } void ObjectSettings::PostToGame() diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/ObjectSettings.h b/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/ObjectSettings.h index 9a126f6603..ea6f521aca 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/ObjectSettings.h +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/Common/ObjectSettings.h @@ -19,10 +19,21 @@ public: ObjectSettings(); ~ObjectSettings(); - int GetPlayerID(); + int GetPlayerID() const; void SetPlayerID(int playerID); + + struct Group + { + wxArrayString variants; + wxString chosen; + }; + + const std::vector GetActorVariation() const; + + const std::set& GetActorSelections() const; void SetActorSelections(const std::set& selections); + // Constructs new sObjectSettings object from settings AtlasMessage::sObjectSettings GetSettings() const; private: @@ -34,6 +45,9 @@ private: // a new actor, and will accumulate variant names) std::set m_ActorSelections; + // List of actor variant groups (each a list of variant names) + std::vector m_VariantGroups; + // Observe changes to unit selection ObservableConnection m_Conn; void OnSelectionChange(const std::vector& selection); @@ -42,6 +56,6 @@ private: void PostToGame(); }; -extern ObjectSettings g_ObjectSettings; +extern Observable g_ObjectSettings; #endif // ObjectSettings_H__ diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/TransformObject.cpp b/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/TransformObject.cpp index 1f107b721d..8a605cc699 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/TransformObject.cpp +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Tools/TransformObject.cpp @@ -3,6 +3,7 @@ #include "Common/Tools.h" #include "Common/Brushes.h" #include "Common/MiscState.h" +#include "Common/ObjectSettings.h" #include "GameInterface/Messages.h" using AtlasMessage::Position; @@ -35,27 +36,41 @@ public: { if (evt.LeftDown()) { - // TODO: multiple selection + // New selection - never merge with movements of other objects + ScenarioEditor::GetCommandProc().FinaliseLastCommand(); + + // Select the object clicked on: + AtlasMessage::qPickObject qry(Position(evt.GetPosition())); qry.Post(); + + // TODO: handle multiple selections g_SelectedObjects.clear(); + + // Check they actually clicked on a valid object if (AtlasMessage::ObjectIDIsValid(qry.id)) { g_SelectedObjects.push_back(qry.id); + // Remember the screen-space offset of the mouse from the + // object's centre, so we can add that back when moving it + // (instead of just moving the object's centre to directly + // beneath the mouse) obj->m_dx = qry.offsetx; obj->m_dy = qry.offsety; SET_STATE(Dragging); } g_SelectedObjects.NotifyObservers(); POST_MESSAGE(SetSelectionPreview, (g_SelectedObjects)); - ScenarioEditor::GetCommandProc().FinaliseLastCommand(); return true; } else if (evt.Dragging() && evt.RightIsDown() || evt.RightDown()) { + // Dragging with right mouse button -> rotate objects to look + // at mouse Position pos (evt.GetPosition()); for (size_t i = 0; i < g_SelectedObjects.size(); ++i) POST_COMMAND(RotateObject, (g_SelectedObjects[i], true, pos, 0.f)); + return true; } else @@ -68,11 +83,20 @@ public: { for (size_t i = 0; i < g_SelectedObjects.size(); ++i) POST_COMMAND(DeleteObject, (g_SelectedObjects[i])); + g_SelectedObjects.clear(); g_SelectedObjects.NotifyObservers(); + POST_MESSAGE(SetSelectionPreview, (g_SelectedObjects)); return true; } + else if (type == KEY_CHAR && (evt.GetKeyCode() >= '0' && evt.GetKeyCode() <= '9')) + { + int playerID = evt.GetKeyCode() - '0'; + g_ObjectSettings.SetPlayerID(playerID); + g_ObjectSettings.NotifyObservers(); + return true; + } else return false; } diff --git a/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp b/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp index ab421b696b..8fe09a55a0 100644 --- a/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp +++ b/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp @@ -110,14 +110,51 @@ QUERYHANDLER(GetObjectSettings) sObjectSettings settings; settings.player = unit->GetPlayerID(); - // TODO: actor variation + // Get the unit's possible variants and selected variants + std::vector > groups = unit->GetObject()->m_Base->GetVariantGroups(); + const std::set& selections = unit->GetActorSelections(); + + // Iterate over variant groups + std::vector > variantgroups; + std::set selections_set; + variantgroups.reserve(groups.size()); + for (size_t i = 0; i < groups.size(); ++i) + { + // Copy variants into output structure + + std::vector group; + group.reserve(groups[i].size()); + int choice = -1; + + for (size_t j = 0; j < groups[i].size(); ++j) + { + group.push_back(groups[i][j]); + + // Find the first string in 'selections' that matches one of this + // group's variants + if (choice == -1) + if (selections.find(groups[i][j]) != selections.end()) + choice = (int)j; + } + + // Assuming one of the variants was selected (which it really ought + // to be), remember that one's name + if (choice != -1) + selections_set.insert(groups[i][choice]); + + variantgroups.push_back(group); + } + + settings.variantgroups = variantgroups; + settings.selections = std::vector (selections_set.begin(), selections_set.end()); // convert set->vector msg->settings = settings; } BEGIN_COMMAND(SetObjectSettings) int m_PlayerOld, m_PlayerNew; + std::set m_SelectionsOld, m_SelectionsNew; void Do() { @@ -128,7 +165,12 @@ BEGIN_COMMAND(SetObjectSettings) m_PlayerOld = unit->GetPlayerID(); m_PlayerNew = settings.player; - // TODO: actor variations + + m_SelectionsOld = unit->GetActorSelections(); + + std::vector selections = *settings.selections; + copy(selections.begin(), selections.end(), + std::insert_iterator >(m_SelectionsNew, m_SelectionsNew.begin())); Redo(); } @@ -139,6 +181,7 @@ BEGIN_COMMAND(SetObjectSettings) if (! unit) return; unit->SetPlayerID(m_PlayerNew); + unit->SetActorSelections(m_SelectionsNew); } void Undo() @@ -147,6 +190,7 @@ BEGIN_COMMAND(SetObjectSettings) if (! unit) return; unit->SetPlayerID(m_PlayerOld); + unit->SetActorSelections(m_SelectionsOld); } END_COMMAND(SetObjectSettings); @@ -405,10 +449,15 @@ QUERYHANDLER(PickObject) if (target) { + // Get screen coordinates of the point on the ground underneath the + // object's model-centre, so that callers know the offset to use when + // working out the screen coordinates to move the object to. + // (TODO: http://trac.0ad.homeip.net/ticket/99) CVector3D centre = target->GetModel()->GetTransform().GetTranslation(); centre.Y = g_Game->GetWorld()->GetTerrain()->getExactGroundLevel(centre.X, centre.Z); float cx, cy; g_Game->GetView()->GetCamera()->GetScreenCoordinates(centre, cx, cy); + msg->offsetx = (int)(cx - x); msg->offsety = (int)(cy - y); } @@ -472,7 +521,8 @@ BEGIN_COMMAND(MoveObject) void MergeWithSelf(cMoveObject* prev) { - // TODO: merge correctly when prev unit != this unit + // TODO: do something valid if prev unit != this unit + debug_assert(prev->msg->id == msg->id); prev->m_PosNew = m_PosNew; } @@ -563,7 +613,8 @@ BEGIN_COMMAND(RotateObject) void MergeWithSelf(cRotateObject* prev) { - // TODO: merge correctly when prev unit != this unit + // TODO: do something valid if prev unit != this unit + debug_assert(prev->msg->id == msg->id); prev->m_AngleNew = m_AngleNew; prev->m_TransformNew = m_TransformNew; } diff --git a/source/tools/atlas/GameInterface/Messages.h b/source/tools/atlas/GameInterface/Messages.h index b315f16cad..6995151131 100644 --- a/source/tools/atlas/GameInterface/Messages.h +++ b/source/tools/atlas/GameInterface/Messages.h @@ -106,6 +106,11 @@ struct sObjectSettings { Shareable player; Shareable > selections; + + // Some settings are immutable and therefore are ignored (and should be left + // empty) when passed from the editor to the game: + + Shareable > > variantgroups; }; SHAREABLE_STRUCT(sObjectSettings); diff --git a/source/tools/atlas/GameInterface/SharedTypes.h b/source/tools/atlas/GameInterface/SharedTypes.h index f306de28c4..8c3839277e 100644 --- a/source/tools/atlas/GameInterface/SharedTypes.h +++ b/source/tools/atlas/GameInterface/SharedTypes.h @@ -9,12 +9,23 @@ class CVector3D; namespace AtlasMessage { +// Represents a position in the game world, with an interface usable from the +// UI (which usually knows only about screen space). Typically constructed +// by the UI, then passed to the game which uses GetWorldSpace. + struct Position { Position() : type(0) { type0.x = type0.y = type0.z = 0.f; } + + // Constructs a position with specified world-space coordinates Position(float x_, float y_, float z_) : type(0) { type0.x = x_; type0.y = y_; type0.z = z_; } + + // Constructs a position on the terrain underneath the screen-space coordinates Position(const wxPoint& pt); // (implementation in ScenarioEditor.cpp) + // Store all possible position representations in a union, instead of something + // like inheritance and virtual functions, so we don't have to care about + // dynamic memory allocation across DLLs. int type; union { struct { float x, y, z; } type0; // world-space coordinates @@ -23,7 +34,7 @@ struct Position }; // Constructs a position with the meaning "same as previous", which is handled - // in an unspecified way by various message handlers. + // in unspecified (but usually obvious) ways by different message handlers. static Position Unchanged() { Position p; p.type = 2; return p; } // Only for use in the game, not the UI. @@ -33,6 +44,7 @@ struct Position void GetWorldSpace(CVector3D& vec, const CVector3D& prev) const; void GetScreenSpace(float& x, float& y) const; }; + SHAREABLE_STRUCT(Position); }