1
0
forked from 0ad/0ad
0ad/source/tools/atlas/AtlasUI/ScenarioEditor/ScenarioEditor.cpp
Ykkrosh 35e91718c5 # Added tool for viewing models and animations outside the game.
Atlas: Added ActorViewer. Moved GL canvas into separate class for shared
use. Disabled message-handling callback while blocked on the game, and
stopped creating dialog boxes inside the game thread in order to avoid
deadlocks (hopefully). Support multiple Views (for independent sets of
camera/update/render code). Recalculate territory boundaries when
necessary. Changed default list of animations to match those currently
used by actors.
# Tidied up more code.
Moved some more #includes out of .h files, to minimise unnecessary
compilation.
MathUtil: Deleted unused/unuseful macros (M_PI (use PI instead), M_PI_2
(use PI/2), MAX3, ABS (use abs)).
ObjectManager: Removed some ScEd-specific things.
Unit: Moved creation out of UnitManager, so units can be created without
adding to the manager. Changed CStr8 to the more conventional CStr.
app_hooks: Removed warning for setting multiple times.
win: Restored SEH catcher.
GameSetup, GameView: Removed RenderNoCull, because it doesn't seem to do
what it says it does ("force renderer to load everything") since we're
loading-on-demand most stuff and it doesn't seem especially useful since
we'd prefer to minimise loading times (but feel free to correct me if
I'm wrong). (And because it crashes when things need to be initialised
in a different order, so it's easier to remove than to understand and
fix it.)
PatchRData, Renderer: Work sensibly when there's no game (hence no LOS
manager, water, etc).
LOSManager: Use entity position instead of actor position when possible.
TerritoryManager: Allow delayed recalculations (so Atlas can issue lots
of move+recalculate commands per frame).
Cinematic: Non-pointer wxTimer, so it doesn't leak and doesn't have to
be deleted manually.

This was SVN commit r4261.
2006-08-28 17:36:42 +00:00

647 lines
17 KiB
C++

