myspace / CommonLua /Editor /editor.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
20.2 kB
XEditorPasteFuncs = {}
function editor.SelectByClass(...)
editor.ClearSel()
editor.AddToSel(MapGet("map", ...) or empty_table)
end
function ToggleEnterExitEditor()
if Platform.editor then
CreateRealTimeThread(function()
while IsChangingMap() or IsEditorSaving() do
WaitChangeMapDone()
if IsEditorSaving() then
WaitMsg("SaveMapDone")
end
end
if GetMap() == "" then
print("There is no map loaded")
return
end
if IsEditorActive() then
EditorDeactivate()
else
EditorActivate()
end
end)
end
end
function EditorViewMapObject(obj, dist, selection)
local la = IsValid(obj) and obj:GetVisualPos() or InvalidPos()
if la == InvalidPos() then
return
end
if not cameraMax.IsActive() then cameraMax.Activate(1) end
local cur_pos, cur_la = cameraMax.GetPosLookAt()
if cur_la == cur_pos then
-- cam not initialized
cur_pos = cur_la - point(guim, guim, guim)
end
la = la:SetTerrainZ()
local pos = la - SetLen(cur_la - cur_pos, (dist or 40*guim) + obj:GetRadius())
cameraMax.SetCamera(pos, la, 200, "Sin in/out")
if selection then
editor.ClearSel()
editor.AddToSel{obj}
OpenGedGameObjectEditor(editor.GetSel())
end
end
local objs
function OnMsg.DoneMap()
objs = nil
end
function _OpenGedGameObjectEditorInGame(reopen_only)
if not GedObjectEditor then
GedObjectEditor = OpenGedApp("GedObjectEditor", objs, { WarningsUpdateRoot = "root" }) or false
else
GedObjectEditor:UnbindObjs("root")
if not reopen_only then
GedObjectEditor:Call("rfnApp", "Activate")
end
GedObjectEditor:BindObj("root", objs)
end
GedObjectEditor:SelectAll("root")
objs = nil
end
function OpenGedGameObjectEditorInGame(obj, reopen_only)
if not obj or not GedObjectEditor and reopen_only then return end
objs = table.create_add_unique(objs, obj)
DelayedCall(0, _OpenGedGameObjectEditorInGame, reopen_only)
end
function CObject:AsyncCheatProperties()
OpenGedGameObjectEditorInGame(self)
end
function OnMsg.SelectedObjChange(obj)
OpenGedGameObjectEditorInGame(obj, "reopen_only")
end
function EditorWaitViewMapObjectByHandle(handle, map, ged)
if GetMapName() ~= map then
if ged then
local answer = ged:WaitQuestion("Change map required!", string.format("Change map to %s?", map))
if answer ~= "ok" then
return
end
end
CloseMenuDialogs()
ChangeMap(map)
StoreLastLoadedMap()
end
EditorActivate()
WaitNextFrame()
local obj = HandleToObject[handle]
if not IsValid(obj) then
print("ERROR: no such object")
return
end
editor.ChangeSelWithUndoRedo({obj})
EditorViewMapObject(obj)
end
----
if FirstLoad then
GedSingleObjectPropEditor = false
end
function OpenGedSingleObjectPropEditor(obj, reopen_only)
if not obj then return end
CreateRealTimeThread(function()
if not GedSingleObjectPropEditor and reopen_only then
return
end
if not GedSingleObjectPropEditor then
GedSingleObjectPropEditor = OpenGedApp("GedSingleObjectPropEditor", obj) or false
else
GedSingleObjectPropEditor:UnbindObjs("root")
GedSingleObjectPropEditor:Call("rfnApp", "Activate")
GedSingleObjectPropEditor:BindObj("root", obj)
end
end)
end
function OnMsg.GedClosing(ged_id)
if GedSingleObjectPropEditor and GedSingleObjectPropEditor.ged_id == ged_id then
GedSingleObjectPropEditor = false
end
end
----
-- Grounds all selected objects.
-- If *relative* is true, grounds only the lowest one and then sets the rest of them relative to it
function editor.ResetZ(relative)
local sel = editor:GetSel()
if #sel < 1 then
return
end
local min_z_ground = false
local min_z_air = false
local min_idx = false
if relative then
-- Get the lowest of all objects and where it will be grounded
for i = 1, #sel do
local obj = sel[i]
local _, _, pos_z = obj:GetVisualPosXYZ()
if not min_z_air or min_z_air > pos_z then
min_z_air = pos_z
min_idx = i
end
end
if min_idx then
local obj = sel[min_idx]
local pos = obj:GetVisualPos2D()
local o, z = GetWalkableObject( pos )
if o ~= nil and not IsFlagSet( obj:GetEnumFlags(), const.efWalkable ) then
min_z_ground = z
else
min_z_ground = terrain.GetSurfaceHeight(pos)
end
end
end
SuspendPassEditsForEditOp()
XEditorUndo:BeginOp{ objects = sel, name = "Snap to terrain" }
for i = 1, #sel do
local obj = sel[i]
obj:ClearGameFlags(const.gofOnRoof)
local pos = obj:GetVisualPos()
local pos_z = pos:z()
pos = pos:SetInvalidZ()
local o, z = GetWalkableObject( pos )
if o ~= nil and not IsFlagSet( obj:GetEnumFlags(), const.efWalkable ) then
if relative and min_z_air and min_z_ground then
pos = point( pos:x(), pos:y(), pos_z - min_z_air + min_z_ground)
else
pos = point( pos:x(), pos:y())
end
elseif relative and min_z_air and min_z_ground then
pos = point( pos:x(), pos:y(), pos_z - min_z_air + min_z_ground)
end
obj:SetPos( pos )
end
local objects = {}
local cfEditorCallback = const.cfEditorCallback
for i = 1, #sel do
if sel[i]:GetClassFlags(cfEditorCallback) ~= 0 then
objects[#objects + 1] = sel[i]
end
end
if #objects > 0 then
Msg("EditorCallback", "EditorCallbackMove", objects)
end
Msg("EditorResetZ")
XEditorUndo:EndOp(sel)
ResumePassEditsForEditOp()
end
-- this method correctly resets anim back to game time, i.e. it resets the anim timestamps so it doesn't have to wait far in the future to start
function ObjectAnimToGameTime(object)
object:SetRealtimeAnim(false)
local e = object:GetEntity()
if IsValidEntity(e) then
object:SetAnimSpeed(1, object:GetAnimSpeed(), 0) -- so the obj can set its anim timestamps correctly in game time
end
end
function editor.GetObjectsCenter(objs)
local min_z
local b = box()
for i = 1, #objs do
local pos = objs[i]:GetPos()
b = Extend(b, pos)
if pos:IsValidZ() then
min_z = Min(min_z or pos:z(), pos:z())
end
end
local center = b:Center()
center = min_z and center:SetZ(min_z) or center
return center
end
function editor.Serialize(objs, collections, center, options)
local objs_orig = objs
center = center or editor.GetObjectsCenter(objs)
options = options or empty_table
local GetVisualPosXYZ = CObject.GetVisualPosXYZ
local GetHeight = terrain.GetHeight
local GetTerrainNormal = terrain.GetTerrainNormal
local GetOrientation = CObject.GetOrientation
local IsValidZ = CObject.IsValidZ
local GetClassFlags = CObject.GetClassFlags
local GetGameFlags = CObject.GetGameFlags
local GetCollectionIndex = CObject.GetCollectionIndex
local IsValidPos = CObject.IsValidPos
local cfLuaObject = const.cfLuaObject
local InvalidPos = InvalidPos
local IsT = IsT
if not collections then
collections = {}
for i = 1, #objs do
local col_idx = GetCollectionIndex(objs[i])
if col_idx ~= 0 then
collections[col_idx] = true
end
end
end
local cols = {}
for idx in pairs(collections) do
cols[#cols + 1] = Collections[idx]
end
local cobjects
if options.compact_cobjects then
if objs == objs_orig then objs = table.icopy(objs) end
for i = #objs,1,-1 do
local obj = objs[i]
if GetClassFlags(obj, cfLuaObject) == 0 then
cobjects = cobjects or {}
cobjects[#cobjects + 1] = obj
table.remove(objs, i)
end
end
end
local get_collection_index_func
local locked_idx = editor.GetLockedCollectionIdx()
if locked_idx ~= 0 and not options.ignore_locked_coll then
get_collection_index_func = function(obj)
local idx = GetCollectionIndex(obj)
if idx ~= locked_idx then
return idx
end
end
end
local no_translation = options.no_translation
local function get_prop(obj, prop_id)
if prop_id == "CollectionIndex" and get_collection_index_func then
return get_collection_index_func(obj)
elseif prop_id == "Pos" then
return IsValidPos(obj) and (obj:GetPos() - center) or InvalidPos()
end
local value = obj:GetProperty(prop_id)
if no_translation and value ~= "" and IsT(value) then
StoreErrorSource(obj, "Translation found for property", prop_id)
return ""
end
return value
end
local code = pstr("", 1024)
code:append(options.comment_tag or "--[[HGE place script]]--")
code:append("\nSetObjectsCenter(")
code:appendv(center)
code:append(")\n")
ObjectsToLuaCode(cols, code, get_prop)
ObjectsToLuaCode(objs, code, get_prop)
local err
if cobjects then
local test_encoding = options.test_encoding
local collection_remap
if get_collection_index_func then
collection_remap = {}
for _, obj in ipairs(cobjects) do
collection_remap[obj] = get_collection_index_func(obj)
end
end
code:append("\n--[[COBJECTS]]--\n")
code:append("PlaceCObjects(\"")
code, err = __DumpObjPropsForSave(code, cobjects, true, center, nil, nil, collection_remap, test_encoding)
code:append("\")\n")
end
if not options.pstr then
local str = code:str()
code:free()
code = str
end
return code, err
end
function editor.Unserialize(script, no_z, forced_center)
return LuaCodeToObjs(script, {
no_z = no_z,
pos = forced_center or (terminal.desktop.inactive and GetTerrainGamepadCursor() or GetTerrainCursor())
})
end
function editor.CopyToClipboard()
if IsEditorActive() and #editor.GetSel() > 0 then
local objs = editor.GetSel()
local script = editor.Serialize(objs, empty_table)
CopyToClipboard(script)
end
end
function editor.PasteFromClipboard(no_z)
if not IsEditorActive() then
return
end
local script = GetFromClipboard(-1)
local objs = editor.Unserialize(script, no_z)
if not objs then
return
end
objs = table.ifilter(objs, function (idx, o) return not o:IsKindOf("Collection") end)
XEditorUndo:BeginOp{name = "Pasted objects"}
XEditorUndo:EndOp(objs)
editor.ClearSel()
editor.AddToSel(objs)
local objects = {}
for i = 1,#objs do
if IsFlagSet(objs[i]:GetClassFlags(), const.cfEditorCallback) then
objects[#objects + 1] = objs[i]
end
end
if #objects > 0 then
SuspendPassEditsForEditOp()
Msg("EditorCallback", "EditorCallbackPlace", objects)
ResumePassEditsForEditOp()
end
return objs
end
editor.SelectDuplicates = function()
local l = MapGet("map") or empty_table
local num = #l
print( num )
local function cmp_x(o1,o2) return o1:GetPos():x() < o2:GetPos():x() end
table.sort( l, cmp_x )
editor.ClearSel()
for i = 1,num do
local pt = l[i]:GetPos()
local axis = l[i]:GetAxis()
local angle = l[i]:GetAngle()
local class = l[i].class
local function TestDuplicate(idx)
local obj = l[idx]
if pt == obj:GetPos() and axis == obj:GetAxis() and angle == obj:GetAngle() and class == obj.class then
editor.AddToSel({obj})
return true
end
return false
end
local j = i + 1
local x = pt:x()
while j <= num and x == l[j]:GetPos():x() do
if TestDuplicate(j) then
table.remove( l, j )
num = num-1
else
j = j+1
end
end
end
end
local function SetReplacedObjectDefaultFlags(new_obj)
new_obj:SetGameFlags(const.gofPermanent)
new_obj:SetEnumFlags(const.efVisible)
local entity = new_obj:GetEntity()
local passability_mesh = HasMeshWithCollisionMask(entity, const.cmPassability)
local entity_collisions = HasAnySurfaces(entity, EntitySurfaces.Collision) or passability_mesh
local entity_apply_to_grids = HasAnySurfaces(entity, EntitySurfaces.ApplyToGrids) or passability_mesh
new_obj:SetCollision(entity_collisions)
new_obj:SetApplyToGrids(entity_apply_to_grids)
end
function editor.ReplaceObject(obj, class)
if g_Classes[class] and IsValid(obj) then
XEditorUndo:BeginOp{ objects = {obj}, name = "Replaced 1 objects" }
Msg("EditorCallback", "EditorCallbackDelete", {obj}, "replace")
local new_obj = PlaceObject(class)
new_obj:CopyProperties(obj)
DoneObject(obj)
SetReplacedObjectDefaultFlags(new_obj)
Msg("EditorCallback", "EditorCallbackPlace", {new_obj}, "replace")
XEditorUndo:EndOp({new_obj})
return new_obj
end
return obj
end
function editor.ReplaceObjects(objs, class)
if g_Classes[class] and #objs > 0 then
SuspendPassEditsForEditOp()
PauseInfiniteLoopDetection("ReplaceObjects")
XEditorUndo:BeginOp{ objects = objs, name = string.format("Replaced %d objects", #objs) }
Msg("EditorCallback", "EditorCallbackDelete", objs, "replace")
local ol = {}
for i = 1 , #objs do
local new_obj = PlaceObject(class)
new_obj:CopyProperties(objs[i])
DoneObject(objs[i])
SetReplacedObjectDefaultFlags(new_obj)
ol[#ol + 1] = new_obj
end
if ol then
editor.ClearSel()
editor.AddToSel(ol)
end
Msg("EditorCallback", "EditorCallbackPlace", ol, "replace")
XEditorUndo:EndOp(ol)
ResumeInfiniteLoopDetection("ReplaceObjects")
ResumePassEditsForEditOp()
else
print("No such class: " .. class)
end
end
function OnMsg.EditorCallback(id, objects, ...)
if id == "EditorCallbackClone" then
local old = ...
for i = 1, #old do
local object = objects[i]
if IsValid(object) and object:IsKindOf("EditorCallbackObject") then
object:EditorCallbackClone(old[i])
end
end
else
local place = id == "EditorCallbackPlace"
local clone = id == "EditorCallbackClone"
local delete = id == "EditorCallbackDelete"
for i = 1, #objects do
local object = objects[i]
if IsValid(object) then
if (place or clone) and object:IsKindOf("AutoAttachObject") and object:GetForcedLODMin() then
object:SetAutoAttachMode(object:GetAutoAttachMode())
end
if IsKindOf(object, "EditorCallbackObject") then
object[id](object, ...)
end
if place then
if IsKindOf(object, "EditorObject") then
object:EditorEnter()
end
elseif delete then
if IsKindOf(object, "EditorObject") then
object:EditorExit()
end
end
end
end
end
end
function editor.GetSingleSelectedCollection(objs)
local collections, remaining = editor.ExtractCollections(objs or editor.GetSel())
local first = collections and next(collections)
return #remaining == 0 and first and not next(collections, first) and Collections[first]
end
function editor.ExtractCollections(objs)
local collections
local remaining = {}
local locked_idx = editor.GetLockedCollectionIdx()
for _, obj in ipairs(objs or empty_table) do
local coll_idx = 0
if obj:IsKindOf("Collection") then
coll_idx = obj.Index
else
coll_idx = obj:GetCollectionIndex()
if locked_idx ~= 0 then
local relation = obj:GetCollectionRelation(locked_idx)
if relation == "child" then -- add just this object
coll_idx = 0
elseif relation == "sub" then -- add the whole collection (use GetCollectionRoot)
coll_idx = Collection.GetRoot(coll_idx)
end
else
if coll_idx ~= 0 then -- try to find root
coll_idx = Collection.GetRoot(coll_idx)
end
end
end
if coll_idx == 0 then -- add just this object
remaining[#remaining + 1] = obj
else -- add the whole collection
collections = collections or {}
collections[coll_idx] = true
end
end
return collections, remaining
end
function editor.SelectionPropagate(objs)
local collections, selection = nil, objs or {}
if XEditorSelectSingleObjects == 0 then
collections, selection = editor.ExtractCollections(objs)
end
for coll_idx, _ in sorted_pairs(collections or empty_table) do
table.iappend(selection, MapGet("map", "collection", coll_idx, true))
end
if const.SlabSizeX then
if terminal.IsKeyPressed(const.vkControl) then
local visited = {}
for _, obj in ipairs(objs) do
if IsKindOf(obj, "StairSlab") and not visited[obj] then
local gx, gy, gz = obj:GetGridCoords()
table.iappend(selection, EnumConnectedStairSlabs(gx, gy, gz, 0, visited))
end
end
end
end
return selection
end
MapVar("EditorCursorObjs", {}, weak_keys_meta)
PersistableGlobals.EditorCursorObjs = nil
function editor.GetPlacementPoint(pt)
local eye = camera.GetEye()
local target = pt:SetTerrainZ()
local objs = IntersectSegmentWithObjects(eye, target, const.efBuilding | const.efVisible)
local pos, dist
if objs then
for _, obj in ipairs(objs) do
if not EditorCursorObjs[obj] and obj:GetGameFlags(const.gofSolidShadow) == 0 then
local hit = obj:IntersectSegment(eye, target)
if hit then
local d = eye:Dist(hit)
if not dist or d < dist then
pos, dist = hit, d
end
end
end
end
end
return pos or pt:SetInvalidZ()
end
function editor.CycleDetailClass()
local sel = editor:GetSel()
if #sel < 1 then
return
end
XEditorUndo:BeginOp{ objects = sel, name = "Toggle Detail Class" }
local seldc = {}
for _, obj in ipairs(sel) do
local dc = obj:GetDetailClass()
seldc[dc] = seldc[dc] or 0
seldc[dc] = seldc[dc] + 1
end
local next_dc = "Eye Candy"
if seldc["Eye Candy"] then
next_dc = "Optional"
elseif seldc["Optional"] then
next_dc = "Essential"
end
for _, obj in ipairs(sel) do
obj:SetDetailClass(next_dc)
end
XEditorUndo:EndOp(sel)
end
function editor.ForceEyeCandy()
local sel = editor:GetSel()
if #sel < 1 then
return
end
XEditorUndo:BeginOp{ objects = sel, name = "Force Eye Candy" }
SuspendPassEditsForEditOp(sel)
for _, obj in ipairs(sel) do
obj:ClearEnumFlags(const.efCollision + const.efApplyToGrids)
obj:SetDetailClass("Eye Candy")
end
ResumePassEditsForEditOp(sel)
XEditorUndo:EndOp(sel)
end
----- Modding editor
--
-- The Mod Editor starts the map editor in this mode when a user is editing a map.
--
-- The Mod Item that contains the map (or map patch) is stored in editor.ModItem;
-- for ModItemMapPatch Ctrl-S generates a patch via XEditorCreateMapPatch.
-- This mode also disables some shortcuts, e.g. closing the editor.
if FirstLoad then
editor.ModdingEditor = false
end
function editor.IsModdingEditor()
return editor.ModdingEditor
end
function editor.AskSavingChanges()
if editor.ModdingEditor and editor.ModItem and EditorMapDirty then
if GedAskEverywhere("Warning", "There are unsaved changes on the map.\n\nSave before proceeding?", "Yes", "No") == "ok" then
editor.ModItem:SaveMap()
end
SetEditorMapDirty(false)
end
end
-- When changing the map via F5, ask the user for saving changes;
-- (in case the map is changed via editor.StartModdingEditor, we call editor.AskSavingChanges before updating editor.ModItem)
function OnMsg.ChangingMap(map, mapdata, handler_fns)
table.insert(handler_fns, editor.AskSavingChanges)
end
function editor.StartModdingEditor(mod_item, map)
if ChangingMap then return end
editor.AskSavingChanges()
editor.ModdingEditor = true
editor.ModItem = mod_item
editor.ModItemMap = map
ReloadShortcuts()
UpdateModEditorsPropPanels() -- update buttons in Mod Item properties, e.g. "Edit Map"
if editor.PreviousModItem ~= mod_item or CurrentMap ~= map then
editor.PreviousModItem = mod_item
ChangeMap(map)
end
if not IsEditorActive() then
EditorActivate()
end
end
function editor.StopModdingEditor(return_to_mod_map)
if ChangingMap or not editor.ModdingEditor then return end
CreateRealTimeThread(function()
-- change map first, so the OnMsg.ChangingMap asks for saving changes
if return_to_mod_map and CurrentMap ~= ModEditorMapName then
editor.PreviousModItem = false
ChangeMap(ModEditorMapName)
end
editor.ModdingEditor = false
editor.ModItem = nil
editor.ModItemMap = nil
ReloadShortcuts()
UpdateModEditorsPropPanels() -- update buttons in Mod Item properties, e.g. "Edit Map"
if IsEditorActive() then
EditorDeactivate()
end
end)
end
function OnMsg.GedClosing(ged_id)
local conn = GedConnections[ged_id]
if conn and conn.app_template == "ModEditor" then
if editor.ModdingEditor and editor.ModItem and conn.bound_objects.root[1] == editor.ModItem.mod then
editor.StopModdingEditor("return to mod map") -- close map editor if the mod editor for its edited map is closed
end
end
end
-- the user may change the map to another one; if it is a mod-created map, find its mod item
function OnMsg.ChangeMap(map)
if editor.ModdingEditor and editor.ModItemMap ~= map then
editor.ModItemMap = nil
editor.ModItem = nil
for _, mod in ipairs(ModsLoaded) do
mod:ForEachModItem(function(mod_item)
if mod_item:GetMapName() == map then
editor.ModItem = mod_item
return "break"
end
end)
end
UpdateModEditorsPropPanels() -- update buttons in Mod Item properties, e.g. "Edit Map"
end
end