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);
}