#include "stdafx.h"
#include "ScenarioEditor.h"
#include "wx/evtloop.h"
#include "wx/tooltip.h"
#include "wx/image.h"
#include "wx/busyinfo.h"
#include "General/AtlasEventLoop.h"
#include "General/Datafile.h"
#include "HighResTimer/HighResTimer.h"
#include "Buttons/ToolButton.h"
#include "CustomControls/Canvas/Canvas.h"
#include "GameInterface/MessagePasser.h"
#include "GameInterface/Messages.h"
#include "tools/Common/Tools.h"
static HighResTimer g_Timer;
using namespace AtlasMessage;
//////////////////////////////////////////////////////////////////////////
// GL functions exported from DLL, and called by game (in a separate
// thread to the standard wx one)
ATLASDLLIMPEXP void Atlas_GLSetCurrent(void* context)
{
((wxGLContext*)context)->SetCurrent();
}
ATLASDLLIMPEXP void Atlas_GLSwapBuffers(void* context)
{
((wxGLContext*)context)->SwapBuffers();
}
//////////////////////////////////////////////////////////////////////////
class GameCanvas : public Canvas
{
public:
GameCanvas(wxWindow* parent, int* attribList)
: Canvas(parent, attribList, wxWANTS_CHARS),
m_MouseState(NONE), m_LastMouseState(NONE)
{
}
private:
bool KeyScroll(wxKeyEvent& evt, bool enable)
{
int dir;
switch (evt.GetKeyCode())
{
case WXK_LEFT: dir = eScrollConstantDir::LEFT; break;
case WXK_RIGHT: dir = eScrollConstantDir::RIGHT; break;
case WXK_UP: dir = eScrollConstantDir::FORWARDS; break;
case WXK_DOWN: dir = eScrollConstantDir::BACKWARDS; break;
case WXK_SHIFT: case WXK_CONTROL: dir = -1; break;
default: return false;
}
float speed = 120.f * ScenarioEditor::GetSpeedModifier();
if (dir == -1) // changed modifier keys - update all currently-scrolling directions
{
if (wxGetKeyState(WXK_LEFT)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::LEFT, speed));
if (wxGetKeyState(WXK_RIGHT)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::RIGHT, speed));
if (wxGetKeyState(WXK_UP)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::FORWARDS, speed));
if (wxGetKeyState(WXK_DOWN)) POST_MESSAGE(ScrollConstant, (eRenderView::GAME, eScrollConstantDir::BACKWARDS, speed));
}
else
{
POST_MESSAGE(ScrollConstant, (eRenderView::GAME, dir, enable ? speed : 0.0f));
}
return true;
}
void OnKeyDown(wxKeyEvent& evt)
{
if (GetCurrentTool().OnKey(evt, ITool::KEY_DOWN))
{
// Key event has been handled by the tool, so don't try
// to use it for camera motion too
return;
}
if (KeyScroll(evt, true))
return;
evt.Skip();
}
void OnKeyUp(wxKeyEvent& evt)
{
if (GetCurrentTool().OnKey(evt, ITool::KEY_UP))
return;
if (KeyScroll(evt, false))
return;
evt.Skip();
}
void OnChar(wxKeyEvent& evt)
{
if (GetCurrentTool().OnKey(evt, ITool::KEY_CHAR))
return;
int dir = 0;
if (evt.GetKeyCode() == '-' || evt.GetKeyCode() == '_')
dir = -1;
else if (evt.GetKeyCode() == '+' || evt.GetKeyCode() == '=')
dir = +1;
// TODO: internationalisation (-/_ and +/= don't always share a key)
if (dir)
{
float speed = 16.f;
if (wxGetKeyState(WXK_SHIFT))
speed *= 4.f;
POST_MESSAGE(SmoothZoom, (eRenderView::GAME, speed*dir));
}
else
evt.Skip();
}
virtual void HandleMouseEvent(wxMouseEvent& evt)
{
// TODO or at least to think about: When using other controls in the
// editor, it's annoying that keyboard/scrollwheel no longer navigate
// around the world until you click on it.
// Setting focus back whenever the mouse moves over the GL window
// feels like a fairly natural solution to me, since I can use
// e.g. brush-editing controls normally, and then move the mouse to
// see the brush outline and magically get given back full control
// of the camera.
if (evt.Moving())
SetFocus();
if (GetCurrentTool().OnMouse(evt))
{
// Mouse event has been handled by the tool, so don't try
// to use it for camera motion too
return;
}
// Global mouse event handlers (for camera motion)
if (evt.GetWheelRotation())
{
float speed = 16.f * ScenarioEditor::GetSpeedModifier();
POST_MESSAGE(SmoothZoom, (eRenderView::GAME, evt.GetWheelRotation() * speed / evt.GetWheelDelta()));
}
else
{
if (evt.MiddleIsDown())
{
if (wxGetKeyState(WXK_CONTROL) || evt.RightIsDown())
m_MouseState = ROTATEAROUND;
else
m_MouseState = SCROLL;
}
else
m_MouseState = NONE;
if (m_MouseState != m_LastMouseState)
{
switch (m_MouseState)
{
case NONE: break;
case SCROLL: POST_MESSAGE(Scroll, (eRenderView::GAME, eScrollType::FROM, evt.GetPosition())); break;
case ROTATEAROUND: POST_MESSAGE(RotateAround, (eRenderView::GAME, eRotateAroundType::FROM, evt.GetPosition())); break;
default: wxFAIL;
}
m_LastMouseState = m_MouseState;
}
else if (evt.Dragging())
{
switch (m_MouseState)
{
case NONE: break;
case SCROLL: POST_MESSAGE(Scroll, (eRenderView::GAME, eScrollType::TO, evt.GetPosition())); break;
case ROTATEAROUND: POST_MESSAGE(RotateAround, (eRenderView::GAME, eRotateAroundType::TO, evt.GetPosition())); break;
default: wxFAIL;
}
}
}
}
enum { NONE, SCROLL, ROTATEAROUND };
int m_MouseState, m_LastMouseState;
DECLARE_EVENT_TABLE();
};
BEGIN_EVENT_TABLE(GameCanvas, Canvas)
EVT_KEY_DOWN(GameCanvas::OnKeyDown)
EVT_KEY_UP(GameCanvas::OnKeyUp)
EVT_CHAR(GameCanvas::OnChar)
END_EVENT_TABLE()
//////////////////////////////////////////////////////////////////////////
volatile bool g_FrameHasEnded;
// Called from game thread
ATLASDLLIMPEXP void Atlas_NotifyEndOfFrame()
{
g_FrameHasEnded = true;
}
enum
{
ID_Quit = 1,
// ID_New,
ID_Open,
ID_Save,
ID_SaveAs,
ID_Wireframe,
ID_MessageTrace,
ID_Screenshot,
ID_Toolbar // must be last in the list
};
BEGIN_EVENT_TABLE(ScenarioEditor, wxFrame)
EVT_CLOSE(ScenarioEditor::OnClose)
EVT_TIMER(wxID_ANY, ScenarioEditor::OnTimer)
// EVT_MENU(ID_New, ScenarioEditor::OnNew)
EVT_MENU(ID_Open, ScenarioEditor::OnOpen)
EVT_MENU(ID_Save, ScenarioEditor::OnSave)
EVT_MENU(ID_SaveAs, ScenarioEditor::OnSaveAs)
EVT_MENU(ID_Quit, ScenarioEditor::OnQuit)
EVT_MENU(wxID_UNDO, ScenarioEditor::OnUndo)
EVT_MENU(wxID_REDO, ScenarioEditor::OnRedo)
EVT_MENU(ID_Wireframe, ScenarioEditor::OnWireframe)
EVT_MENU(ID_MessageTrace, ScenarioEditor::OnMessageTrace)
EVT_MENU(ID_Screenshot, ScenarioEditor::OnScreenshot)
EVT_IDLE(ScenarioEditor::OnIdle)
END_EVENT_TABLE()
static AtlasWindowCommandProc g_CommandProc;
AtlasWindowCommandProc& ScenarioEditor::GetCommandProc() { return g_CommandProc; }
ScenarioEditor::ScenarioEditor(wxWindow* parent)
: wxFrame(parent, wxID_ANY, _T(""), wxDefaultPosition, wxSize(1024, 768))
{
// Global application initialisation:
// wxLog::SetTraceMask(wxTraceMessages);
SetOpenFilename(_T(""));
SetIcon(wxIcon(_T("ICON_ScenarioEditor")));
wxToolTip::Enable(true);
wxImage::AddHandler(new wxPNGHandler);
//////////////////////////////////////////////////////////////////////////
// Menu
wxMenuBar* menuBar = new wxMenuBar;
SetMenuBar(menuBar);
wxMenu *menuFile = new wxMenu;
menuBar->Append(menuFile, _("&File"));
{
// menuFile->Append(ID_New, _("&New"));
menuFile->Append(ID_Open, _("&Open..."));
menuFile->Append(ID_Save, _("&Save"));
menuFile->Append(ID_SaveAs, _("Save &As..."));
menuFile->AppendSeparator();//-----------
menuFile->Append(ID_Quit, _("E&xit"));
// m_FileHistory.UseMenu(menuFile);//-------
// m_FileHistory.AddFilesToMenu();
}
// m_menuItem_Save = menuFile->FindItem(ID_Save); // remember this item, to let it be greyed out
// wxASSERT(m_menuItem_Save);
wxMenu *menuEdit = new wxMenu;
menuBar->Append(menuEdit, _("&Edit"));
{
menuEdit->Append(wxID_UNDO, _("&Undo"));
menuEdit->Append(wxID_REDO, _("&Redo"));
}
GetCommandProc().SetEditMenu(menuEdit);
GetCommandProc().Initialize();
wxMenu *menuMisc = new wxMenu;
menuBar->Append(menuMisc, _("&Misc hacks"));
{
menuMisc->AppendCheckItem(ID_Wireframe, _("&Wireframe"));
menuMisc->AppendCheckItem(ID_MessageTrace, _("Message debug trace"));
menuMisc->Append(ID_Screenshot, _("&Screenshot"));
}
m_SectionLayout.SetWindow(this);
// Toolbar:
ToolButtonBar* toolbar = new ToolButtonBar(this, &m_SectionLayout, ID_Toolbar);
// TODO: configurable small vs large icon images
// (button label; tooltip text; image; internal tool name; section to switch to)
toolbar->AddToolButton(_("Default"), _("Default"), _T("default.png"), _T(""), _T(""));
toolbar->AddToolButton(_("Move"), _("Move/rotate object"), _T("moveobject.png"), _T("TransformObject"), _T("ObjectSidebar"));
toolbar->AddToolButton(_("Elevation"), _("Alter terrain elevation"), _T("alterelevation.png"), _T("AlterElevation"), _T("TerrainSidebar"));
toolbar->AddToolButton(_("Flatten"), _("Flatten terrain elevation"), _T("flattenelevation.png"), _T("FlattenElevation"), _T("TerrainSidebar"));
toolbar->AddToolButton(_("Paint Terrain"), _("Paint terrain texture"), _T("paintterrain.png"), _T("PaintTerrain"), _T("TerrainSidebar"));
toolbar->Realize();
SetToolBar(toolbar);
// Set the default tool to be selected
SetCurrentTool(_T(""));
// Set up GL canvas:
int glAttribList[] = {
WX_GL_RGBA,
WX_GL_DOUBLEBUFFER,
WX_GL_DEPTH_SIZE, 24, // TODO: wx documentation doesn't say 24 is valid
WX_GL_BUFFER_SIZE, 24, // colour bits
WX_GL_MIN_ALPHA, 8, // alpha bits
0
};
Canvas* canvas = new GameCanvas(m_SectionLayout.GetCanvasParent(), glAttribList);
m_SectionLayout.SetCanvas(canvas);
// The canvas' context gets made current on creation; but it can only be
// current for one thread at a time, and it needs to be current for the
// thread that is doing the draw calls, so disable it for this one.
wglMakeCurrent(NULL, NULL);
// Set up sidebars:
m_SectionLayout.Build();
// Send setup messages to game engine:
POST_MESSAGE(SetContext, (canvas->GetContext()));
POST_MESSAGE(Init, ());
canvas->InitSize();
// Start with a blank map (so that the editor can assume there's always
// a valid map loaded)
POST_MESSAGE(GenerateMap, (9));
POST_MESSAGE(RenderEnable, (eRenderView::GAME));
// Set up a timer to make sure tool-updates happen frequently (in addition
// to the idle handler (which makes them happen more frequently if there's nothing
// else to do))
m_Timer.SetOwner(this);
m_Timer.Start(20);
}
float ScenarioEditor::GetSpeedModifier()
{
if (wxGetKeyState(WXK_SHIFT) && wxGetKeyState(WXK_CONTROL))
return 1.f/64.f;
else if (wxGetKeyState(WXK_CONTROL))
return 1.f/4.f;
else if (wxGetKeyState(WXK_SHIFT))
return 4.f;
else
return 1.f;
}
void ScenarioEditor::OnClose(wxCloseEvent&)
{
SetCurrentTool(_T(""));
POST_MESSAGE(Shutdown, ());
qExit().Post();
// blocks until engine has noticed the message, so we won't be
// destroying the GLCanvas while it's still rendering
Destroy();
}
static void UpdateTool()
{
// Don't keep posting events if the game can't keep up
if (g_FrameHasEnded)
{
g_FrameHasEnded = false; // (thread safety doesn't matter here)
// TODO: Smoother timing stuff?
static double last = g_Timer.GetTime();
double time = g_Timer.GetTime();
GetCurrentTool().OnTick(time-last);
last = time;
}
}
void ScenarioEditor::OnTimer(wxTimerEvent&)
{
UpdateTool();
}
void ScenarioEditor::OnIdle(wxIdleEvent&)
{
UpdateTool();
}
void ScenarioEditor::OnQuit(wxCommandEvent&)
{
Close();
}
void ScenarioEditor::OnUndo(wxCommandEvent&)
{
GetCommandProc().Undo();
}
void ScenarioEditor::OnRedo(wxCommandEvent&)
{
GetCommandProc().Redo();
}
//////////////////////////////////////////////////////////////////////////
// TODO (eventually): replace all this file-handling stuff with the Workspace Editor
void ScenarioEditor::OnOpen(wxCommandEvent& WXUNUSED(event))
{
wxFileDialog dlg (NULL, wxFileSelectorPromptStr,
Datafile::GetDataDirectory() + _T("/mods/official/maps/scenarios"), m_OpenFilename,
_T("PMP files (*.pmp)|*.pmp|All files (*.*)|*.*"),
wxOPEN);
wxString cwd = wxFileName::GetCwd();
if (dlg.ShowModal() == wxID_OK)
{
wxBusyInfo busy(_("Loading map"));
wxBusyCursor busyc;
// TODO: Work when the map is not in .../maps/scenarios/
std::wstring map = dlg.GetFilename().c_str();
// Deactivate tools, so they don't carry forwards into the new CWorld
// and crash.
SetCurrentTool(_T(""));
// TODO: clear the undo buffer, etc
POST_MESSAGE(LoadMap, (map));
SetOpenFilename(dlg.GetFilename());
// Wait for it to load, while the wxBusyInfo is telling the user that we're doing that
qPing qry;
qry.Post();
}
wxCHECK_RET(cwd == wxFileName::GetCwd(), _T("cwd changed"));
// paranoia - MSDN says OFN_NOCHANGEDIR (used when we don't give wxCHANGE_DIR)
// "is ineffective for GetOpenFileName", but it seems to work anyway
// TODO: Make this a non-undoable command
}
void ScenarioEditor::OnSave(wxCommandEvent& event)
{
if (m_OpenFilename.IsEmpty())
OnSaveAs(event);
else
{
wxBusyInfo busy(_("Saving map"));
std::wstring map = m_OpenFilename.c_str();
POST_MESSAGE(SaveMap, (map));
// Wait for it to finish saving
qPing qry;
qry.Post();
}
}
void ScenarioEditor::OnSaveAs(wxCommandEvent& WXUNUSED(event))
{
wxFileDialog dlg (NULL, wxFileSelectorPromptStr,
Datafile::GetDataDirectory() + _T("/mods/official/maps/scenarios"), m_OpenFilename,
_T("PMP files (*.pmp)|*.pmp|All files (*.*)|*.*"),
wxSAVE | wxOVERWRITE_PROMPT);
if (dlg.ShowModal() == wxID_OK)
{
wxBusyInfo busy(_("Saving map"));
// TODO: Work when the map is not in .../maps/scenarios/
std::wstring map = dlg.GetFilename().c_str();
POST_MESSAGE(SaveMap, (map));
SetOpenFilename(dlg.GetFilename());
// Wait for it to finish saving
qPing qry;
qry.Post();
}
}
void ScenarioEditor::SetOpenFilename(const wxString& filename)
{
SetTitle(wxString::Format(_("Atlas - Scenario Editor - %s"),
filename.IsEmpty() ? _("(untitled)") : filename));
m_OpenFilename = filename;
}
//////////////////////////////////////////////////////////////////////////
void ScenarioEditor::OnWireframe(wxCommandEvent& event)
{
POST_MESSAGE(RenderStyle, (event.IsChecked()));
}
void ScenarioEditor::OnMessageTrace(wxCommandEvent& event)
{
POST_MESSAGE(MessageTrace, (event.IsChecked()));
}
void ScenarioEditor::OnScreenshot(wxCommandEvent& WXUNUSED(event))
{
POST_MESSAGE(Screenshot, (10));
}
//////////////////////////////////////////////////////////////////////////
Position::Position(const wxPoint& pt)
: type(1)
{
type1.x = pt.x;
type1.y = pt.y;
}
//////////////////////////////////////////////////////////////////////////
/* Disabled (and should be removed if it turns out to be unnecessary)
- see MessagePasserImpl.cpp for information
static void QueryCallback()
{
// If this thread completely blocked on the semaphore inside Query, it would
// never respond to window messages, and the system deadlocks if the
// game tries to display an assertion failure dialog. (See
// WaitForSingleObject on MSDN.)
// So, this callback is called occasionally, and gives wx a change to
// handle messages.
// 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();
// 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();
// evtLoop might be NULL, particularly if we're still initialising windows
// and haven't got into the normal event loop yet. But we'd have to process
// messages anyway, to avoid the deadlocks that this is for. So, don't bother
// with that and just crash instead.
// (Maybe it could be solved better by constructing/finding an event loop
// object here and setting it as the global one, assuming it's not overwritten
// later by wx.)
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 QueryMessage::Post()
{
// g_MessagePasser->Query(this, &QueryCallback);
g_MessagePasser->Query(this, NULL);
}