|
SetupVarTable(editor, "editor.") |
|
|
|
if FirstLoad then |
|
XEditorHideTexts = false |
|
XEditorOriginalHandleRand = HandleRand |
|
end |
|
|
|
XEditorHRSettings = { |
|
ResolutionPercent = 100, |
|
EnablePreciseSelection = 1, |
|
ObjectCounter = 1, |
|
VerticesCounter = 1, |
|
TR_MaxChunksPerFrame=100000, |
|
} |
|
|
|
|
|
local handle_seed = AsyncRand() |
|
function XEditorNewHandleRand(rand) rand, handle_seed = BraidRandom(handle_seed, rand) return rand end |
|
function XEditorGetHandleSeed(seed) return handle_seed end |
|
function XEditorSetHandleSeed(seed) handle_seed = seed end |
|
|
|
function IsEditorActive() |
|
return editor.Active |
|
end |
|
|
|
function EditorActivate() |
|
if Platform.editor and not editor.Active and GetMap() ~= "" then |
|
editor.Active = true |
|
NetPauseUpdateHash("Editor") |
|
local executeBeforeEnter = {} |
|
Msg("GameEnteringEditor", executeBeforeEnter) |
|
for _, fn in ipairs(executeBeforeEnter) do |
|
fn() |
|
end |
|
OpenDialog("XEditor") |
|
HandleRand = XEditorNewHandleRand |
|
Msg("GameEnterEditor") |
|
SuspendDesyncErrors("Editor") |
|
end |
|
end |
|
|
|
function EditorDeactivate() |
|
if editor.Active then |
|
editor.Active = false |
|
local executeBeforeExit = {} |
|
Msg("GameExitEditor", executeBeforeExit) |
|
for _, fn in ipairs(executeBeforeExit) do |
|
fn() |
|
end |
|
HandleRand = XEditorOriginalHandleRand |
|
CloseDialog("XEditor") |
|
NetResumeUpdateHash("Editor") |
|
ResumeDesyncErrors("Editor") |
|
end |
|
end |
|
|
|
function OnMsg.ChangeMap(map) |
|
if map == "" then |
|
EditorDeactivate() |
|
end |
|
end |
|
|
|
if FirstLoad then |
|
CameraMaxZoomSpeed = tonumber(hr.CameraMaxZoomSpeed) |
|
CameraMaxZoomSpeedSlow = tonumber(hr.CameraMaxZoomSpeedSlow) |
|
CameraMaxZoomSpeedFast = tonumber(hr.CameraMaxZoomSpeedFast) |
|
end |
|
|
|
function OnMsg.ChangeMapDone(map) |
|
if map == "" then return end |
|
|
|
local small_map_size = 1024 * guim |
|
local map_size = Max(terrain.GetMapSize()) |
|
local coef = Max(map_size * 1.0 / small_map_size, 1.0) |
|
hr.CameraMaxZoomSpeed = tostring(CameraMaxZoomSpeed * coef) |
|
hr.CameraMaxZoomSpeedSlow = tostring(CameraMaxZoomSpeedSlow * coef) |
|
hr.CameraMaxZoomSpeedFast = tostring(CameraMaxZoomSpeedFast * coef) |
|
end |
|
|
|
|
|
|
|
|
|
|
|
|
|
DefineClass.XEditor = { |
|
__parents = { "XDialog" }, |
|
Dock = "box", |
|
InitialMode = "XEditorTool", |
|
ZOrder = -1, |
|
|
|
mode = false, |
|
mode_dialog = false, |
|
play_box = false, |
|
toolbar_context = false, |
|
help_popup = false, |
|
} |
|
|
|
function XEditor:Open(...) |
|
local size = terrain.GetMapSize() |
|
XChangeCameraTypeLayer:new({ CameraType = "cameraMax", CameraClampXY = size, CameraClampZ = 2 * size }, self) |
|
XPauseLayer:new({ togglePauseDialog = false, keep_sounds = true }, self) |
|
|
|
|
|
XShortcutsSetMode("Editor", function() EditorDeactivate() end) |
|
XEditorHRSettings.EnableCloudsShadow = EditorSettings:GetCloudShadows() and 1 or 0 |
|
table.change(hr, "Editor", XEditorHRSettings) |
|
SetSplitScreenEnabled(false, "Editor") |
|
ShowMouseCursor("Editor") |
|
|
|
self.toolbar_context = { |
|
filter_buttons = LocalStorage.FilteredCategories, |
|
roof_visuals_enabled = LocalStorage.FilteredCategories["Roofs"], |
|
} |
|
OpenDialog("XEditorToolbar", XShortcutsTarget, self.toolbar_context):SetVisible(EditorSettings:GetEditorToolbar()) |
|
OpenDialog("XEditorStatusbar", XShortcutsTarget, self.toolbar_context) |
|
|
|
if EditorSettings:GetShowPlayArea() then |
|
self.play_box = PlaceTerrainBox(GetPlayBox(), nil, nil, nil, nil, "depth test") |
|
end |
|
|
|
|
|
XDialog.Open(self, ...) |
|
CreateRealTimeThread(XEditorUpdateHiddenTexts) |
|
self:NotifyEditorObjects("EditorEnter") |
|
ShowConsole(false) |
|
|
|
if IsKindOf(XShortcutsTarget, "XDarkModeAwareDialog") then |
|
XShortcutsTarget:SetDarkMode(GetDarkModeSetting()) |
|
end |
|
|
|
|
|
self:SetMode("XSelectObjectsTool") |
|
editor.SetSel(SelectedObj and { SelectedObj } or Selection) |
|
|
|
|
|
if not LocalStorage.editor_help_shown then |
|
self:ShowHelpText() |
|
LocalStorage.editor_help_shown = true |
|
SaveLocalStorage() |
|
end |
|
end |
|
|
|
function XEditor:Close(...) |
|
|
|
XShortcutsSetMode("Game") |
|
table.restore(hr, "Editor") |
|
SetSplitScreenEnabled(true, "Editor") |
|
HideMouseCursor("Editor") |
|
CloseDialog("XEditorToolbar") |
|
CloseDialog("XEditorStatusbar") |
|
CloseDialog("XEditorRoomTools") |
|
editor.ClearSel() |
|
XShortcutsTarget:SetStatusTextLeft("") |
|
XShortcutsTarget:SetStatusTextRight("") |
|
XEditorDeleteMapButtons() |
|
if self.help_popup and self.help_popup.window_state == "open" then |
|
self.help_popup:Close() |
|
end |
|
|
|
if IsValid(self.play_box) then |
|
DoneObject(self.play_box) |
|
end |
|
|
|
|
|
self:NotifyEditorObjects("EditorExit") |
|
XDialog.Close(self, ...) |
|
end |
|
|
|
function XEditor:NotifyEditorObjects(method) |
|
SuspendPassEdits("Editor") |
|
MapForEach(true, "EditorObject", function(obj) |
|
if not EditorCursorObjs[obj] then |
|
obj[method](obj) |
|
end |
|
end) |
|
ResumePassEdits("Editor") |
|
end |
|
|
|
function XEditor:SetMode(mode, context) |
|
if mode == self.Mode and (context or false) == self.mode_param then return end |
|
if self.mode_dialog then |
|
self.mode_dialog:Close() |
|
XPopupMenu.ClosePopupMenus() |
|
end |
|
|
|
self:UpdateStatusText() |
|
|
|
assert(IsKindOf(g_Classes[mode], "XEditorTool")) |
|
self.mode_dialog = OpenDialog(mode, self, context) |
|
self.mode_param = context |
|
self.Mode = mode |
|
self:ActionsUpdated() |
|
GetDialog("XEditorToolbar"):ActionsUpdated() |
|
GetDialog("XEditorStatusbar"):ActionsUpdated() |
|
XEditorUpdateToolbars() |
|
if not self.mode_dialog.ToolKeepSelection then |
|
editor.ClearSel() |
|
end |
|
self.mode_dialog:SetFocus() |
|
|
|
Msg("EditorToolChanged", mode, IsKindOf(self.mode_dialog, "XEditorPlacementHelperHost") and self.mode_dialog.helper_class) |
|
end |
|
|
|
function XEditor:UpdateStatusText() |
|
local left_status = mapdata.ModMapPath and _InternalTranslate(mapdata.DisplayName, nil, false) or mapdata.id |
|
if config.ModdingToolsInUserMode then |
|
local extra_row = |
|
(not mapdata.ModMapPath and not editor.ModItem) and "<color 255 60 60>Original map - saving disabled!" or |
|
not editor.IsModdingEditor() and "<color 255 60 60>Editor not opened from a mod item - saving disabled!" or |
|
editor.ModItem:IsPacked() and "<color 255 60 60>The map's mod is not unpacked for editing - saving disabled!" or |
|
string.format("%s%s", editor.ModItem:GetEditorMessage(), Literal(editor.ModItem.mod.title)) |
|
left_status = string.format("%s\n%s", left_status, extra_row) |
|
else |
|
left_status = left_status .. (mapdata.group ~= "Default" and " (" .. mapdata.group .. ")" or "") |
|
if EditedMapVariation then |
|
left_status = string.format("%s\n<style EditorMapVariation>Variation: %s", left_status, EditedMapVariation.id) |
|
if EditedMapVariation.save_in ~= "" then |
|
left_status = left_status .. string.format(" (%s)", EditedMapVariation.save_in) |
|
end |
|
end |
|
end |
|
|
|
XShortcutsTarget:SetStatusTextLeft(left_status) |
|
XShortcutsTarget:SetStatusTextRight(string.format("Object details: %s (Ctrl-Alt-/)", EngineOptions.ObjectDetail)) |
|
XEditorCreateMapButtons() |
|
end |
|
|
|
function XEditor:ShowHelpText() |
|
self.help_popup = CreateMessageBox(XShortcutsTarget, |
|
Untranslated("Welcome to the Map Editor!"), |
|
Untranslated([[Here are some short tips to get you started. |
|
|
|
Camera controls: |
|
• <mouse_wheel_up> - zoom in/out |
|
• hold <middle_click> - pan the camera |
|
• hold Ctrl - faster movement |
|
• hold Alt - look around |
|
• hold Ctrl+Alt - rotate camera |
|
|
|
Look through the editor tools on the left - for example, press N to place objects. |
|
|
|
Use <right_click> to access object properties and actions.]])) |
|
end |
|
|
|
|
|
|
|
|
|
function OnMsg.ShortcutsReloaded() |
|
XShortcutsTarget:ActionById("E_EditorSettings"):SetActionSortKey("999998") |
|
XShortcutsTarget:ActionById("E_EditorHelpText"):SetActionSortKey("999999") |
|
end |
|
|
|
function OnMsg.EditorSelectionChanged() |
|
local xeditor = GetDialog("XEditor") |
|
if xeditor then |
|
ObjModified(xeditor.toolbar_context) |
|
end |
|
end |
|
|
|
function OnMsg.DevMenuVisible(visible) |
|
local toolbar = GetDialog("XEditorToolbar") |
|
if toolbar then |
|
toolbar:SetVisible(visible and EditorSettings:GetEditorToolbar()) |
|
end |
|
end |
|
|
|
function OnMsg.ChangeMapDone() |
|
if IsEditorActive() then |
|
local dlg = GetDialog("XEditor") |
|
dlg:NotifyEditorObjects("EditorEnter") |
|
dlg:UpdateStatusText() |
|
if not cameraMax.IsActive() then |
|
cameraMax.Activate() |
|
end |
|
end |
|
end |
|
|
|
|
|
|
|
|
|
function OnMsg.EditorToolChanged(mode, helper_class) |
|
if g_Classes[mode].UsesCodeRenderables or helper_class and g_Classes[helper_class].UsesCodeRenderables then |
|
if hr.RenderCodeRenderables == 0 then |
|
hr.RenderCodeRenderables = 1 |
|
local statusbar = GetDialog("XEditorStatusbar") |
|
if statusbar then |
|
statusbar:ActionsUpdated() |
|
end |
|
ExecuteWithStatusUI("Code renderables turned ON!", function() Sleep(2000) end) |
|
end |
|
end |
|
XEditorSettingsJustOpened = XEditorGetCurrentTool().FocusPropertyInSettings |
|
end |
|
|
|
function OnMsg.EditorSelectionChanged(sel) |
|
if hr.RenderCodeRenderables == 0 and #sel > 0 then |
|
ExecuteWithStatusUI("Code renderables are OFF!\n\nPress Alt-Shift-R to show selection.", function() Sleep(1000) end) |
|
end |
|
end |
|
|
|
|
|
|
|
|
|
if FirstLoad then |
|
XEditorContextMenu = false |
|
end |
|
|
|
function XEditorOpenContextMenu(context, pos) |
|
XEditorContextMenu = XShortcutsTarget:OpenContextMenu(context, pos) |
|
end |
|
|
|
function XEditorIsContextMenuOpen() |
|
return XEditorContextMenu and XEditorContextMenu.window_state == "open" |
|
end |
|
|
|
|
|
|
|
|
|
if FirstLoad then |
|
EditorAutosaveThread = false |
|
EditorAutosaveNextTime = false |
|
end |
|
|
|
function EditorCreateAutosaveThread() |
|
EditorDeleteAutosaveThread() |
|
EditorAutosaveThread = CreateRealTimeThread(function() |
|
if EditorSettings:GetAutosaveTime() == 0 or config.ModdingToolsInUserMode then return end |
|
EditorAutosaveNextTime = EditorAutosaveNextTime or now() + EditorSettings:GetAutosaveTime() * 60 * 1000 |
|
while true do |
|
if EditorAutosaveNextTime > now() then |
|
Sleep(EditorAutosaveNextTime - now()) |
|
end |
|
XEditorSaveMap() |
|
EditorAutosaveNextTime = now() + EditorSettings:GetAutosaveTime() * 60 * 1000 |
|
end |
|
end) |
|
end |
|
|
|
function EditorDeleteAutosaveThread() |
|
DeleteThread(EditorAutosaveThread) |
|
end |
|
|
|
OnMsg.GameEnterEditor = EditorCreateAutosaveThread |
|
OnMsg.GameExitEditor = EditorDeleteAutosaveThread |
|
|
|
|
|
|
|
|
|
function XEditorGetCurrentTool() |
|
return GetDialog("XEditor") and GetDialog("XEditor").mode_dialog |
|
end |
|
|
|
function XEditorIsDefaultTool() |
|
return GetDialogMode("XEditor") == "XSelectObjectsTool" |
|
end |
|
|
|
function XEditorSetDefaultTool(helper_class, properties) |
|
XEditorShowCustomFilters = false |
|
if XEditorIsDefaultTool() then |
|
ObjModified(XEditorGetCurrentTool()) |
|
XEditorUpdateToolbars() |
|
end |
|
SetDialogMode("XEditor", "XSelectObjectsTool") |
|
if helper_class then |
|
GetDialog("XEditor").mode_dialog:SetHelperClass(helper_class, properties) |
|
end |
|
end |
|
|
|
function XEditorRemoveFocusFromToolbars() |
|
local focused_ctrl = terminal.desktop:GetKeyboardFocus() |
|
if focused_ctrl and (GetDialog(focused_ctrl) == GetDialog("XEditorToolbar") or GetDialog(focused_ctrl) == GetDialog("XEditorStatusbar")) then |
|
terminal.desktop:RemoveKeyboardFocus(focused_ctrl, true) |
|
end |
|
end |
|
|
|
function XEditorUpdateToolbars() |
|
local editor = GetDialog("XEditor") |
|
if editor then |
|
editor:DeleteThread("toolbar_update") |
|
editor:CreateThread("toolbar_update", function() |
|
Sleep(200) |
|
ObjModified(editor.toolbar_context) |
|
end) |
|
end |
|
end |
|
|
|
function XEditorUpdateStatusText() |
|
local editor = GetDialog("XEditor") |
|
if editor then |
|
editor:UpdateStatusText() |
|
end |
|
end |
|
|
|
function XEditorSaveMap(skipBackup, force) |
|
WaitChangeMapDone() |
|
ExecuteWithStatusUI( |
|
EditedMapVariation and "Saving map variation..." or "Saving map...", |
|
function() SaveMap(skipBackup, force) end, |
|
"wait") |
|
end |
|
|
|
function XEditorGetVisibleObjects(filter_func) |
|
local frame = (GetFrameMark() / 1024 - 1) * 1024 |
|
filter_func = filter_func or function() return true end |
|
return MapGet("map", "attached", false, nil, const.efVisible, function(x) return x:GetFrameMark() - frame > 0 and filter_func(x) end) or empty_table |
|
end |
|
|
|
local function ApproxDisplayColor(color) |
|
local r, g, b = GetRGB(color) |
|
local upper_bound = Max(100, Max(r, Max(g, b))) |
|
r = MulDivRound(r, 255, upper_bound) |
|
g = MulDivRound(g, 255, upper_bound) |
|
b = MulDivRound(b, 255, upper_bound) |
|
return RGB(r, g, b) |
|
end |
|
|
|
function GetTerrainTexturesItems() |
|
local items = {} |
|
for _, descr in pairs(TerrainTextures) do |
|
local image = GetTerrainImage(descr.basecolor) |
|
items[#items + 1] = { |
|
text = descr.id, |
|
value = descr.id, |
|
color = ApproxDisplayColor(descr.color_modifier), |
|
image = image, |
|
} |
|
end |
|
table.sortby_field(items, "value") |
|
return items |
|
end |
|
|
|
function GetDarkModeSetting() |
|
local setting = XEditorSettings:GetDarkMode() |
|
if setting == "Follow system" then |
|
return GetSystemDarkModeSetting() |
|
else |
|
return setting and setting ~= "Light" |
|
end |
|
end |
|
|
|
function CanSelect(obj) |
|
if not obj or not editor.CanSelect(obj) then |
|
if not const.SlabSizeX or not IsKindOf(obj, "EditorLineGuide") then |
|
return false |
|
end |
|
end |
|
if XEditorShowCustomFilters then |
|
local filter_mode = XSelectObjectsTool:GetFilterMode() |
|
local objects = XSelectObjectsTool:GetFilterObjects() or empty_table |
|
local filtered = objects[XEditorPlaceId(obj)] |
|
if filter_mode == "On" and not filtered or filter_mode == "Negate" and filtered then |
|
return false |
|
end |
|
end |
|
return XEditorFilters:CanSelect(obj) |
|
end |
|
|
|
|
|
function GetObjectAtCursor() |
|
|
|
local sel = GetNextObjectAtScreenPos(function(o) return IsKindOfClasses(o, "Decal", "WaterObj") and editor.IsSelected(o) end, "topmost") |
|
if sel then return sel end |
|
|
|
local solid, transparent = GetPreciseCursorObj() |
|
local obj = (CanSelect(transparent) and transparent) or (CanSelect(solid) and solid) |
|
obj = obj or XEditorSettings:GetSmartSelection() and GetNextObjectAtScreenPos(CanSelect, "topmost") |
|
|
|
return obj or GetNextObjectAtScreenPos(function(o) return IsKindOfClasses(o, "Decal", "WaterObj") and CanSelect(o) end, "topmost") |
|
end |
|
|
|
function HasAlignedObjs(objs) |
|
for _, obj in ipairs(objs) do |
|
if obj:IsKindOf("AlignedObj") then |
|
return true |
|
end |
|
end |
|
end |
|
|
|
function XEditorSnapPos(obj, initial_pos, delta, by_slabs) |
|
if obj:IsKindOf("AlignedObj") then |
|
if obj.AlignObj ~= AlignedObj.AlignObj then |
|
obj:AlignObj(initial_pos + delta) |
|
end |
|
elseif by_slabs then |
|
obj:SetPos(initial_pos + XEditorSettings:PosSnap(delta, "by_slabs")) |
|
else |
|
obj:SetPos(XEditorSettings:PosSnap(initial_pos + delta)) |
|
end |
|
end |
|
|
|
function XEditorSetPosAxisAngle(obj, pos, axis, angle) |
|
if obj:IsKindOf("AlignedObj") then |
|
obj:AlignObj(pos, angle, axis) |
|
else |
|
obj:SetPos(pos) |
|
if axis and angle then |
|
obj:SetAxisAngle(axis, angle) |
|
end |
|
end |
|
end |
|
|
|
local suspend_id = 1 |
|
|
|
function SuspendPassEditsForEditOp(objs) |
|
NetPauseUpdateHash("EditOp") |
|
table.change(config, "XEditor"..suspend_id, { |
|
PartialPassEdits = #(objs or editor.GetSel()) < 500, |
|
}) |
|
SuspendPassEdits("XEditor"..suspend_id) |
|
suspend_id = suspend_id + 1 |
|
end |
|
|
|
function ResumePassEditsForEditOp() |
|
suspend_id = suspend_id - 1 |
|
ResumePassEdits("XEditor"..suspend_id, true) |
|
table.restore(config, "XEditor"..suspend_id, true) |
|
NetResumeUpdateHash("EditOp") |
|
assert(suspend_id >= 1) |
|
end |
|
|
|
function ArePassEditsForEditOpSuspended() |
|
return suspend_id > 1 |
|
end |
|
|
|
function XEditorGroupsComboItems(objects) |
|
local items = {} |
|
local read_only = #objects == 0 |
|
local group_names = table.keys2(Groups or empty_table, "sorted") |
|
for _, name in ipairs(group_names) do |
|
local group = Groups[name] |
|
if next(group) then |
|
local in_group_count = #table.intersection(group, objects) |
|
items[#items + 1] = { |
|
id = name, |
|
value = not read_only and in_group_count == #objects and true or in_group_count > 0 and Undefined() or false, |
|
read_only = read_only, |
|
} |
|
end |
|
end |
|
return items |
|
end |
|
|
|
local cam_pos, cam_lookat, stored_sel |
|
|
|
function XEditorShowObjects(objs, show) |
|
if show == "select_permanently" then |
|
editor.ClearSel("dont_notify") |
|
editor.SetSel(objs) |
|
ViewObjects(objs) |
|
cam_pos, cam_lookat, stored_sel = nil, nil, nil |
|
elseif show then |
|
cam_pos, cam_lookat = GetCamera() |
|
stored_sel = editor.GetSel() |
|
editor.SetSel(objs, "dont_notify") |
|
ViewObjects(objs) |
|
elseif cam_pos then |
|
SetCamera(cam_pos, cam_lookat) |
|
editor.SetSel(stored_sel, "dont_notify") |
|
end |
|
end |
|
|
|
function XEditorUpdateHiddenTexts() |
|
for _, obj in ipairs(MapGet("map", "Text")) do |
|
if obj.hide_in_editor then |
|
obj:SetVisible(not XEditorHideTexts) |
|
end |
|
end |
|
end |
|
|
|
function XEditorChooseAndChangeMap() |
|
if IsMessageBoxOpen("XEditorChooseAndChangeMap") then return end |
|
CreateRealTimeThread(function() |
|
local caption = "Choose map:" |
|
local maps = table.ifilter(ListMaps(), function(idx, map) return not IsOldMap(map) end) |
|
table.insert(maps, 1, "") |
|
local parent_container = XWindow:new({}, terminal.desktop) |
|
parent_container:SetScaleModifier(point(1250, 1250)) |
|
|
|
local map = WaitListChoice(parent_container, maps, caption, GetMapName(), nil, nil, "XEditorChooseAndChangeMap") |
|
if not map then return end |
|
|
|
DeveloperChangeMap(map) |
|
end) |
|
end |