Files
2025-09-29 00:52:08 +02:00

4702 lines
136 KiB
Plaintext
Executable File

-----------------------------------------------------------------------------
-- Combined-LOD Generator Tool (ComboLodder)
-----------------------------------------------------------------------------
-- Data-structures (loaded by lodCombinerTool_UI.ms)
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Uses
-----------------------------------------------------------------------------
-- Print verbose debug-data to listener:
global RsComboLodDebug = False
global gRsLodCombinerVals
RSTA_LoadCommonFunction #("FN_Geometry.ms")
filein (RsConfigGetWildWestDir() + "script/3dsMax/_Common_Functions/FN_Objects.ms")
filein (RsConfigGetWildwestDir() + "Script/3dsMax/Maps/MemResidentZones.ms")
filein (RsConfigGetWildwestDir() + "Script/3dsMax/Maps/lodCombinerTool_funcs.ms")
-- Struct used to store various values used by this script:
struct RsLodCombinerVals
(
-- Changing the integer part of VersionNum will invalidate existing LOD-objects, and will warn people if they're opening data from newer version of tool.
-- Changing the floating-point bit has no effect, so can be used to track non-data tool-changes.
versionNum = 11.24,
versionName = "Magma Carp",
dataVersion, -- 'VersionNum' as integer
dummySlodVersion = 2, -- Used to update dummySlod meshes
versionString,
lodSuffixes = #("LOD", "SLOD", "SLOD2"),
dummySlodSuffix = "dSLOD",
lodDistUnits = #(400, 750, 1500), -- Values to be used for LOD-object distances
defBlockRadius = #(-1, 50, 100), -- Default block-radius for sub-groups
maxIplCount = 1000, -- We don't allow IPL Groups to have more objects than this in the scene
billboardMatVersion = 4 + (100 * RsComboLod_billboardMatVersion), -- Billboard-objects are invalidated if this changes
natAOMult = 254, -- "AO Multiplier" value used for billboard-lods
artAOMult = 0, -- "Artificial AO Multiplier" value used for billboard-lods
textureScale = 0.25, -- ComboLod materials are given this texture-scale in Max
meshTintVersion = 1, -- Meshtint-generation method's version
propLodTexDir = (RsConfigGetTextureSourceDir core:True + "Props/Prop_Lods/lod_textures/"),
lodTintPattern = "*prop_lod_TINTED*", -- Filename-pattern used to indicate that faces are to be tinted
lodColourFile = (propLodTexDir + "prop_lod.bmp"), -- Filename to apply prop-lod colour-texture:
defaultTint = (color 199 194 188), -- Default untinted colour for meshtinted objects
defaultMat, -- Default material for replacing undefined/temp mats. Uses lodColourFile as texture.
-- Path for storing Memory Resident model-lists:
residentPath = (RsMakeSafePath (RsConfigGetCommonDir() + "/data/levels/" + RsConfigGetProjectName() + "/resident/")),
-- Memory Resident model-lists generated elsewhere, that will be read by this tool:
residentGridPath = (residentPath + "grid/"),
blockTypes = #(#box, #hex, #tri),
blockTypeNames = #("Square", "Hexagon", "Triangle"),
maxLodDist = 350, -- Max LOD-distance to be set on first-level LOD-objects
maxLodCount = 255, -- Max number of lod-children per lod-object
maxLodWidth = 400, -- Max width/length of lod-block
maxLodRadius, -- Set to (maxLodWidth / 2)
minFaceSize = 3.0, -- Don't use objects whose biggest billboard-face is smaller than this
tiltAltAngle = 5.0, -- Use Tiltable Alternative mesh for a prop if it is tilted more than this
minBoxSizes = #(1.5, 2.4, 3), -- Minimum bounding-box edge-lengths for each LOD-level. Don't include an instance that is smaller than this.
-- Memory Resident cut-off values: --
resGridCountRatio = 0.5, -- Make a model Resident if its props are used in this fraction of all unique 50x50m grids used by loddable props.
resPropCountRatio = 0.05, -- Make a model Resident if its loddable props make up this fraction of the map's loddable props.
-----------------------------------------------
resTinyBlockCount = 5, -- Make a model Resident if it is used in more than this many Tiny Blocks (set to -1 to deactivate Tiny-Block checking)
resTinyBlockSize = 2, -- A lod-block is considered to be a Tiny Block if it lods this many props, or fewer:
-----------------------------------------------
resMaxDistRange = 60.0, -- Don't allow Resident lod-blocks with props with lod-distance range greater than this.
-- Use these values instead to deactivate Memory Resident bits:
--resGridCountRatio = -1,
--resPropCountRatio = -1,
--resTinyBlockCount = -1,
-- List of properties to be copied when cloning a RsLodCombinerGroup
grpClonePropNames = Undefined,
-----------------------------------------------
textOutlineWidth = 2, -- Width of text-outline pen
nodeItemHeight = 32, -- Height of treeview nodes
viewportSelClr, windowColour,
windowBrush, blackBrush, OuterPen, ThinPen, DottedPen,
selectBrushes, defaultBrushes, disabledBrushes,
redBrushes, greenBrushes, yellowBrushes,
greyColour = (color 135 135 175),
fn colourToDN clr =
(
clr = clr as point3
(dotNetClass "System.Drawing.Color").FromArgb clr[1] clr[2] clr[3]
),
fn makeBrush clr =
(
dotNetObject "System.Drawing.SolidBrush" (colourToDN clr)
),
fn makeBrushPair clr =
(
dataPair normal:(makeBrush clr) faded:(makeBrush (clr * 0.7))
),
fn getDefaultMat =
(
-- Set up defaultMat:
if (defaultMat == undefined) do
(
defaultMat = Rage_Shader name:"DefaultLOD"
RstSetShaderName defaultMat "default"
local subTexMap = bitmapTexture()
setSubTexmap defaultMat 1 subTexMap
subTexMap.filename = lodColourFile
)
local copyMat = copy defaultMat
copyMat.name = defaultMat.name
return copyMat
),
fn init =
(
dataVersion = (VersionNum as integer)
versionString = (" [v" + (VersionNum as string) + ":" + versionName + "]")
-- Ensure that residentPath exists:
RsMakeSurePathExists residentPath
maxLodRadius = maxLodWidth / 2
-- Define text-outline pen:
OuterPen = dotNetObject "System.Drawing.Pen" (dotNetClass "System.Drawing.Color").Black
--OuterPen.Alignment = OuterPen.Alignment.Outset
OuterPen.Width = textOutlineWidth
ThinPen = dotNetObject "System.Drawing.Pen" (dotNetClass "System.Drawing.Color").Black
ThinPen.Width = 1
DottedPen = dotNetObject "System.Drawing.Pen" (dotNetClass "System.Drawing.Color").Black
DottedPen.DashStyle = DottedPen.DashStyle.Dash
-- Default window-back colour:
local windowClr = (colorMan.getColor #window) * 255
windowBrush = makeBrush windowClr
windowColour = colourToDN windowClr
blackBrush = dotNetObject "System.Drawing.SolidBrush" (dotNetClass "System.Drawing.Color").Black
-- Default window-text colour:
--local textClr = (colorMan.getColor #windowText) * 255
-- White text by default (it'll be outlined, so should always be visible)
local defaultTextClr = white
-- Selection-box brushes:
local selColour = (dotNetClass "System.Drawing.Color").fromKnownColor (dotNetClass "system.Drawing.KnownColor").MenuHighlight
local selClr = (color selColour.r selColour.g selColour.b)
local halfSelClr = (windowClr + windowClr + selClr) / 3
-- Make viewport select-colour lighter:
viewportSelClr = selClr + ((white - selClr) * 0.65)
selectBrushes = dataPair normal:(makeBrush selClr) faded:(makeBrush halfSelClr)
defaultBrushes = makeBrushPair white
greenBrushes = makeBrushPair green
redBrushes = makeBrushPair red
yellowBrushes = makeBrushPair yellow
disabledBrushes = makeBrushPair greyColour
)
)
gRsLodCombinerVals = RsLodCombinerVals()
gRsLodCombinerVals.init()
-- Clear existing RsRef combolod-related data-structs, ready for reload:
-- (deactivated, used during script-development)
if FALSE do
(
RsRefData.refs.comboLod = undefined
for ref in RsRefData.refs do
(
-- Force slod-ref data to be reloaded:
for slodLevelList in ref.slodRefs do
(
slodLevelList.comboLod = undefined
slodLevelList.meshObj = undefined
slodLevelList.material = undefined
slodLevelList.tintMeshObjs = undefined
slodLevelList.meshTime = 0
)
)
)
-- Struct for storing '.ComboLod' data per RsRef ref-definition:
struct RsLodCombinerRefData
(
maxFaceSize = 0, -- Mesh's maximum billboard-size
maxBoxSize = 0, -- Mesh's maximum bounding-box length
dataHash = 0, -- Stores hash of current data
hasBBs = False, -- True if ref's mesh uses billboards
hasTiltAlt = False, -- True if ref has any Tiltable Alternative mesh
maxTiltAltLevel = 0, -- Max lod-level to use for prop if hasTiltAlt is true
paletteHash = 0, -- Changes when palette-file changes
lodDistance,
tintingDone = False,
CanComboLod = True, -- Set to 'False' if prop is invalid for some reason.
HasAnim = False, -- 'True' if prop is animated (unless it has 'Can ComboLod Animation' flag set)
HasMissingModel = False, -- 'True' if prop has model(s) defined in xmls but object is missing from maxfile
isTree = False -- True if prop's slodrefs include billboards
)
-- Structure used to store tree/node data on tree-nodes:
-- Also includes functions shared between node-types
struct RsLodCombinerNodeTag
(
Private
_data, -- Type-specific data-structure
Public
type = #none,
tagIdx, -- Item's dataTag index, as collected in RsLodCombinerTool.dataTags
childTagIdxs = #{},
selected = False, -- Item is selected in tree
viewShow = True, -- Item should be drawn in viewport
isHidden = False, -- Item's sub-objects are all hidden
nameText = "", smallText,
statusHash = 0, -- Changes whenever an object's data changes
-- Default text-values:
textStyle = 0,
textSize = 13,
statusBrush, subStatusBrush, textBrush, treeNode,
viewportColour, viewportSelColour,
-- Accessor-function for private value:
fn GetData = (return _data),
fn SetData newData = (_data = newData),
fn UpdateStatus updateObjVals:True refreshObjVals:False \
showLodDists:True showLodCounts:True showPropCounts:True showChildCounts:True show1to1Counts:True show2to1Counts:True =
(
-- This may be changed by later steps:
textBrush = gRsLodCombinerVals.defaultBrushes.faded
local textStream = stringStream ""
smallText = ""
local data = this.GetData()
if (data != undefined) do
(
--format "[xxx:%] " data.genLodLevel to:textStream
case type of
(
#container:
(
-- Unpack from paramblock, if necessary
if (data.GetGroupList() == undefined) do
data.Restore()
-- Recursively update values for map's comboLod hierarchy
if updateObjVals do
data.UpdateVals refreshObjVals:refreshObjVals
)
#group:
(
format "[BlockType: %] " (toUpper data.childBlockType) to:textStream
local isLodBlock = (data.genLodLevel == 2)
)
#block:
(
-- Show prop-name for LOD-blocks, if blockByProp is set:
if data.blockByProp and (data.blockByPropName != undefined) do
(
format "[%] " (data.blockByPropName as string) to:textStream
)
if data.isResidentBlock do
(
format "[MEM-RESIDENT (lod-dist idx: %)] " data.lodDistIdx to:textStream
)
)
)
if RsComboLodDebug do
(
if (isProperty data #GUID) do
(
format "[%] " data.guid to:textStream
)
if (isProperty data #treeHash) do
(
format "[HASH: %] " data.treeHash to:textStream
)
)
if (type != #object) do
(
local GrpCount = (Data.GetGroupList()).Count
if showChildCounts and (GrpCount != 0) do
(
format "[Child-Nodes: %] " GrpCount to:textStream
)
if showPropCounts do
(
format "[Props: %] " data.objCount to:textStream
)
if showLodCounts do
(
format "[LODs: %] " data.lodObjCount to:textStream
)
if show1to1Counts do
(
local thisCount = (data.GetPropsPerLodCounts())[1]
if (thisCount == undefined) do (thisCount = 0)
format "[1:1 LODs:%] " thisCount to:textStream
)
if show2to1Counts do
(
local thisCount = (data.GetPropsPerLodCounts())[2]
if (thisCount == undefined) do (thisCount = 0)
format "[2:1 LODs:%] " thisCount to:textStream
)
)
-- Only add newline if some text has already been added:
if (filePos textStream) != 0 do
(
format "\n" to:textStream
)
smallText += (textStream as string)
)
case type of
(
#container:
(
--textStyle = 2 -- italics
textSize = 13
)
#group:
(
-- Show red text if IPL Group has too many props
if (data.iplObjCount != 0) and (data.iplObjCount > gRsLodCombinerVals.maxIplCount) then
textBrush = gRsLodCombinerVals.redBrushes.normal
else
textBrush = gRsLodCombinerVals.defaultBrushes.normal
textStyle = 0 -- normal
textSize = 13
nameText = data.showName()
)
#block:
(
--textStyle = 2 -- italics
textSize = 13
nameText = data.showName()
)
#object:
(
textStyle = 1 -- Bold
textSize = 10
nameText = data.node.name
if (not data.hasValidRef) then
(
-- Show undefined refs in red:
textBrush = gRsLodCombinerVals.redBrushes.normal
format "[undefined ref!] " to:textStream
)
else
(
if data.isResidentProp do
(
data.getResidentRefNums()
local reason = (data.GetResidentReasons())[data.refNum]
format "[MEM-RESIDENT: %] " reason to:textStream
)
if (data.dontExport) do
format "[\"Dont Export\" set] " to:textStream
if (data.dontAddIPL) do
format "[\"Don't Add To IPL\" set] " to:textStream
if (data.ignoreMe) do
format "[\"Ignored By ComboLodder\" set] " to:textStream
if (data.doUnTilt) do
format "['Use Full Matrix' NOT SET - LOD will be untilted] " to:textStream
if (data.IPLGroup != "undefined") do
format "[IPL:%] " data.IPLGroup to:textStream
if (data.lowPriority) do
format "[Low Priority] " to:textStream
if (data.nonZeroPriority) do
format "[Non-zero Priority] " to:textStream
if (data.useTiltAlt) do
format "[Using Tiltable mesh] " to:textStream
if (data.hasSmallFaces.numberSet != 0) do
format "[Small billboard, SLODs 50\% of time] " to:textStream
case of
(
-- Reasons for not lodding prop:
(data.hasMissingModel):
(
format "[Missing meshes: xml/maxfile mismatch?] " to:textStream
)
(data.tooSmallToShow):
(
local smallBBoxLevel = data.smallBBoxLevel
format "[Bounds too small for % (<%m)] " gRsLodCombinerVals.lodSuffixes[smallBBoxLevel] gRsLodCombinerVals.minBoxSizes[smallBBoxLevel] to:textStream
)
(data.hasAnim):
(
format "[has animation] " to:textStream
)
(data.sourceLodLevels == 0):
(
format "[no LOD-source] " to:textStream
)
-- Show lod-suffixes available for this prop:
Default:
(
local lodSuffixes = gRsLodCombinerVals.lodSuffixes
for n = 1 to data.sourceLodLevels do
(
format "[%] " lodSuffixes[n] to:textStream
)
)
)
if (data.canLod) do
(
textBrush = gRsLodCombinerVals.defaultBrushes.normal
)
)
)
default:()
)
if (type != #object) do
(
if (data == undefined) or (data.genLodLevel == 0) then
(
statusBrush = gRsLodCombinerVals.disabledBrushes.normal
if (type == #container) then
(
case of
(
((data == undefined) or (data.lodsSynced == Undefined)):
(
format "[Contains no ComboLOD groups] " to:textStream
viewportColour = gRsLodCombinerVals.greyColour
)
(data.lodsSynced == True):
(
format "[LOD-objects are up-to-date] " to:textStream
viewportColour = green
)
Default:
(
format "[Contains out-of-date LOD-objects] " to:textStream
viewportColour = red
)
)
)
)
else
(
if (data.dummySlod) do
(
format "[Dummy-SLOD] " to:textStream
)
local iplName = data.blockIPLName
if (iplName != undefined) and (iplName != "undefined") do
(
if (iplName == #Multiple) do
iplName = "MULTIPLE!"
local iplTooBig = (data.iplObjCount > gRsLodCombinerVals.maxIplCount)
local iplWarningStr = if iplTooBig then " !!! " else ""
Format "[%IPL:%" iplWarningStr iplName to:textStream
-- Print the number of objects in this IPL if it's too big
if iplTooBig do
Format " - % objs in IPL! (>%)" data.iplObjCount gRsLodCombinerVals.maxIplCount to:textStream
Format "] " iplWarningStr to:textStream
)
case of
(
(data.lodSynced == Undefined):
(
statusBrush = gRsLodCombinerVals.disabledBrushes.normal
viewportColour = gRsLodCombinerVals.greyColour
-- Don't bother showing this for the SLOD2 level
if (data.genLodLevel != 3) do
Format "[No ComboLOD props for lod-level: %] " data.genLodLevel to:textStream
)
(data.lodSynced == True):
(
statusBrush = gRsLodCombinerVals.greenBrushes.normal
format "[LOD-objects are up-to-date] " to:textStream
viewportColour = green
)
(data.hasLod):
(
statusBrush = gRsLodCombinerVals.yellowBrushes.normal
format "[LOD-objects out-of-date] " to:textStream
viewportColour = yellow
)
Default:
(
statusBrush = gRsLodCombinerVals.redBrushes.normal
format "[LOD-object not generated] " to:textStream
viewportColour = red
)
)
)
-- Set sub-node status-circle colour:
if (data != undefined) and (type != #block) do
(
subStatusBrush = case of
(
(data.lodsSynced == Undefined):
(
gRsLodCombinerVals.disabledBrushes.normal
)
(data.lodsSynced == True):
(
gRsLodCombinerVals.greenBrushes.normal
)
Default:
(
gRsLodCombinerVals.redBrushes.normal
)
)
)
if showLodDists and (data != undefined) and (data.genLodLevel != 0) do
(
local lodObjs = data.getLodObjs recurse:false
if (lodObjs.count != 0) do
(
local lodDists = for obj in lodObjs collect (getattr obj idxLodDistance)
local childLodDists = for obj in lodObjs collect (getattr obj idxChildLodDistance)
format "[LOD dist%: " (if lodDists.count == 1 then "" else "s") to:textStream
for n = 1 to lodDists.count do
(
format "%m" lodDists[n] to:textStream
if (n != lodDists.count) do
(
format ", " to:textStream
)
)
format "] " to:textStream
format "[Child LOD dist%: " (if childLodDists.count == 1 then "" else "s") to:textStream
for n = 1 to lodDists.count do
(
format "%m" childLodDists[n] to:textStream
if (n != childLodDists.count) do
(
format ", " to:textStream
)
)
format "] " to:textStream
)
)
format "\n" to:textStream
)
if (viewportColour != undefined) do
(
viewportSelColour = viewportColour + (0.4 * (white - viewportColour))
)
smallText = (textStream as string)
if (type == #object) do
(
statusHash = getHashValue smallText 1
)
)
)
-- Structure used to define data for LodCombiner ingredient-props and combined-lod objects:
struct RsLodCombinerObject
(
Private
_parentData, -- Object's parent block/group struct
_residentReasons,
Public
tagIdx, -- Object's dataTag index, as collected in RsLodCombinerTool.dataTags
gridParentIdxList, -- Idx-list of grid-parent nodes, used for reblocking objects
node, lodDistance,
sourceHash = 0,
treeHash = 0,
refHash = 0,
refNum = 0,
lodLevel = 0, -- Used to denote lod-level of combined-LOD meshes.
sourceLodLevels = 0, -- Used to denote number of source-lod meshes a prop has.
lodLevelsUsed,
lodLevelsCount, -- Numbers of props available for each lod-level: #([none],[lod],[slod],[slod2])
genLodLevel = 0, -- Objects don't generate any lod-objects on their own
hasValidRef = false, -- True if object has a valid refDef
hasSmallFaces = #{}, -- Set if any of object's slod-meshes are too small to always be shown
smallBBoxLevel = 0, -- Set if an object's bounding-box is too small for it to be shown for particular lod-levels
tooSmallToShow = false, -- True if prop is too small to add to Lod, but is otherwise valid
isTree = false, -- True if prop has billboards
isResidentProp = false, -- True if prop's model has been marked for being made Memory Resident
residentRefNums,
residentDistLimits,
lodDistIdx = 0,
hasAnim = false,
hasMissingModel = false,
lowPriority = false, -- True if object has "Low Priority" flag
nonZeroPriority = false, -- True if object's Priority isn't 0
canLod = false, -- True if object is suitable LOD-source object
useTiltAlt = False, -- True if item's node has been rotated more than gRsLodCombinerVals.tiltAltAngle
doUnTilt = False, -- True if item's node is slightly tilted, but prop will be shown as untilted ingame (due to not having Full Matrix Rotation)
propLodDist,
ignoreMe = false, -- True if "Ignored By ComboLodder" is set
dontExport = false, -- True if "Dont Export" is set
dontAddIPL = false, -- True if "Dont Add To IPL" is set
iplGroup = "undefined", -- Set to node's "IPL Group"
AOMult, artAOMult,
attrFlagged,
genWaterReflect = False,
aoMultEnable = False,
tintIdx = 0, -- Object's tintIdx attribute value, if ref has palettes
expanded = false, -- Stores state of treeview node
-- Accessor-functions for private values:
fn GetParentData = (return _parentData),
fn SetParentData newParentData =
(
_parentData = newParentData
),
fn GetResidentReasons = (return _residentReasons),
-- Get this object's top-level parent:
fn GetRootData =
(
return (_parentData.GetRootData())
),
-- Returns object (as list for cross-compatibility with the other hierarchy-data structs)
fn getObjs info:False =
(
if info then #(This) else #(node)
),
-- Get this object's lodDistance value:
fn getLodDistance =
(
-- Default value for invalid stuff:
local retVal = 40
if (isValidNode node) and (node.refDef != undefined) do
(
local refDef = node.refDef
if (refDef.comboLod == undefined) or (refDef.comboLod.lodDistance == undefined) do
(
refDef.comboLod = undefined
gRsComboLodFuncs.loadSlodData #(refDef)
)
retVal = refDef.comboLod.lodDistance
retVal *= node.scale.z
)
return retVal
),
-- Lod-mesh will have this transform - this will be untilted from original if "unTilt" flag has been set
fn lodXform =
(
local retVal = node.transform
-- Return untilted transform for non-Full Matrix props:
if doUnTilt do
(
local objRot = retVal.rotationPart
local untiltedRot = (quat 0 0 objRot.Z objRot.W)
retVal = preRotate retVal (inverse objRot)
retVal = preRotate retVal (untiltedRot)
)
return retVal
),
-- Generates number used to find out when a LOD-source prop has changed:
fn getSourceHash getFresh:true =
(
if not getFresh do return sourceHash
if (not canLod) or (not hasValidRef) or (tooSmallToShow) or (not isValidNode node) do return 0
local refLodData = node.refDef.comboLod
local vals = stringStream ""
format "%" refLodData.dataHash to:vals
if (iplGroup != undefined) and (iplGroup != "undefined") do
(
format "#IPL:%" iplGroup to:vals
)
format "#%#%#%#%#%#%" propLodDist AOMult artAOMult (lodXform()) useTiltAlt tintIdx to:vals
--if genWaterReflect do (format "#WR" to:vals) -- Doesn't require regenerate to correct this
if aoMultEnable do (format "#AOM" to:vals)
return (getHashValue (vals as string) 1)
),
-- Object LodLevelCounts are combined for the parent-struct's values:
-- (used to properly update progress-bar during lod-generation)
fn SetLodLevelsCount =
(
this.lodLevelsUsed = #{}
this.lodLevelsCount = for n = 1 to 4 collect 0
if (this.sourceLodLevels == 0) then
(
-- Index 1 is for props that are missing lods
this.lodLevelsCount[1] = 1
)
else
(
for n = 1 to this.sourceLodLevels do
(
this.lodLevelsCount[n + 1] = 1
this.lodLevelsUsed[n] = True
)
)
return OK
),
-- Returns list of map's Memory Resident object-refs indexes, referenced from map-node's bitarray:
fn getResidentRefNums =
(
-- Set up references:
if (residentRefNums == undefined) do
(
residentRefNums = _parentData.GetResidentRefNums()
_residentReasons = _parentData.GetResidentReasons()
residentDistLimits = _parentData.residentDistLimits
)
return residentRefNums
),
-- Update values to match this object's current state:
fn UpdateVals =
(
sourceHash = 0
sourceLodLevels = 0
canLod = False
hasValidRef = false
lowPriority = false
nonZeroPriority = false
dontExport = false
dontAddIPL = false
ignoreMe = false
hasSmallFaces = #{}
smallBBoxLevel = 0
tooSmallToShow = false
hasAnim = false
hasMissingModel = false
useTiltAlt = False
doUnTilt = False
tintIdx = 0
refNum = 0
attrFlagged = False
isTree = False
AOMult = gRsLodCombinerVals.natAOMult
artAOMult = gRsLodCombinerVals.artAOMult
if (isProperty node #refDef) do
(
lowPriority = (getAttr node idxLowPriority)
nonZeroPriority = ((getAttr node idxPriority) != 0)
dontExport = (getAttr node idxDontExport)
dontAddIPL = (getAttr node idxDontAddToIpl)
ignoreMe = (idxIgnoredByComboLodder != undefined) and (getAttr node idxIgnoredByComboLodder)
canLod = not (ignoreMe or dontExport or dontAddIPL or lowPriority or nonZeroPriority)
propLodDist = getAttr node idxLodDistance
iplGroup = getAttr node idxIPLGroup
genWaterReflect = ((idxGenerateWaterReflectiveLODs != undefined) and (getAttr node idxGenerateWaterReflectiveLODs))
--aoMultEnable = getAttr node idxEnableAmbientMultiplier -- Not currently used; we always set True for trees, False otherwise:
attrFlagged = (genWaterReflect or aoMultEnable)
local refDef = node.refDef
hasValidRef = (refDef != undefined)
if hasValidRef do
(
refNum = refDef.num
isResidentProp = (getResidentRefNums())[refNum]
local refData = refDef.comboLod
-- Don't use objects with 'CanComboLod' set to 'False':
CanLod = CanLod and RefData.CanComboLod
-- This prop's model has billboards:
isTree = refData.isTree
-- Collect flags explaining reasons why prop isn't lodding:
HasMissingModel = refData.HasMissingModel
HasAnim = refData.HasAnim
if (CanLod) do
(
-- Lod-meshes will be automatically untilted on generation, if their props aren't going to be tilted in-game:
local hasFullMatrix = ((refDef.isDynamic) or (getAttr node idxUseFullMatrix) or (RsHasAutoFullMatrix node))
-- Can/should this prop use a Tiltable Alternative mesh?
if (hasFullMatrix) then
(
if (refData.hasTiltAlt) do
(
local tiltAngle = RsGetObjTilt node
useTiltAlt = (tiltAngle > gRsLodCombinerVals.tiltAltAngle)
)
)
else
(
-- Set non-full-matrix props to be untilted, if they are tilted:
local objRot = node.rotation
doUnTilt = ((objRot.X != 0) or (objRot.Y != 0))
)
-- If this object has non-default tinting, get its tint-index attribute:
if (refData.paletteHash != 0) and (refDef.paletteCount > 1) do
(
tintIdx = getAttr node idxTint
)
local slodRefs = node.refDef.slodRefs
hasSmallFaces.count = slodRefs.count
local maxLodLevel = if useTiltAlt then refData.maxTiltAltLevel else slodRefs.count
-- Stop counting lod-meshes if any slodRefs have invalid meshObj (failed to load mesh from Maxfile)
local validMeshes = True
local minBoxSizes = gRsLodCombinerVals.minBoxSizes
local minFaceSize = gRsLodCombinerVals.minFaceSize
local nodeScale = node.scale.Z
for lodLevel = 1 to maxLodLevel while validMeshes do
(
-- Don't bother checking tiltable-alternative slodRef for size...
local slodRef = slodRefs[lodLevel][1]
validMeshes = (slodRef.meshObj != undefined)
if validMeshes then
(
local slodRefData = slodRef.comboLod
if ((refData.maxBoxSize * nodeScale) < minBoxSizes[lodLevel]) then
(
-- Don't add for this lod-level if prop's bounding-box is too small
smallBBoxLevel = lodLevel
validMeshes = False
if (lodLevel == 1) do (tooSmallToShow = True)
)
else
(
sourceLodLevels += 1
-- Don't always use slodrefs if their billboards scale down too small (ignore if they don't have billboards)
if (lodLevel > 1) and (slodRefData.maxFaceSize != 0) and ((slodRefData.maxFaceSize * nodeScale) < minFaceSize) do
(
hasSmallFaces[lodLevel] = True
)
)
)
else
(
-- We shouldn't find ourselves here, meshes -should- have already been checked during 'fn loadSlodData':
format "MISSING LOD-SOURCE MODEL: %\n" slodRef.name
hasMissingModel = True
canLod = False
)
)
)
)
canLod = canLod and (sourceLodLevels != 0)
sourceHash = getSourceHash getFresh:true
refHash = if (sourceHash == 0) then 0 else refDef.comboLod.dataHash
)
treeHash = sourceHash
-- Set object-counts, used to properly update progress-bar during lod-generation:
this.SetLodLevelsCount()
),
-- Create struct-instance based on obj:
fn create obj parentData:undefined =
(
local objInfo = RsLodCombinerObject node:obj
ObjInfo.SetParentData ParentData
objInfo.UpdateVals()
return objInfo
),
-- Store object to xml, if node is valid:
fn store xmlWriter objsList:#() =
(
if (isValidNode node) do
(
xmlWriter.WriteStartElement "object"
-- Add node to object-list, and store its index:
append objsList node
xmlWriter.WriteAttributeString "objNum" (objsList.count as string)
-- Write attributes:
for item in
#(
dataPair name:"sourceHash" val:sourceHash,
dataPair name:"lodLevel" val:lodLevel
)
where (item.val != 0) do
(
xmlWriter.WriteAttributeString item.name (item.val as string)
)
xmlWriter.WriteEndElement()
)
return OK
),
-- Reconstitute object-struct from stored xml element:
fn Restore xmlReader objsList:#() parentData:Undefined readVersion:0 =
(
local objNum = xmlReader.Item["objNum"]
if (objNum == undefined) do return dontCollect
-- Retrieve indexed object from list:
local getObj = objsList[objNum as integer]
-- Cancel object-import if object wasn't found...
if (not isValidNode getObj) do return dontCollect
-- If obj was found, generate new struct:
local restoredObj = RsLodCombinerObject.create getObj parentData:parentData
-- LodLevel is set to lod-level a LOD-object was generated for.
-- Set to 0 for props (default)
-- Set to -1 for containerLOD proxies
local lodLevel = xmlReader.Item["lodLevel"]
if (lodLevel != undefined) do
(
restoredObj.lodLevel = lodLevel as integer
)
lodLevel = restoredObj.lodLevel
-- Abort if object-class doesn't match lodLevel value:
case of
(
(lodLevel == -1):
(
-- ContainerLods:
if not (RsIsContainerLod getObj) do return dontCollect
)
(lodLevel == 0):
(
-- Props:
if not (isRsRef getObj) do return dontCollect
)
(lodLevel > 0):
(
if not (isEditPolyMesh getObj) do return dontCollect
-- Only bother retrieving sourceHash for generated combined-mesh objects:
local sourceHash = xmlReader.Item["sourceHash"]
if (sourceHash != undefined) do
(
restoredObj.sourceHash = sourceHash as integer
)
)
)
return restoredObj
),
-- Used by viewport-draw callback to draw object's overlays:
fn viewportDraw colour:white showBoxes:True showShapes:True =
(
--gw.wMarker (gw.wtransPoint node.pos) #hollowBox color:colour
)
)
-- Structure used to define LodCombiner groups:
struct RsLodCombinerGroup
(
Private
_parentData, -- Object's parent block/group struct
_groupList = #(), -- Subgroups
_objItems = #(), -- Level-1 nodes contain objects
_residentReasons,
_boundsOutline,
_propsPerLodCounts = #(), -- Collects number of LOD-blocks lodding different numbers of props
Public
GUID, -- Randomly-generated identifier-value
groupName,
childNum = 0, -- Used to generate LOD-block names
lodPrefixName,
lodObjName,
-- Top-level Lod-objects can be automatically parented to a ContainerLOD object:
containerLodParent,
tagIdx, -- Group's dataTag index, as collected in RsLodCombinerTool.dataTags
gridParentIdxList, -- Idx-list of grid-parent nodes, used for reblocking objects
lodObjItem, -- LOD-object generated for this group
dummySlod = False, -- Generate dummy-object for level-2 lod-groups with non-slod lods
objCount = 0, -- Total number of subobjects
lodObjCount = 0, -- Total number of generated lod-objects
sourceHash = 0, -- Current prop-hash at last check
treeHash = 0, -- Used to combine object-hashes, without setting to 0 for non-lodding nodes.
lodObjHash = 0, -- Set to combined prop-hash when lodObjItem is generated
hasLod, -- Set to true if lodObjItem exists
hasTrees = False, -- Group-sources include billboard (tree) props
dontWaterReflect = True, -- A source-prop has "Generate Water Reflective LODs" ticked
lodSynced, -- Set to true if lodObjHash matches sourceHash, false if not. Undefined if LOD isn't required.
lodsSynced, -- True if this node and its children are all in-sync.
genLodLevel, -- This group-node generates this lod-level:
lodLevelsUsed,
lodLevelsCount,
blockType, blockRadius, blockPos, blockRot, childBlockPos, blockOrigin,
childBlockType = #Hex, childBlockRadius,
blockByProp = True, -- If true, LOD-groups will only contains props with the same name
blockByPropName, -- If a blockByProp group, group only contains props with this objectname
blockIPLName, -- We only allow groups to include props with the same "IPL Group" name
iplObjCount = 0, -- Number of objects that would exist in this IPL if Lods have been generated
residentRefNums,
residentDistLimits,
isResidentBlock = False, -- True if this is LOD-group used for Memory Resident props
lodDistIdx = 0, -- Used to split Resident Model lod-blocks up into different LOD-distance ranges.
worldNormal = [0,0,1], -- Used to set "Tree_Lod2" shader "World Normal" values; set to undefined if Tree Normals are to be used instead.
-- Used to store bounding-box and area-shape info:
hexQuadTriHeight, triVerts,
boundsMin, boundsMax, boundsSize,
expanded = false,
-- This is set to True if an outline-change has been made,
-- and reblocking still needs to happen
reblockRequired = False,
-- Accessor-functions for private values:
fn GetGroupList = (return _groupList),
fn SetGroupList newList = (_groupList = newList),
fn GetParentData = (return _parentData),
fn SetParentData newParentData = (_parentData = newParentData),
fn GetObjItems = (return _objItems),
fn SetObjItems newList = (_objItems = newList),
fn GetResidentReasons = (return _residentReasons),
fn GetBoundsOutline = (return _boundsOutline),
fn GetPropsPerLodCounts = (return _propsPerLodCounts),
-- Get this group's top-level parent
fn GetRootData =
(
return (_parentData.GetRootData())
),
-- Returns list of map's Memory Resident object-refs indexes, referenced from map-node's bitarray:
fn getResidentRefNums =
(
-- Set up references:
if (residentRefNums == undefined) do
(
local slodRoot = this.GetRootData()
residentRefNums = slodRoot.residentRefNums
_residentReasons = slodRoot.GetResidentReasons()
residentDistLimits = slodRoot.residentDistLimits
)
return residentRefNums
),
-- Get list of value-names to copy when cloning a struct
fn GetClonePropNames =
(
local outVal = gRsLodCombinerVals.grpClonePropNames
if (outVal != Undefined) do
return outVal
local ignoreNames = #(#LodObjItem, #TriVerts, #ContainerLodParent, #Guid)
outVal = #()
for propName in (GetPropNames this) do
(
local val = GetProperty this propName
-- Collect non-function values if they're not to be ignored
if (not IsKindOf val MAXScriptFunction) and (FindItem ignoreNames propname == 0) do
append outVal propName
)
-- Store the list for further uses
gRsLodCombinerVals.grpClonePropNames = outVal
return outVal
),
-- Makes copy of current LOD-block group-struct, with arrays dereferenced and cleared:
fn Clone =
(
-- Make new Group
local newGrp = RsLodCombinerGroup guid:(GenGuid())
-- Copy most values across
for propName in (this.GetClonePropNames()) do
(
local val = GetProperty this propName
SetProperty newGrp propName val
)
-- Transfer this private value
newGrp.SetParentData _parentData
return newGrp
),
-- Gets combined list of group's subgroup list-items:
fn GetSubGroups recurse:false =
(
local outList = #()
Join outList _groupList
if recurse do
(
for item in _groupList do
(
join outList (item.getSubGroups recurse:true)
)
)
return outList
),
fn UpdateBounds =
(
-- Filter out undefined values (these shouldn't happen!)
_objItems = for item in _objItems collect
(
if (item == undefined) then (print "updateBounds: UNDEFINED OBJECT-ITEM!"; dontCollect) else item
)
-- Collect object-positions:
local minMaxList = for item in _objItems where (isValidNode item.node) collect item.node.pos
local outlineConsiderList = deepCopy minMaxList
-- Add bounds of subgroups to position-list:
for grp in _groupList do
(
if (grp.boundsSize == Undefined) do
grp.UpdateBounds()
if (grp.boundsSize != undefined) do
(
append minMaxList grp.boundsMin
append minMaxList grp.boundsMax
)
local grpOutline = grp.GetBoundsOutline()
if (grpOutline != Undefined) do
Join outlineConsiderList grpOutline
)
this.boundsSize = Undefined
this.boundsMin = Undefined
this.boundsMax = undefined
_boundsOutline = Undefined
if (outlineConsiderList.count == 0) do
return undefined
RsGetBBox minMaxList &boundsMin &boundsMax
boundsSize = boundsMax - boundsMin
-- Generate bounding-outline:
--_boundsOutline = #(boundsMin, [boundsMax.X, boundsMin.Y, 0], boundsMax, [boundsMin.X, boundsMax.Y, 0]) -- Bounding-box
_boundsOutline = RsFindConvexOutline outlineConsiderList
local minZ = boundsMin.z
for pos in _boundsOutline do pos.z = minZ
if (blockType == #none) do
(
blockPos = boundsMin + (0.5 * boundsSize)
)
if (childBlockType == #none) do
(
childBlockPos = boundsMin + (0.5 * boundsSize)
)
return boundsSize
),
-- Gets implicitly-set values from parent, if undefined:
fn getParentVals force:False =
(
if force or (blockType == undefined) do
(
blockType = _parentData.childBlockType
blockRadius = _parentData.childBlockRadius
)
if (childBlockRadius == undefined) and (genLodLevel != 1) do
(
childBlockRadius = gRsLodCombinerVals.defBlockRadius[genLodLevel]
)
),
-- Update block-area values, if group is gridded:
fn setBlockGeom scaleUp:true =
(
-- Get implicitly-set values from parent, if undefined:
getParentVals()
if (blockRadius == undefined) do return false
case blockType of
(
#hex:
(
hexQuadTriHeight = blockRadius * cos 30
)
#tri:
(
local triBaseHeight = blockRadius * sin 30
local triBaseRadius = blockRadius * cos 30
triVerts = #([0,-blockRadius,0], [-triBaseRadius,triBaseHeight,0], [triBaseRadius,triBaseHeight,0])
-- Verts are scaled out slightly, to make sure isInBlockArea picks up edge-cases:
local triXformer = if scaleUp then (scaleMatrix [1.001, 1.001, 1.001]) else (matrix3 1)
-- Rotate verts around pivot:
if (blockRot != undefined) and (blockRot != 0) then
(
rotateZ triXformer blockRot
)
triXformer[4] = blockPos
-- Transform verts:
for n = 1 to 3 do
(
triVerts[n] *= triXformer
)
)
)
),
-- Get blockByPropName from first object in block:
fn setBlockPropName =
(
if blockByProp and (not isResidentBlock) and (_objItems.count != 0) and (_objItems[1].node != undefined) then
(
blockByPropName = toLower _objItems[1].node.objectName
)
else
(
blockByPropName = undefined
)
),
-- Find lod-distance index for a prop, used to avoid having Resident blocks containing props with wildly different lod-distances.
-- propLodDistIdx is index below where (DistLimit > propLodDist)
getPropLodDistIdx_ref,
fn getPropLodDistIdx obj =
(
-- Set up reference to map-node's function:
if (getPropLodDistIdx_ref == undefined) do
(
local slodRoot = this.GetRootData()
getPropLodDistIdx_ref = slodRoot.getPropLodDistIdx
)
getPropLodDistIdx_ref obj
),
-- Returns true if a position is within this block's area:
fn isInBlockArea obj =
(
local retVal = true
-- These tests are only applied to level-1 lods:
if (genLodLevel == 1) do
(
getResidentRefNums()
local isResidentProp = (obj.refDef != undefined) and residentRefNums[obj.refDef.num]
case of
(
-- Don't add Resident models to non-resident blocks, and vice-versa:
(isResidentProp != isResidentBlock):
(
retVal = false
)
-- Only match up Resident props to blocks with matching lod-distance index:
isResidentProp:
(
-- Allow prop into block if lodDistIdx matches:
retVal = ((getPropLodDistIdx obj) == lodDistIdx)
)
-- Check prop-name, if blockByProp is active:
blockByProp:
(
if (blockByPropName == undefined) do
(
setBlockPropName()
)
if ((blockByPropName != undefined) and (not matchPattern obj.objectName pattern:blockByPropName)) do
(
retVal = false
)
)
)
)
if not retVal do
return False
if ((blockPos == undefined) and (blockType != #none)) do
this.UpdateBounds()
local objPos = obj.pos
case blockType of
(
#none:()
#box:
(
-- Is point inside box?
retVal = (objPos.x <= (blockPos.x + blockRadius)) and (objPos.x >= (blockPos.x - blockRadius)) and
(objPos.y <= (blockPos.y + blockRadius)) and (objPos.y >= (blockPos.y - blockRadius))
)
#hex:
(
-- Get pos relative to hex, and put it in positive hex-quadrant:
local qx = abs (objPos.x - blockPos.x)
local qy = abs (objPos.y - blockPos.y)
-- Is point outside hex-quad's bounding-box?
if (qx > blockRadius) or (qy > hexQuadTriHeight) then
(
retVal = False
)
else
(
-- Is point inside quad?
retVal = (blockRadius * hexQuadTriHeight) - (hexQuadTriHeight * qx) - (0.5 * blockRadius * qy) >= 0
)
)
#tri:
(
-- Is point inside triangle?
retVal = RSGeom_PointInTriangle triVerts[1] triVerts[2] triVerts[3] objPos
)
)
return retVal
),
-- Get number of objects for each lod-level (item 1 is for non-lod objects)
-- (used to properly update progress-bar during lod-generation)
fn SetLodLevelsCount =
(
this.lodLevelsUsed = #{}
this.lodLevelsCount = for n = 1 to 4 collect 0
for itemList in #(this.GetObjItems(), this.GetGroupList()) do
(
for n = 1 to itemList.count do
(
local thisItem = itemList[n]
local childLodLevelsUsed = thisItem.lodLevelsUsed
local childLodLevelsCount = thisItem.lodLevelsCount
this.lodLevelsUsed += childLodLevelsUsed
for n = 1 to 4 do
this.lodLevelsCount[n] += childLodLevelsCount[n]
)
)
return this
),
-- Returns list of objects in group:
fn getObjs lodFilter: recurse:true info:false =
(
-- Get group's object-list:
local retVal = for item in _objItems collect item
-- Add subgroups' object-lists:
if recurse do
(
for item in _groupList do
(
join retVal (item.getObjs info:true)
)
)
-- Filter to objects offering meshes for specific LOD-level:
if (lodFilter != unsupplied) and (lodFilter != false) do
(
local filterLevel = if (lodFilter == true) then genLodLevel else lodFilter
retVal = for item in retVal where (item.canLod) and (item.sourceLodLevels >= filterLevel) collect item
-- Filter out half of the small-face slod-sources:
if (filterLevel > 1) do
(
local useThis = True
retVal = for item in retVal collect
(
-- Use every other small-face slod-source:
if item.hasSmallFaces[filterLevel] then
(
if useThis then
(
useThis = False
item
)
else
(
useThis = True
DontCollect
)
)
else
(
item
)
)
)
)
if not info do
(
retVal = for item in retVal collect item.node
)
return retVal
),
-- Returns list of lod-objects generated by group (and its subgroups)
fn getLodObjs info:false recurse:true =
(
local retVal = #(lodObjItem)
if recurse do
(
for grp in (getSubGroups recurse:true) do
(
append retVal grp.lodObjItem
)
)
retVal = for item in retVal where ((item != undefined) and (isValidNode item.node)) collect item
if not info do
(
retVal = for item in retVal collect item.node
)
return retVal
),
-- Remove group's lod-objects:
fn deleteLods recurse:false =
(
local grps = #()
if recurse do
(
join grps (getSubGroups recurse:true)
)
-- Delete lod-objects:
local delObjs = getLodObjs recurse:recurse
--format "DELETING OBJS: %\n" (delObjs as string)
undo off
(
delete delObjs
)
-- Set groups to unsynced:
lodObjItem = grps.lodObjItem = undefined
lodObjHash = grps.lodObjHash = 0
lodSynced = lodsSynced = grps.lodSynced = grps.lodsSynced = undefined
return OK
),
-- Removes this group from its parent-grouplist, and deletes removed-hierarchy's LOD-objects:
fn DeleteGroup =
(
-- Delete group's generated lod-objects:
deleteLods recurse:true
-- Filter this group out from parent-item's groupList:
local ParentGrpList = _parentData.GetGroupList()
local FindNum = FindItem ParentGrpList This
if (FindNum != 0) do
(
DeleteItem ParentGrpList FindNum
)
),
-- Generate combined hash for group's lod-source objects:
fn getSourceHash getFresh:true srcObjItems: =
(
if (srcObjItems == unsupplied) do
(
srcObjItems = getObjs lodFilter:true info:true
)
-- Collect hashes for objects with matching LodLevels:
local hashes = for item in srcObjItems collect (item.getSourceHash getFresh:getFresh)
local retVal = if (hashes.count == 0) then 0 else (getHashValue hashes 1)
-- Special case for slods with sub-lods
this.dummySlod = False
-- SLOD is index 3 because lodLevelsCount corresponds to these lod-prop counts #([none],[lod],[slod],[slod2])
if (retVal == 0) and (genLodLevel == 2) and (lodLevelsCount[3] == 0) do
(
-- Do any sub-lods generate lods?
local noSubLods = True
for grp in _groupList while noSubLods do
(
noSubLods = (grp.sourceHash == 0)
)
-- If some do, this node should generate a dummy-slod:
if (not noSubLods) do
(
this.dummySlod = True
retVal = (getHashValue #("dummySlod", gRsLodCombinerVals.dataVersion, gRsLodCombinerVals.dummySlodVersion) 1) -- Arbitrary version-linked value for dummy-slod
)
)
--format "GROUP HASH: %\n" retVal
return retVal
),
-- Gets number lod-objects for each props-per-lod-group count...
-- (used to show 1-prop/2-prop status-text bits)
fn SetPropsPerLodCounts =
(
_propsPerLodCounts = #()
local lodPropsCount = 0
for item in _objItems where (item.sourceHash != 0) do
(
lodPropsCount += 1
)
if (lodPropsCount != 0) do
(
_propsPerLodCounts[lodPropsCount] = 1
)
for grp in _groupList do
(
local grpCounts = grp.GetPropsPerLodCounts()
for n = 1 to grpCounts.count do
(
local thisVal = grpCounts[n]
if (thisVal != undefined) do
(
if (_propsPerLodCounts[n] == undefined) do
(
_propsPerLodCounts[n] = 0
)
_propsPerLodCounts[n] += thisVal
)
)
)
return OK
),
-- Update values to match this group's current state:
fn UpdateVals refreshObjVals:False =
(
-- Update block-prop assignment if block-gridding values have changed
if this.reblockRequired do
this.ReassignBlocks()
-- Generate GUID, if group doesn't already have one:
if (this.guid == Undefined) do
this.guid = genGuid()
local doPrompt = (groupName != undefined)
if doPrompt do
PushPrompt ("Updating SLOD-group values: " + groupName)
-- Initialise implicitly-set values from parent, if undefined
this.GetParentVals()
local subIpls = #()
local treeHashes = #()
-- Filter out deleted/undefined objects, do object-value updates if required:
_objItems = for item in _objItems where (item != undefined) and (isValidNode item.node) collect
(
if refreshObjVals do
(
item.UpdateVals()
)
Append subIpls (item.iplGroup as string)
Append treeHashes item.treeHash
item
)
objCount = _objItems.count
-- Update subgroup values first:
for item in _groupList do
(
item.UpdateVals refreshObjVals:refreshObjVals
objCount += item.objCount
append treeHashes item.treeHash
-- Collect 'IPL Group' for group-nodes that generate lods:
if (item.blockIPLName != Undefined) do
Append subIpls item.blockIPLName
)
-- Used to track overall changes to sub-nodes:
this.treeHash = GetHashValue treeHashes 1
-- Get props-per-lod counts, used to show 1-prop/2-prop status-text bits:
this.SetPropsPerLodCounts()
-- Set IPL Group text based on group's sub-objects/groups:
subIpls = makeUniqueArray subIpls
blockIPLName = case subIpls.count of
(
1:(subIpls[1])
0:(undefined)
Default:(#Multiple)
)
this.SetBlockGeom()
this.UpdateBounds()
this.SetLodLevelsCount()
-- Get list of props that'll be used to generate this node's lod-object:
local srcObjs = (getObjs lodFilter:genLodLevel info:true)
sourceHash = getSourceHash getFresh:true srcObjItems:srcObjs
-- Get total lodObjCount for this group:
lodObjCount = if (sourceHash == 0) then 0 else 1
for grp in _groupList do
(
lodObjCount += grp.lodObjCount
)
-- Check for missing lod-object:
local propCount = srcObjs.count
local needsLod = (dummySlod) or (propCount != 0)
hasLod = (lodObjItem != undefined) and (isValidNode lodObjItem.node)
-- Does this node use tree-props?
hasTrees = False
for objItem in srcObjs while (not hasTrees) do
(
hasTrees = objItem.isTree
)
-- Set as unsynced if lod-object is missing, or no longer requires one:
lodSynced = if needsLod then
(
(hasLod and (sourceHash == lodObjHash))
)
else
(
if hasLod then False else undefined
)
if (genLodLevel == 1) then
(
-- Should this lod-node be water-reflected?
dontWaterReflect = True
for objItem in srcObjs while (dontWaterReflect) do
(
dontWaterReflect = (not objItem.genWaterReflect)
)
-- First-level lods just get lodsSynced value from themselves:
lodsSynced = lodSynced
-- Get blockByPropName from first object in block:
if (blockByPropName == undefined) do
(
setBlockPropName()
)
)
else
(
-- Get child-node LOD-sync status:
lodsSynced = undefined
for item in _groupList where (item.lodsSynced != undefined) while (lodsSynced != false) do
(
lodsSynced = item.lodsSynced
)
for item in _groupList where (item.lodSynced != undefined) while (lodsSynced != false) do
(
lodsSynced = item.lodSynced
)
)
if doPrompt do
PopPrompt()
return lodSynced
),
-- Returns true if LOD-group should be split up into siblings - if it has too many objects, or is too big:
fn ShouldSplitGroup =
(
-- Split groups if they will be lod-parents to >maxLodCount objects:
local subLodObjCount = 0
for itemList in #(_objItems, _groupList) do
(
for item in itemList where (item.sourceHash != 0) do (subLodObjCount += 1)
)
local shouldSplit = (subLodObjCount > gRsLodCombinerVals.maxLodCount)
return shouldSplit
),
-- Generate smaller sibling LOD-block groups, based on current one
fn SplitGroup allowEmpty:False =
(
local sourceGrp = This
local newBlocks = #()
local halfRadius = 0.5 * blockRadius
-- List all objItems in group/subgroups:
local allObjItems = this.GetObjs recurse:True info:True
local objsAdded = #{}
objsAdded.count = allObjItems.count
case blockType of
(
#none: -- Split non-gridded blocks in the middle of longest axis
(
local longAxis = if (boundsSize.Y > boundsSize.X) then 2 else 1
local midVal = blockPos[longAxis]
local objItemLists = #(#(),#())
-- Split object-list in two along longest axis:
for item in allObjItems do
(
if (item.node.pos[longAxis] > midVal) then
Append objItemLists[1] item
else
Append objItemLists[2] item
)
-- Set all objsAdded flags to true:
objsAdded = -objsAdded
for objList in objItemLists do
(
local newGrp = this.Clone()
newGrp.SetObjItems objList
newGrp.UpdateBounds()
Append newBlocks newGrp
)
)
#box:
(
-- Split boxes to smaller boxes:
local offsets = #([1,1,0], [1,-1,0], [-1,-1,0], [-1,1,0])
local offsets = for pos in offsets collect (pos * halfRadius)
for offset in offsets do
(
local newGrp = this.Clone()
newGrp.blockPos = (blockPos + offset)
newGrp.blockRadius = halfRadius
Append newBlocks newGrp
)
)
#hex: -- Split hexes to tris:
(
if (hexQuadTriHeight == undefined) do
this.SetBlockGeom()
local outerVertOffsets = #([-halfRadius, hexQuadTriHeight, 0], [halfRadius, hexQuadTriHeight, 0], [blockRadius, 0, 0], [halfRadius, -hexQuadTriHeight, 0], [-halfRadius, -hexQuadTriHeight, 0], [-blockRadius, 0, 0])
local outerVerts = for offset in outerVertOffsets collect (blockPos + offset)
append outerVerts outerVerts[1]
-- Create six triangle-blocks from this hex-block
local triFlipped = false
for n = 1 to 6 do
(
local triVerts = #(blockPos, outerVerts[n], outerVerts[n + 1])
local triMidPos = (triVerts[1] + triVerts[2] + triVerts[3]) / 3
local thisRot = if (blockRot == undefined) then 0 else (copy blockRot)
local triRot = if triFlipped then (thisRot + 180) else (thisRot)
local triRadius = distance blockPos triMidPos
-- Create triangles around hex's pivot:
local triBlock = this.Clone()
triBlock.blockType = #Tri
triBlock.childBlockType = #Tri
triBlock.blockPos = triMidPos
triBlock.blockRot = triRot
triBlock.blockRadius = triRadius
triBlock.setBlockGeom()
triFlipped = (not triFlipped)
Append newBlocks triBlock
)
)
#tri: -- Split tris to smaller tris:
(
-- Make sure verts don't have extra scaling on them:
setBlockGeom scaleUp:false
local outerPoints = #(triVerts[1], triVerts[2], triVerts[3], triVerts[1])
local midPoints = for n = 1 to 3 collect
(
0.5 * (outerPoints[n] + outerPoints[n + 1])
)
insertItem midPoints[3] midPoints 1
local triVertLists = for n = 1 to 3 collect #(outerPoints[n], midPoints[n + 1], midPoints[n])
append triVertLists #(midPoints[1], midPoints[2], midPoints[3])
for triNum = 1 to 4 do
(
local triVerts = triVertLists[triNum]
local triMidPos = (triVerts[1] + triVerts[2] + triVerts[3]) / 3
local thisRot = if (blockRot == undefined) then 0 else (copy blockRot)
local triRot = if (triNum == 4) then (thisRot + 180) else (thisRot)
local triRadius = distance blockPos triMidPos
local triBlock = this.Clone()
triBlock.blockPos = triMidPos
triBlock.blockRot = triRot
triBlock.blockRadius = triRadius
triBlock.setBlockGeom()
Append newBlocks triBlock
)
)
)
if (blockType != #none) do
(
for blockItem in newBlocks while (objsAdded.numberSet != allObjItems.count) do
(
local newBlockObjs = #()
for objNum = (-objsAdded) do
(
local objItem = allObjItems[objNum]
if (blockItem.IsInBlockArea objItem.node) do
(
Append newBlockObjs objItem
objsAdded[objNum] = True
)
)
blockItem.SetObjItems newBlockObjs
)
)
-- Empty current block's object-list:
-- (For safety, this keeps objects that weren't inside the new blocks for some reason:
local remainingObjs = for objNum in (-objsAdded) collect allObjItems[objNum]
this.SetObjItems remainingObjs
if (remainingObjs.count != 0) do
Append newBlocks this
-- Filter out empty blocks:
if not allowEmpty do
newBlocks = for newBlock in newBlocks where ((newBlock.GetObjItems()).count != 0) collect newBlock
return newBlocks
),
-- Renames a set of SLOD-groups, adding number-suffixes to avoid repeats for a particular LOD-level:
fn setGrpNames renameGrps newName recurse:false =
(
if (renameGrps.count == 0) do
return False
local changed = false
local grps = for item in renameGrps collect item
if recurse do
(
-- Add groups' subgroups to rename-list:
local grpNum = 0
while (grpNum < grps.count) do
(
grpNum += 1
-- Don't add level-1 groups to rename-list, as they don't get their own groupNames:
if (grps[grpNum].genLodLevel < 2) do
(
join grps (grps[grpNum].GetGroupList())
)
)
)
-- Filter out LOD-level groups:
grps = for grp in grps where (grp.genLodLevel > 1) collect grp
-- Seperate selected groups per map:
local mapNodes = #()
local perMapGrps = #()
-- Make array of selected slod-groups per lod-level per map:
for grpData in grps do
(
local mapNode = grpData.GetRootData()
local mapNum = findItem mapNodes mapNode
local grpLevel = grpData.genLodLevel
if (mapNum == 0) do
(
append mapNodes mapNode
append perMapGrps #()
mapNum = mapNodes.count
)
if (perMapGrps[mapNum][grpLevel] == undefined) do
(
perMapGrps[mapNum][grpLevel] = #()
)
append perMapGrps[mapNum][grpLevel] grpData
)
-- Filter out undefined items from lists:
perMapGrps = for lodLevelGrps in perMapGrps collect (for item in lodLevelGrps where (item != undefined) collect item)
for mapNum = 1 to mapNodes.count do
(
-- Collect nodes for map that are not due to be renamed
local mapGrps = (mapNodes[mapNum].GetSubGroups recurse:true)
mapGrps = for grp in mapGrps where (grp.genLodLevel > 1) and (grp.groupName != Undefined) collect grp
mapGrps = for grp in mapGrps where (FindItem grps grp == 0) collect grp
-- We don't want to generate objects with names more than 30 characters long - clamp name-length:
local thisMapName = mapNodes[mapNum].mapName
local maxLength = (30 - thisMapName.count - 8)
local setNewName = newName
if (setNewName.count > maxLength) do
(
setNewName = RsStripSuffixNums (subString newName 1 maxLength)
)
-- Find groups in same container with same name and lod-level:
for lodLevelGrps in perMapGrps[mapNum] do
(
local lodLevel = lodLevelGrps[1].genLodLevel
mapLodLevelGrps = for grp in mapGrps where (grp.genLodLevel == lodLevel) collect grp
local exactMatchFound = False
local matchedNameNums = #()
local newNameNoNum = (RsStripSuffixNums setNewName)
local newNameHasNum = (newNameNoNum != setNewName)
local matchNamePattern = newNameNoNum + "*"
-- Find existing matching group-names, or same name but with number-suffix
for grp in mapLodLevelGrps where (FindItem lodLevelGrps item == 0) do
(
local grpName = grp.groupName
if (MatchPattern grpName pattern:matchNamePattern) do
(
if (not exactMatchFound) and (MatchPattern grpName pattern:setNewName) do
exactMatchFound = True
-- Collect suffix-number value
local grpNameNoNum = RsStripSuffixNums grpName
if (grpNameNoNum.count != grpName.count) do
(
-- Get number-string from group-name
local grpNumStr = (Substring grpName (newNameNoNum.count + 1) -1)
-- If this substring definately only contains numbers
if (RsStripSuffixNums grpNumStr).count == 0 do
(
local grpNameNum = (grpNumStr as Integer)
AppendIfUnique matchedNameNums grpNameNum
)
)
)
)
-- Strip index from matched name, so new unused numbers can be used instead
if exactMatchFound do
setNewName = newNameNoNum
-- Get the next available suffix-index
local suffixNum = 1
if (matchedNameNums.count != 0) do
(
Sort matchedNameNums
suffixNum = matchedNameNums[matchedNameNums.count]
suffixNum += 1
)
local addSuffix = exactMatchFound or (lodLevelGrps.count != 1)
for grpData in lodLevelGrps do
(
local grpName = if addSuffix then
(
local numString = (FormattedPrint suffixNum format:"02i")
suffixNum += 1
local thisNewName = subString newNameNoNum 1 (maxLength - numString.count)
(thisNewName + numString)
)
else setNewName
--format "Setting groupName to % %\n" grpName grpData.guid
grpData.groupName = grpName
local objGrps = #(grpData)
if (grpData.genLodLevel == 2) do
(
join objGrps (grpData.GetGroupList())
)
for thisGrp in objGrps do
(
thisGrp.showName()
local lodObj = (thisGrp.getLodObjs recurse:False)[1]
if (lodObj != undefined) do
(
lodObj.name = thisGrp.lodObjName
)
)
changed = true
)
)
)
return changed
),
-- Save group-data to xml element:
fn store xmlWriter objsList:#() defaultStruct: =
(
xmlWriter.WriteStartElement "group"
-- Use this struct-instance to determine default values:
if (defaultStruct == unsupplied) do
(
defaultStruct = RsLodCombinerGroup()
)
-- Write attributes:
-- Write for all lod-levels
local attribsList =
#(
dataPair name:"guid" val:guid,
dataPair name:"genLodLevel" val:genLodLevel,
dataPair name:"blockPos" val:blockPos,
dataPair name:"blockRot" val:blockRot,
dataPair name:"worldNormal" val:worldNormal,
dataPair name:"blockOrigin" val:blockOrigin
)
if (genLodLevel == 1) then
(
-- Only store for lowest-level LODs:
join attribsList \
#(
dataPair name:"isResidentBlock" val:isResidentBlock,
dataPair name:"lodDistIdx" val:lodDistIdx
)
)
else
(
-- Only store these values for non-lowest-level LODs:
join attribsList \
#(
dataPair name:"groupName" val:groupName,
dataPair name:"childBlockType" val:childBlockType,
dataPair name:"childBlockPos" val:childBlockPos,
dataPair name:"childBlockRadius" val:childBlockRadius,
dataPair name:"blockByProp" val:blockByProp
)
)
for item in attribsList where
(
-- Either don't store default values, or don't store undefined values:
if (item.name == "worldNormal") then (item.val != (getProperty defaultStruct item.name)) else (item.val != undefined)
) do
(
local writeVal = if (isKindOf item.val booleanClass) then (if item.val then "1" else "0") else (item.val as string)
xmlWriter.WriteAttributeString item.name writeVal
)
-- Store objects-list:
for item in _objItems do
(
item.store xmlWriter objsList:objsList
)
-- Store lodObjs data, if defined:
if (lodObjItem != undefined) do
(
lodObjItem.store xmlWriter objsList:objsList
)
-- Store linked containerLod parent:
if (containerLodParent != undefined) do
(
containerLodParent.store xmlWriter objsList:objsList
)
-- Add subgroup data:
for item in _groupList do
item.Store xmlWriter objsList:objsList defaultStruct:defaultStruct
xmlWriter.WriteEndElement()
return OK
),
-- Set up group-struct based on xml element:
fn Restore xmlReader parentData: objsList:#() setBlockType:#None setBlockRadius:-1 readVersion:0 &progressVal: progressMax: &cancelled: &doReblock: =
(
if (progressMax != 0) do
(
progressVal += 1
if not (progressUpdate (100.0 * progressVal / progressMax)) do
(
cancelled = True
return false
)
)
local restoredGrp = RsLodCombinerGroup blockType:setBlockType blockRadius:setBlockRadius
restoredGrp.SetParentData parentData
for item in
#(
dataPair name:"guid" class:string,
dataPair name:"groupName" class:string,
dataPair name:"isResidentBlock" class:booleanClass,
dataPair name:"lodDistIdx" class:integer,
dataPair name:"blockPos" class:undefined,
dataPair name:"blockRot" class:float,
dataPair name:"blockOrigin" class:undefined,
dataPair name:"childBlockType" class:name,
dataPair name:"childBlockPos" class:undefined,
dataPair name:"childBlockRadius" class:float,
dataPair name:"blockByProp" class:booleanClass,
dataPair name:"genLodLevel" class:integer
)
do
(
local getVal = xmlReader.Item[item.name]
if (getVal != undefined) do
(
getVal = case item.class of
(
undefined:(execute getVal)
booleanClass:(getVal == "1")
default:(getVal as (item.class))
)
setProperty restoredGrp item.name getVal
)
)
-- Update values from older data:
if (readVersion < 1) do
(
-- Work out genLodLevel value for older data:
case of
(
-- Old block-structs are converted to level-1 groups:
(xmlReader.name == "block"):(restoredGrp.genLodLevel = 1)
-- Top-level groups are given the maximum lod-level:
(parentData.genLodLevel == 0):(restoredGrp.genLodLevel = gRsLodCombinerVals.lodSuffixes.count)
-- All other groups work out their lod-level from their parent-group:
Default:(restoredGrp.genLodLevel = (parentData.genLodLevel - 1))
)
-- In earlier data, only level-2 slodgroups have child-gridding:
case of
(
(restoredGrp.genLodLevel == 2):
(
local getVal = xmlReader.Item["blockType"]
if (getVal != undefined) do
(
restoredGrp.childBlockType = (getVal as name)
)
local getVal = xmlReader.Item["blockRadius"]
if (getVal != undefined) do
(
restoredGrp.childBlockRadius = (getVal as float)
)
local getVal = xmlReader.Item["groupPos"]
if (getVal != undefined) do
(
restoredGrp.childBlockPos = (execute getVal)
)
)
(restoredGrp.genLodLevel > 2):
(
restoredGrp.childBlockType = #none
restoredGrp.blockType = #none
)
)
)
-- LOD-block groups aren't allowed their own names, and take their blockByProp value from their parents:
if (restoredGrp.genLodLevel < 2) do
(
restoredGrp.groupName = undefined
restoredGrp.blockByProp = parentData.blockByProp
)
-- Reconstitute group's sub-item lists from xml, if it has any sub-elements:
if not xmlReader.IsEmptyElement do
(
xmlReader.Read()
local stopLoop = cancelled or xmlReader.IsEmptyElement
local blankLoopNum = 0
while (not stopLoop) do
(
local nodeType = xmlReader.NodeType
stopLoop = (nodeType == nodeType.EndElement) or (nodeType == nodeType.None) or (blankLoopNum == 2)
if (not stopLoop) do
(
local elemName = xmlReader.name
case elemName of
(
-- Prop/LOD objects:
"object":
(
blankLoopNum = 0
local newObjItem = RsLodCombinerObject.Restore xmlReader objsList:objsList parentData:restoredGrp readVersion:readVersion
case of
(
(newObjItem == dontCollect):()
(newObjItem.lodLevel == 0):(Append (restoredGrp.GetObjItems()) newObjItem)
(newObjItem.lodLevel == -1):(restoredGrp.containerLodParent = newObjItem)
(newObjItem.lodLevel > 0):(restoredGrp.lodObjItem = newObjItem)
)
)
-- Blank element...
"":
(
blankLoopNum += 1
)
-- SLOD/LOD-groups:
Default:
(
blankLoopNum = 0
local newGroup = RsLodCombinerGroup.restore xmlReader objsList:objsList parentData:restoredGrp setBlockType:restoredGrp.childBlockType setBlockRadius:restoredGrp.childBlockRadius readVersion:readVersion progressVal:&progressVal progressMax:progressMax cancelled:&cancelled doReblock:&doReblock
if (newGroup != dontCollect) do
(
local GrpList = restoredGrp.GetGroupList()
append GrpList newGroup
)
)
)
-- Read next element:
xmlReader.Read()
)
stopLoop = (stopLoop or cancelled)
)
)
-- Set "isResidentProp" value on props if this is a Memory Resident block:
if restoredGrp.isResidentBlock do
(
(restoredGrp.GetObjItems()).isResidentProp = True
)
-- See if any child-groups have the same saved LodLevel.
-- If so, the reader's gone wrong, or data has been saved after using previous wonky reader:
-- Fixed by moving dodgy nodes up until they hit the right level.
local GrpList = restoredGrp.GetGroupList()
for n = GrpList.Count to 1 by -1 do
(
local SubGrp = GrpList[n]
if (subGrp.genLodLevel == restoredGrp.genLodLevel) then
(
format " Arse! Xml-tree is out of whack. Fixing by moving node up a level: \"%\" (level %)\n" subGrp.groupName newGroup.genLodLevel
append ((restoredGrp.GetParentData()).GetGroupList()) subGrp
DeleteItem GrpList n
)
)
-- Retrieve lod-object's hash, so we can tell if this node is up-to-date:
if (restoredGrp.lodObjItem != undefined) do
(
restoredGrp.lodObjHash = restoredGrp.lodObjItem.sourceHash
)
-- Fix any unnamed subgroups:
if (restoredGrp.genLodLevel > 2) and (restoredGrp.groupName != undefined) do
(
local unnamedGrps = for grp in (restoredGrp.GetGroupList()) where (grp.groupName == undefined) collect grp
if (unnamedGrps.count != 0) do
restoredGrp.SetGrpNames unnamedGrps restoredGrp.groupName
unnamedGrps = undefined
)
-- We no longer want to allow non-gridded lod-groups, so convert them to the default Hex:
local isUngriddedBlockParent = False
if (restoredGrp.childBlockType == #None) do
(
restoredGrp.childBlockType = #Box
isUngriddedBlockParent = True
-- Trigger reblock (if this block's parent isn't going to be reblocked)
local parData = restoredGrp.GetParentData()
doReblock = not ((isKindOf parData RsLodCombinerGroup) and (parData.childBlockType == #None))
if doReblock do
format "SETTING \"NONE\" SLOD-GROUP TO \"HEX\": %\n" restoredGrp.groupName
)
-- If something in this group's hierarchy triggered a reblock:
if doReblock and (isUngriddedBlockParent or (restoredGrp.blockType == #None) or (restoredGrp.genLodLevel == gRsLodCombinerVals.lodSuffixes.count)) do
(
format "* Reblocking parent of fixed-up group: %\n" restoredGrp.groupName
doReblock = False
restoredGrp.ReassignBlocks()
)
return restoredGrp
),
-- Text to print in treeView for group struct:
fn ShowName =
(
if (genLodLevel == 1) then
(
if (_parentData.lodPrefixName == undefined) do
_parentData.ShowName()
-- LOD-block objects take most of their name from their parents:
this.lodPrefixName = _parentData.lodPrefixName + (formattedPrint this.childNum format:"02i") + "_"
)
else
(
local topNode = this.GetRootData()
this.lodPrefixName = topNode.mapName + "_" + (this.groupName as string) + "_"
)
local lodSuffix = if this.dummySlod then (gRsLodCombinerVals.dummySlodSuffix) else (gRsLodCombinerVals.lodSuffixes[this.genLodLevel])
this.lodObjName = this.lodPrefixName + lodSuffix
return lodObjName
),
-- Generate new sub-groups to hold objects:
fn CreateBlocks objs =
(
if (objs.count == 0) do
return #()
local newGrps = #()
local newLodLevel = (genLodLevel - 1)
local isLodBlocks = (newLodLevel == 1)
local doBlockByProp = (isLodBlocks and blockByProp)
-- Get implicitly-set values from parent, if undefined:
getParentVals()
local objNames = #()
local objListNums = #()
local objBlockPosList = #()
-- See if any of these objects' models are marked as Memory Resident:
local residentObjNums = #{}
if isLodBlocks do
(
residentObjNums.count = objs.count
getResidentRefNums()
for objNum = 1 to objs.count do
(
local objRef = objs[objNum].refDef
residentObjNums[objNum] = ((objRef != undefined) and residentRefNums[objRef.num])
)
)
local hasResidents = (residentObjNums.numberSet != 0)
local groupList = this.GetGroupList()
-- Split object-list per group if blocking per prop-name:
if doBlockByProp or hasResidents then
(
for objNum = 1 to objs.count do
(
local obj = objs[objNum]
local isResident = residentObjNums[objNum]
local objName = case of
(
isResident:(getPropLodDistIdx obj) -- Ranges from 0 to residentDistLimits.count
doBlockByProp:(toLower obj.objectName)
Default:(-1) -- Used for non-residents, when not blocking by prop-name
)
local findNum = findItem objNames objName
if (findNum == 0) do
(
local newArray = #{}
--newArray.count = objs.count
append objListNums newArray
append objNames objName
findNum = objNames.count
-- Get initial-blockPos from first subgroup with matching blockByPropName:
local nameBlockPos = undefined
for grp in groupList while (nameBlockPos == undefined) do
(
if (grp.blockByPropName == objName) do
(
if (grp.blockOrigin == undefined) do
(
grp.blockOrigin = grp.blockPos
)
nameBlockPos = grp.blockOrigin
)
)
append objBlockPosList nameBlockPos
)
objListNums[findNum][objNum] = True
)
)
else
(
Append objListNums #{1..objs.count}
)
for listNum = 1 to objListNums.count do
(
local nameVal = objNames[listNum]
local setResidentBlock = hasResidents and (isKindOf nameVal number) and (nameVal >= 0)
-- The lodDistIdx for Resident blocks will have been saved to objNames list:
local lodDistIdx = 0
if setResidentBlock do
lodDistIdx = objNames[listNum]
local theseNewGrps = #()
local objList = for objNum in objListNums[listNum] collect objs[objNum]
-- Create new blocks:
(
pushPrompt "Generating block-grid"
local blockGridOrigin = if doBlockByProp then objBlockPosList[listNum] else childBlockPos
-- Find box around objects' pivots:
local objPosList = for obj in objList collect obj.pos
local minPos
local maxPos
local listBoundsSize
if (blockGridOrigin == undefined) then
(
RsGetBBox objPosList &minPos &maxPos
listBoundsSize = maxPos - minPos
-- Set blockPos to midpoint of group's bounding-box:
blockGridOrigin = minPos + (0.5 * listBoundsSize)
blockGridOrigin.z = minPos.z
if (not doBlockByProp) do
(
childBlockPos = copy blockGridOrigin
)
)
else
(
listBoundsSize = [0,0,0]
-- if blockPos is already defined, make bounding-box cover same area but with this pivot:
for pos in objPosList do
(
local objOffset = pos - blockGridOrigin
for n = 1 to 3 do
(
local val = abs objOffset[n]
if (val > listBoundsSize[n]) do
(
listBoundsSize[n] = val
)
)
)
minPos = blockGridOrigin - listBoundsSize
maxPos = blockGridOrigin + listBoundsSize
listBoundsSize *= 2
)
-- Shift blocking-centre if generating tris via hexes:
if (childBlockType == #tri) do
(
blockGridOrigin -= childBlockRadius
minPos.Y -= childBlockRadius
listBoundsSize.Y += childBlockRadius
)
if (childBlockType == #box) then
-- Generate box-blocks to cover bounds:
(
-- blockRadius is the block's radius...
local childBlockWidth = 2 * childBlockRadius
--box pos:blockGridOrigin width:(maxPos.x - minPos.x) length:(maxPos.y - minPos.y)
-- Generate blockRadius grid around pos:
local xPosList = (for xPos = blockGridOrigin.x to (maxPos.x + childBlockWidth) by childBlockWidth collect xPos)
join xPosList (for xPos = (blockGridOrigin.x - childBlockWidth) to (minPos.x - childBlockWidth) by -childBlockWidth collect xPos)
local yPosList = (for yPos = blockGridOrigin.y to (maxPos.y + childBlockWidth) by childBlockWidth collect yPos)
join yPosList (for yPos = (blockGridOrigin.y - childBlockWidth) to (minPos.y - childBlockWidth) by -childBlockWidth collect yPos)
for xPos in xPosList do
(
join theseNewGrps \
(
for yPos in yPosList collect
(
--box pos:[xPos, yPos, blockGridOrigin.z] width:childBlockWidth length:childBlockWidth
local BoxGrp = RsLodCombinerGroup guid:(genGuid()) lodDistIdx:lodDistIdx genLodLevel:newLodLevel blockRadius:childBlockRadius childBlockType:#box blockPos:[xPos, yPos, blockGridOrigin.z] \
blockOrigin:blockGridOrigin blockByProp:blockByProp isResidentBlock:setResidentBlock
BoxGrp.SetParentData This
BoxGrp
)
)
)
)
else
-- Tris/hexes both start off with hex-blocks..
(
-- Double-up radius if hexes are going to be split to tris:
local hexRadius = if (childBlockType == #tri) then (2 * childBlockRadius) else childBlockRadius
local hexHorizSpacing = hexRadius * 1.5
local hexVerticSpacing = hexRadius * (cos 30.0)
local hexHeight = 2.0 * hexVerticSpacing
-- Get size of hexes in each direction required to cover bounding-box:
local hexXNum = 1 + (0.5 * listBoundsSize.x / hexHorizSpacing) as integer
local hexYNum = 1 + (0.5 * listBoundsSize.y / (2 * hexVerticSpacing)) as integer
local hexColumn = #()
for n = -hexYNum to hexYNum do
(
local offset = case of
(
(n < 0):(-hexVerticSpacing)
(n > 0):(hexVerticSpacing)
default:0
)
append hexColumn [0, n * hexHeight, 0]
)
local hexStaggerColumn = for hexPos in hexColumn collect (hexPos + [0, hexVerticSpacing,0])
hexStaggerColumn.count = (hexStaggerColumn.count - 1)
--append hexStaggerColumn (hexStaggerColumn[1] - [0, hexHeight, 0])
local colA, colB
if (mod hexXNum 2 == 0) then
(
colA = hexColumn
colB = hexStaggerColumn
)
else
(
colA = hexStaggerColumn
colB = hexColumn
)
local hexPositions = #()
for n = -hexXNum to hexXNum by 2 do
(
local thisCol = for pos in colA collect (pos + [n * hexHorizSpacing,0,0])
join hexPositions thisCol
)
for n = (-hexXNum + 1) to hexXNum by 2 do
(
local thisCol = for pos in colB collect (pos + [n * hexHorizSpacing,0,0])
join hexPositions thisCol
)
-- Offset hex-positions to cover group:
hexPositions = (for pos in hexPositions collect (blockGridOrigin + pos))
for hexPos in hexPositions collect
(
local hexBlock = RsLodCombinerGroup guid:(genGuid()) lodDistIdx:lodDistIdx genLodLevel:newLodLevel blockType:#hex childBlockType:#hex blockPos:hexPos blockRadius:hexRadius \
blockOrigin:blockGridOrigin blockByProp:blockByProp isResidentBlock:setResidentBlock
hexBlock.SetParentData this
hexBlock.setBlockGeom()
Append theseNewGrps hexBlock
)
)
-- Sort blocks by distance from start-point, to ensure blocks closer to centre get filled first:
(
local blockDists = for item in theseNewGrps collect (dataPair grp:item dist:(distance blockGridOrigin item.blockPos))
fn sortBlocks v1 v2 =
(
case of
(
(v1.dist == v2.dist):0
(v1.dist > v2.dist):1
Default:1
)
)
qsort blockDists sortBlocks
theseNewGrps = for item in blockDists collect item.grp
)
-- Split hex-blocks up into tri-blocks, if group is using that mode:
if (childBlockType == #Tri) do
(
local triBlocks = #()
for hexBlock in theseNewGrps do
Join triBlocks (hexBlock.SplitGroup allowEmpty:True)
theseNewGrps = triBlocks
)
popPrompt()
)
-- Add prop-objects to new blocks
local objsAdded = #{}
objsAdded.count = objList.count
for newGrp in theseNewGrps while (objsAdded.numberSet != objsAdded.count) do
newGrp.AddObjs objList done:&objsAdded firstRecurse:False
-- Filter out unoccupied blocks:
theseNewGrps = for item in theseNewGrps where ((item.GetGroupList()).count != 0) or ((item.GetObjItems()).count != 0) collect item
-- Set block z-positions, for showing in viewport:
(
local objPosList = for obj in objList collect obj.pos
local minPos, maxPos
RsGetBBox objPosList &minPos &maxPos
local midPosZ = minPos.Z + (0.5 * (maxPos.Z - minPos.Z))
for newGrp in theseNewGrps where (isKindOf newGrp.blockPos point3) and ((newGrp.GetObjItems()).count != 0) do
(
newGrp.blockPos.Z = midPosZ
)
)
-- Add new groups to main output-list:
Join newGrps theseNewGrps
)
return newGrps
),
-- Add objects to LOD-group:
fn AddObjs addObjList doRedraw:true &done: firstRecurse:true =
(
--if firstRecurse do (Format ".AddObjs - %\n" (gRsDebugStack.GetCallerLine()))
-- Allow Escape to break if in debug-mode:
if RsComboLodDebug and keyboard.escpressed do
return false
if (addObjList.count == 0) do
return False
-- Set to True if an Add occurs
local changed = False
-- Get implicitly-set values from parent, if undefined:
this.GetParentVals()
-- Only run these checks for function's first recursion:
if firstRecurse do
(
-- Only allow RsRefs in the same container to be added:
local contNode = (this.GetRootData()).contNode
addObjList = for obj in addObjList where (isRsRef obj) and ((Containers.IsInContainer obj) == contNode) collect obj
-- Load lod-distances for objects, if they've not already been loaded:
local refDefs = for obj in addObjList collect obj.refDef
gRsComboLodFuncs.loadSlodData refDefs
)
-- Set bitarray count to same as object-list, for proper use of inverted array:
if (done.count == 0) do
done.count = addObjList.count
-- Collect list of objects that fit in current group's area
local grpAreaObjs = #{}
grpAreaObjs.count = addObjList.count
for objNum = -done do
(
local objIsInArea = this.IsInBlockArea addObjList[objNum]
grpAreaObjs[objNum] = objIsInArea
)
-- This many objects are within this group's area:
local areaObjsCount = grpAreaObjs.numberSet
local groupList = this.GetGroupList()
-- Don't bother trying to add to group/subgroups if it doesn't fall within its block-area boundaries:
if (areaObjsCount != 0) do
(
case genLodLevel of
(
1: -- Add these objects to LOD-block's objItems list:
(
-- Valid objects currently in block's list: (getting list this way to avoid adding duplicates)
local curObjList = getObjs()
for objNum = grpAreaObjs do
(
local obj = addObjList[objNum]
-- Only actually add to this group's object-list if the object wasn't here already:
if (appendIfUnique curObjList obj) do
(
Append _objItems (RsLodCombinerObject.create obj parentData:This lodDistIdx:lodDistIdx)
changed = True
)
)
done += grpAreaObjs
)
Default: -- Add objs to subgroups, if they're inside this group's block-area:
(
local changedGrps = #{}
pushPrompt "Adding objects to existing LOD-groups.."
for grpNum = 1 to groupList.count while ((done * grpAreaObjs).numberSet != areaObjsCount) do
(
-- Try adding unassigned objects to subgroup:
local subGrp = groupList[grpNum]
-- Next line's recursion will not attempt to add objects that have been done or are outside this block's area:
local ignoreObjs = (done + (-grpAreaObjs))
local added = subGrp.AddObjs addObjList done:&ignoreObjs firstRecurse:False
-- Extract done-object indexes from updated IgnoreObjs bitarray, and apply changes to Done bitarray:
done += (ignoreObjs * grpAreaObjs)
changedGrps[grpNum] = added
if added do
changed = True
)
popPrompt()
-- Collect list of objects that fit in current group's area, but not in existing subgroups:
local newBlockObjNums = (grpAreaObjs - done)
local newBlockObjs = for objNum = newBlockObjNums collect addObjList[objNum]
-- Generate new sub-groups for these objects:
if (newBlockObjs.count != 0) do
(
local grpCount = groupList.count
Join groupList (this.CreateBlocks newBlockObjs)
if (grpCount != groupList.count) do
changed = True
done += grpAreaObjs
changedGrps += #{(grpCount + 1)..groupList.count}
)
-- Split changed LOD-blocks if necessary:
if (genLodLevel > 1) do
(
local grpCount = groupList.count
local delGrps = #{}
delGrps.count = grpCount
-- Split changed subgroups until they won't split no more:
while (changedGrps.numberSet != 0) do
(
local newChanges = #{}
-- Loop through unprocessed changed groups:
for grpNum = changedGrps do
(
if groupList[grpNum].shouldSplitGroup() do
(
didSplit = True
delGrps[grpNum] = True
local grpCount = groupList.count
local newGrps = groupList[grpNum].SplitGroup()
join groupList newGrps
newChanges += #{(grpCount + 1)..groupList.count}
)
)
-- Set up new groups for next round of processing:
changedGrps = newChanges
)
-- Remove split groups:
if (delGrps.numberSet != 0) do
(
-- Remove any generated lod-objects:
for grpNum = delGrps do
(
groupList[grpNum].deleteLods()
)
-- Filter deleted groups out of groupList:
delGrps.count = groupList.count
groupList = for grpNum = (-delGrps) collect groupList[grpNum]
_groupList = groupList
)
-- Higher lod-level group-nodes shouldn't have object-lists, so pass those objects down to lower levels...
if (genLodLevel > 2) do
(
for grp in groupList where ((grp.GetObjItems()).count != 0) do
(
local subGrpObjs = for item in grp.GetObjItems() collect item.node
(grp.GetObjItems()).count = 0
local subDone = #{}
grp.AddObjs subGrpObjs done:&subDone doRedraw:False firstRecurse:False
)
)
)
-- Rename any new groups:
if (groupName != undefined) do
(
local newGrps = for grp in groupList where (grp.groupName == undefined) collect grp
if (newGrps.count != 0) do
this.SetGrpNames newGrps groupName recurse:True
)
)
)
)
-- If any objects couldn't be added to this group or its subgroups, create a new non-gridded new subgroup instead:
if firstRecurse and (genLodLevel != 1) and (done.numberSet != done.count) do
(
local extraObjs = for objNum = -done collect addObjList[objNum]
pushPrompt "Adding objects to new LOD-groups..."
-- (createGroups sets current group as ungridded)
this.CreateGroups #(extraObjs) doRemove:False
popPrompt()
changed = True
)
-- Update group's bounds and redraw if change was made:
if changed do
(
this.UpdateBounds()
if firstRecurse and doRedraw do
CompleteRedraw()
)
return changed
),
-- Set up sub-blocks based on group's settings
fn ReassignBlocks =
(
PushPrompt ("Reassigning group's blocks: " + (groupName as String))
-- Reblocking is no longer required for this group
this.reblockRequired = False
-- Collect objects from group's current subgroups
local propObjs = this.GetObjs()
-- Don't reblock ungridded upper-level groups - try reblocking their child-groups:
(
-- Delete existing LOD-objects from current subgroups:
(
local lodObjs = #()
for grp in _groupList do
(
join lodObjs (grp.getLodObjs recurse:True)
)
if (lodObjs.count != 0) do
(
undo off
(
delete lodObjs
)
)
)
-- Remove existing groups:
_groupList.count = 0
-- Assign objects to new lod-blocks with updated block-shape settings
(
PushPrompt ("Creating new subgroups for: " + (groupName as string))
local done = #{}
this.AddObjs propObjs done:&done
PopPrompt()
)
-- Rename new group-blocks:
if (groupName != Undefined) do
RsLodCombinerGroup.setGrpNames _groupList groupName recurse:True
)
popPrompt()
),
fn GrpIsEmpty =
(
(_objItems.count == 0) and (_groupList.count == 0)
),
-- Remove gridded subgroups, and delete their generated ComboLod objects, if they have no objects:
fn RemoveEmptyGridBlocks delObjs: =
(
local firstRecurse = (delObjs == unsupplied)
if firstRecurse do
(
PushPrompt "Finding/removing empty grid-blocks..."
delObjs = #()
)
-- Recurse through subgroups first:
for grp in _groupList do
(
grp.removeEmptyGridBlocks delObjs:delObjs
)
-- Clear away empty lod-blocks and gridded groups:
(
local newGroupList = #()
for grp in _groupList do
(
if (not grp.GrpIsEmpty()) then
(
Append newGroupList grp
)
else
(
local lodObjs = grp.getLodObjs recurse:True
if (lodObjs.count != 0) do
Join delObjs lodObjs
)
)
_groupList = newGroupList
)
-- Delete obsolete lodObjs:
if firstRecurse do
(
if (delObjs.count != 0) do
Delete delObjs
PopPrompt()
)
return OK
),
-- Remove objects from group:
fn RemoveObjs remObjs removed:#{} checkAll:true removeEmpty:true updateBnds:True =
(
-- Get list of objects listed on this node:
local objs = getObjs recurse:false
--format "removeObjs, has %: \"%\"\n" objs.count (for item in _objItems collect (substring ((classof item) as string) 1 20))
local remCount = removed.numberSet
local remGrpObjNums = #{}
remGrpObjNums.count = _objItems.count
for objNum = 1 to remObjs.count where checkAll or (not removed[objNum]) while checkAll or ((removed.numberSet != remObjs.count) and (remGrpObjNums.numberSet != _objItems.count)) do
(
local remObj = remObjs[objNum]
local findNum = findItem objs remObj
if (findNum != 0) do
(
removed[objNum] = true
remGrpObjNums[findNum] = true
)
)
-- Recollect objs list without the remove-numbered items:
if (remGrpObjNums.numberSet != 0) do
(
_objItems = for objNum = -remGrpObjNums collect _objItems[objNum]
)
--format "removeObjs, keeps %: \"%\"\n" _objItems.count (for item in _objItems collect (substring ((classof item) as string) 1 20))
-- Now remove objects from subgroups:
for item in _groupList while checkAll or (removed.numberSet != remObjs.count) do
item.RemoveObjs remObjs removed:removed checkAll:checkAll removeEmpty:False updateBnds:false
-- Get rid of now-empty LOD-block subgroups:
if removeEmpty do
this.RemoveEmptyGridBlocks()
return OK
),
-- Add new subgroup(s) to group:
fn CreateGroups objLists doRemove:true grpName: grpParent: =
(
--Format ".CreateGroups - %\n" (gRsDebugStack.GetCallerLine())
if (not isKindOf objLists[1] Array) do
(objLists = #(for item in objLists collect item))
if (grpName == unsupplied) do
(grpName = groupName)
if (grpParent == unsupplied) do
(grpParent = this)
-- Remove objs from all map's groups, if they're already in use:
if doRemove do
(
local objs = #()
for objList in objLists do (join objs objList)
local topData = grpParent.GetRootData()
topData.removeObjs objs
)
-- Create new groups:
local newLodLevel = if (grpParent.genLodLevel == 0) then (gRsLodCombinerVals.lodSuffixes.count) else (grpParent.genLodLevel - 1)
-- Split object-lists up if they include multiple IPL Group values - LODs can't include multiple IPLs
local newObjLists = #()
for objList in objLists do
(
local objsPerIpl = #()
local iplGroups = #()
for obj in objList where (IsRsRef obj) do
(
local objIpl = GetAttr obj idxIPLGroup
local idx = FindItem iplGroups objIpl
if (idx == 0) then
(
Append objsPerIpl #(obj)
Append iplGroups objIpl
)
else
(
Append objsPerIpl[idx] obj
)
)
Join newObjLists objsPerIpl
)
objLists = newObjLists
-- Create new ungridded groups, one for each object-list:
local newGrps = #()
for grpNum = 1 to objLists.count do
(
local newGrp = RsLodCombinerGroup guid:(genGuid()) genLodLevel:newLodLevel blockType:#none blockByProp:blockByProp
NewGrp.SetParentData GrpParent
append (grpParent.GetGroupList()) newGrp
append newGrps newGrp
local done = #{}
newGrp.AddObjs objLists[grpNum] done:&done doRedraw:False
)
-- Rename the new group(s), and subgroups:
local grpNum = 0
while (grpNum < newGrps.count) do
(
grpNum += 1
join newGrps (newGrps[grpNum].GetGroupList())
)
RsLodCombinerGroup.SetGrpNames newGrps grpName recurse:True
return newGrps
),
-- Adds extra group-nodes to a group if its nodes have anappropriate genLodLevel values for that level:
fn treeLevelCorrector lodLevel grp: firstRecurse:true =
(
if (grp == unsupplied) do (grp = This)
local newGrp
local newGrpList = #()
for subGrp in (grp.GetGroupList()) do
(
if (subGrp.genLodLevel >= lodLevel) then
(
append newGrpList subGrp
)
else
(
-- Add new subgroup to be this subgroup's new parent:
if (newGrp == undefined) do
(
newGrp = RsLodCombinerGroup guid:(genGuid()) genLodLevel:lodLevel blockByProp:blockByProp
NewGrp.SetParentData Grp
this.SetGrpNames #(newGrp) grp.groupName
if (isKindOf grp RsLodCombinerGroup) do
(
newGrp.blockType = grp.blockType
newGrp.blockRadius = grp.blockRadius
newGrp.blockByProp = grp.blockByProp
)
append newGrpList newGrp
)
append (newGrp.GetGroupList()) subGrp
subGrp.SetParentData newGrp
)
)
-- Replace group's grouplist:
local GrpList = grp.GetGroupList()
GrpList.Count = 0
join GrpList newGrpList
newGrpList = undefined
local subLevel = (lodLevel - 1)
for subGrp in GrpList do
subGrp.TreeLevelCorrector subLevel firstRecurse:False
OK
),
-- Used by viewport-draw callback to draw group's overlays:
fn viewportDraw selected:false tagItem: showBoxes:True showShapes:True =
(
if (tagIdx == undefined) do return false
local dataTags = ::RsLodCombinerTool.dataTags
if (tagItem == unsupplied) do
(
tagItem = dataTags[tagIdx]
)
if tagItem.selected do
(
selected = true
)
-- Draw for this node:
if tagItem.viewShow and (not tagItem.isHidden) and (lodObjName != undefined) and (blockPos != undefined) do
(
if (boundsSize == Undefined) do
this.UpdateBounds()
if (boundsSize == Undefined) do
return False
local showColour = if selected then tagItem.viewportSelColour else tagItem.viewportColour
-- Show higher lod-levels as slightly darker:
for n = 2 to genLodLevel do
(
showColour *= 0.9
)
-- Draw LOD/grid-blocks:
local shapeLines = #()
if (showBoxes) or (showShapes) do
(
if (showBoxes) do
(
if (boundsSize == Undefined) do
this.UpdateBounds()
local outlineCorners = for pos in _boundsOutline collect (gw.wtransPoint pos)
append shapeLines outlineCorners
-- Draw additional marker if bounding box is zero-sized:
if ((distance boundsMin boundsMax) < 0.1) do
(
gw.wMarker (gw.wtransPoint boundsMin) #hollowBox color:showColour
)
-- If group has a containerLodParent, represent that:
if (containerLodParent != undefined) and (isValidNode containerLodParent.node) do
(
local contLodClr = containerLodParent.node.wireColor
contLodClr = ((3 * contLodClr) + white) / 4
local zPos = boundsMin.z
local contLodMin = containerLodParent.node.min
local contLodMax = containerLodParent.node.max
local drawContLodCorners = for cornerPos in #(contLodMin, [contLodMin.x,contLodMax.y,0], contLodMax, [contLodMax.x,contLodMin.y,0]) collect
(
cornerPos.z = zPos
gw.wtransPoint cornerPos
)
local drawBlockPos = (gw.wtransPoint blockPos)
-- Draw lines from box-corners to closest group-outline corners:
gw.setColor #line (contLodClr * 0.9)
for n = 1 to 4 do
(
local boxCorner = drawContLodCorners[n]
local cornerDists = for corner in outlineCorners collect (distance boxCorner corner)
local nearCorner = outlineCorners[findItem cornerDists (aMin cornerDists)]
gw.wPolyline #(boxCorner, nearCorner) False
)
gw.setColor #line contLodClr
gw.wPolyline drawContLodCorners true
)
)
if (showShapes) and (blockRadius != undefined) do
(
local shapeCorners
case blockType of
(
#box:
(
shapeCorners = for cornerPos in #([-1,-1,0], [1,-1,0], [1,1,0], [-1,1,0]) collect
(
gw.wtransPoint (blockPos + (blockRadius * cornerPos))
)
)
#hex:
(
local distA = blockRadius
local distB = (0.5 * blockRadius)
local distC = hexQuadTriHeight
shapeCorners = for cornerPos in #([-distA,0,0], [-distB,distC,0], [distB,distC,0], [distA,0,0], [distB,-distC,0], [-distB,-distC,0]) collect
(
gw.wtransPoint (blockPos + cornerPos)
)
)
#tri:
(
shapeCorners = for cornerPos in triVerts collect
(
gw.wtransPoint cornerPos
)
)
)
if (shapeCorners != undefined) do
(
append shapeLines shapeCorners
)
)
)
-- Draw shapes:
local multiLine = (genLodLevel > 1)
local midPos = (gw.wtransPoint blockPos)
for shapeNum = 1 to shapeLines.count do
(
local corners = shapeLines[shapeNum]
local useColour = showColour
-- Show bounding-boxes slightly darker than block-shapes:
if (showBoxes) and (shapeNum == 1) do
(
useColour *= 0.85
)
-- Find corner-offsets for additional block-outline lines:
local cornerOffsets
if multiLine do
(
local vecCorners = #(corners[corners.count])
join vecCorners corners
local vecs = for n = 2 to vecCorners.count collect
(
vecCorners[n] - vecCorners[n - 1]
)
append vecs vecs[1]
cornerOffsets = for n = 1 to corners.count collect
(
local vecA = vecs[n]
local vecB = vecs[n + 1]
3 * normalize [-(vecA.Y + vecB.Y), (vecA.X + vecB.X), 0]
)
)
for lineNum = 1 to genLodLevel do
(
gw.setColor #line useColour
local drawCorners = corners
gw.wPolyline drawCorners true
if multiLine do
(
-- Draw each extra line slightly darker than the previous one:
useColour *= 0.9
-- Expand corners out by offset-amount:
for n = 1 to corners.count do
(
corners[n] += cornerOffsets[n]
)
)
)
)
-- Draw centred text if node is selected
if selected do
(
local textMid = (GetTextExtent lodObjName) / 2
midPos.X -= textMid.X
midPos.Y += textMid.Y
gw.wtext midPos lodObjName color:showColour
)
)
-- Now draw sub-nodes - weight the ordering to put selected/out-of-sync items earlier:
local weightedSubItems = for item in _groupList where (item.tagIdx != undefined) collect
(
local subItemTag = dataTags[item.tagIdx]
local drawWeight = 0
if subItemTag.selected do (drawWeight += 5)
case (subItemTag.GetData()).lodSynced of
(
undefined:(drawWeight -= 1)
false:(drawWeight += 1)
)
dataPair tagItem:subItemTag drawWeight:drawWeight
)
-- Don't keep this big array around longer than it needs to be...
dataTags = undefined
-- Order subitems in order
fn orderItems v1 v2 = (v1.drawWeight - v2.drawWeight)
qsort weightedSubItems orderItems
-- Now draw subitems:
for item in weightedSubItems do
(
(item.tagItem.GetData()).viewportDraw tagItem:item.tagItem showBoxes:showBoxes showShapes:showShapes --selected:selected
)
)
)
-- Top-level structure used to define LodCombiner per map:
struct RsLodCombinerMap
(
Private
_parentData, -- Always undefined, supplied for compatibility with other data-structs
_residentReasons = #(), -- Why where these objects made Resident?
_groupList,
_propsPerLodCounts = #(), -- Collects number of LOD-blocks lodding different numbers of props
Public
dataNode, -- Helper-object used to store this map's data
contNode, -- DataNode's container
childBlockType = #none,
childBlockRadius = -1,
groupName = "combo", -- Used by new subgroups looking for a parent-name
mapName = "undefined",
fullMapName = "undefined",
tagIdx, -- Object's dataTag index, as collected in RsLodCombinerTool.dataTags
objCount = 0, -- Total number of subobjects
lodObjCount = 0, -- Total number of generated lod-objects
treeHash = 0,
lodLevelsUsed,
lodLevelsCount,
genLodLevel = 0, -- Map-structs don't generate specific lod-level objects
boundsSize, boundsMin, boundsMax,
doResidents = True, -- Enables Memory Resident listing for this map
residentRefNums = #{}, -- Object-ref indexes that make up most of map's props
residentDistLimits = #(), -- Resident props are only added to a block with matching lodDistIdx (a prop's lodDistIdx will be index that is below the next highest limit)
lodsSynced, -- Set to true if group-lods are all up-to-date
expanded = true,
needsLoad = False, -- Used by updateMapData function
-- Accessor-functions for private values:
fn GetParentData = (return _parentData),
fn GetGroupList = (return _groupList),
fn GetResidentReasons = (return _residentReasons),
fn GetPropsPerLodCounts = (return _propsPerLodCounts),
-- This is the map's root-structure, so this function returns itself:
fn GetRootData =
(
return This
),
-- Gets combined list of map's subgroup/block list-items:
fn getSubGroups recurse:false =
(
local outList = #()
Join outList _groupList
if recurse do
(
for item in _groupList do
(
join outList (item.getSubGroups recurse:true)
)
)
return outList
),
-- Get number of objects for each lod-level (item 1 is for non-lod objects)
-- (used to properly update progress-bar during lod-generation)
fn SetLodLevelsCount =
(
this.objCount = 0
this.lodObjCount = 0
this.lodLevelsUsed = #{}
this.lodLevelsCount = for n = 1 to 4 collect 0
for item in _groupList do
(
this.objCount += item.objCount
this.lodObjCount += item.lodObjCount
this.lodLevelsUsed += item.lodLevelsUsed
local itemLevels = item.lodLevelsCount
for n = 1 to 4 do
this.lodLevelsCount[n] += itemLevels[n]
)
return OK
),
-- Generate combined hash for map's lod-source objects:
fn getSourceHash getFresh:true =
(
local hashes = for item in _groupList collect (item.getSourceHash getFresh:getFresh)
if (hashes.count == 0) then 0 else (getHashValue hashes 1)
),
-- Removes objitems from their current lod-blocks, and re-adds them to their lod-blocks' parents.
-- This ensures that the objects are in suitable lod-blocks, via the block-assigning methods.
fn ReblockObjLods objItems =
(
if (objItems.count == 0) do
return OK
--Format ".ReblockObjLods - %\n" (gRsDebugStack.GetCallerLine())
PushPrompt "Updating LOD-blocking..."
objItems = makeUniqueArray objItems
-- Parent-groups for re-blocking, and the objects to be re-added to each group:
local parentGrps = #()
local grpObjs = #()
for item in objItems do
(
-- Find lod-group-parent for object:
local parGrp = (Item.GetParentData()).GetParentData()
local grpNum = findItem parentGrps parGrp
if (grpNum == 0) do
(
append parentGrps parGrp
append grpObjs #()
grpNum = parentGrps.count
)
-- Collect objects per lod-group:
append grpObjs[grpNum] item.node
)
for grpNum = 1 to parentGrps.count do
(
local grp = parentGrps[grpNum]
local objs = grpObjs[grpNum]
local msg = stringStream ""
format "Updating group: % (changing % objects)\n" grp.groupName objs.count to:msg
pushPrompt (msg as string)
-- Remove objects from their current groups, then re-add:
grp.removeObjs objs checkAll:False removeEmpty:False
local added = #{}
grp.AddObjs objs done:&added
popPrompt()
)
-- Update group-values:
for grp in _groupList do
grp.UpdateVals refreshObjVals:True removeEmpty:False
PopPrompt()
),
-- Returns list of objects in map's groups:
fn getObjs recurse:true info:false =
(
if not recurse do return #()
local grpObjs = #()
for grpInfo in _groupList do
(
join grpObjs (grpInfo.getObjs info:info)
)
return grpObjs
),
-- Find Lod Distance Index for a prop, used to avoid having Resident blocks containing props with wildly different lod-distances.
-- propLodDistIdx is index below where (DistLimit > propLodDist)
fn getPropLodDistIdx obj =
(
local propLodDist = (obj.scale.z * obj.refDef.comboLod.lodDistance)
local retVal = 0
for idx = 1 to residentDistLimits.count while (residentDistLimits[idx] < propLodDist) do
(
retVal = idx
)
return retVal
),
-- Finds objects in mixed-objectname lodgroups, and marks them for reblocking if they're not meant to be so:
fn findMixedNameBlocks mapObjItems:#() reblockObjItems:#() =
(
for objItem in mapObjItems do
(
local lodGrp = objItem.GetParentData()
if (lodGrp.blockByProp) and (not lodGrp.isResidentBlock) and (not objItem.isResidentProp) do
(
local modelName = objItem.node.objectName
if (lodGrp.blockByPropName == undefined) then
(
lodGrp.blockByPropName = (toLower modelName)
)
else
(
if not matchPattern (modelName) pattern:(lodGrp.blockByPropName) do
(
append reblockObjItems objItem
)
)
)
)
return OK
),
-- Generate list of 50x50m grid-points covered by this object's lod-circle:
fn getPropGrids objItem gridSize:50 =
(
local objPos = objItem.node.pos
local propGridPos = (objPos / gridSize)
propGridPos = [integer propGridPos.X, integer propGridPos.Y]
propGridPos *= gridSize
local radius = objItem.getLodDistance()
-- Collect list of possible grid-points for prop's circle to cover:
local xPosList = for x = propGridPos.x to (propGridPos.x + radius + gridSize) by gridSize collect x
join xPosList (for x = (propGridPos.x - gridSize) to (propGridPos.x - radius - gridSize) by -gridSize collect x)
local yPosList = for y = propGridPos.y to (propGridPos.y + radius + gridSize) by gridSize collect y
join yPosList (for y = (propGridPos.y - gridSize) to (propGridPos.y - radius - gridSize) by -gridSize collect y)
local propGrids = #()
for x in xPosList do
(
join propGrids \
(
for y in yPosList collect
(
[x,y]
)
)
)
-- Ignore grids that are outside prop's lod-circle:
local propPos = [objPos.X, objPos.Y]
propGrids = for pos in propGrids where (distance pos propPos < radius) or (pos == propGridPos) collect pos
return propGrids
),
-- Update map's ComboLodder-prop bounds
fn UpdateBounds updateGrpBounds:True =
(
boundsSize = boundsMin = boundsMax = undefined
-- Only bother updating group-boundaries if requested:
if updateGrpBounds do
(
for grp in _groupList do
grp.UpdateBounds()
)
local minMaxList = #()
for grp in _groupList where (grp.boundsMin != undefined) do
Join minMaxList #(grp.boundsMin, grp.boundsMax)
RsGetBBox minMaxList &boundsMin &boundsMax
boundsSize = boundsMax - boundsMin
return OK
),
-- Find common refs, and generate ResidentRefNums list for this map:
fn SetupResidents =
(
--Format ".SetupResidents - %\n" (gRsDebugStack.GetCallerLine())
local resGridCountRatio = gRsLodCombinerVals.resGridCountRatio
local resPropCountRatio = gRsLodCombinerVals.resPropCountRatio
local resTinyBlockCount = gRsLodCombinerVals.resTinyBlockCount
local resTinyBlockSize = gRsLodCombinerVals.resTinyBlockSize
local AreaZoneReason = "Area-Zone Resident"
local gridCountReason = "Area-count >" + (formattedPrint (100 * resGridCountRatio)) + "%"
local propCountReason = "Prop-count >" + (formattedPrint (100 * resPropCountRatio)) + "%"
local tinyBlockReason = "Tiny-block count >" + (formattedPrint resTinyBlockCount)
-- Collect list of all object-nodes used by ComboLodder for this map:
local mapObjItems = (getObjs info:True)
-- Clear current Resident data from refs:
(
mapObjItems.isResidentProp = False
residentRefNums.count = 0
residentDistLimits.count = 0
_residentReasons.count = 0
)
local timeStart = timeStamp()
pushPrompt "Organising Memory Resident props..."
-- List props-nodes per model (for lodding props)
local allObjsCount = 0
local usedRefNums = #()
local usedRefObjItems = #()
local usedRefGrids = #()
if doResidents do
(
for objItem in mapObjItems where (objItem.sourceHash != 0) do
(
local refNum = objItem.refNum
local findNum = findItem usedRefNums refNum
if (findNum == 0) do
(
append usedRefNums refNum
append usedRefObjItems #()
append usedRefGrids #()
findNum = usedRefNums.count
)
append usedRefObjItems[findNum] objItem
join usedRefGrids[findNum] (getPropGrids objItem)
allObjsCount += 1
)
)
-- Get resident-props list for Area zones intersecting with this map's bounds:
local areaResidents = #()
if doResidents do
(
-- Find bounding-box for map's comboloddy objects:
this.UpdateBounds updateGrpBounds:True
-- Get list of models made resident for Area-zones intersecting with this map:
areaResidents = gRsMemResidentLists.GetAreaPropsForBox boundsMin boundsMax
areaResidents = for propName in areaResidents collect (ToLower PropName)
)
-- Are any of this map's combolodder-prop on this list?
if (areaResidents.count != 0) do
(
local dlcPrefix = (ToLower gRsProject.assetPrefix.mapPrefix)
for idx = 1 to usedRefNums.count do
(
local RefNum = UsedRefNums[idx]
-- Get ref-data from RSref database:
local thisRef = rsRefData.refs[refNum]
local refName = (ToLower thisRef.name)
-- Add prefix if this is a DLC object-name,
-- because that's how it'll be in the residents-list metafile
if rSrefData.fileIsDLC[thisRef.maxfileNum] do
refName = (dlcPrefix + refName)
gRsProject.assetPrefix.mapPrefix
-- Is this model-name on the area-residents list?
if (FindItem AreaResidents RefName != 0) do
(
-- Mark this ref's objects as Resident if it's on the list:
local RefObjItems = usedRefObjItems[idx]
refObjItems.isResidentProp = True
residentRefNums[refNum] = True
_residentReasons[refNum] = areaZoneReason
)
)
)
-- Set models as resident if they're used by a certain ratio of map's props:
if doResidents and (resPropCountRatio > 0) do
(
for idx = 1 to usedRefNums.count do
(
local refNum = usedRefNums[idx]
-- No need to check models that have already been marked as Resident:
if (not residentRefNums[refNum]) do
(
local refObjItems = usedRefObjItems[idx]
local refObjRatio = (float refObjItems.count / allObjsCount)
if (refObjRatio > resPropCountRatio) do
(
refObjItems.isResidentProp = True
residentRefNums[refNum] = True
_residentReasons[refNum] = propCountReason
)
)
)
)
-- Set models as resident if they cover a certain ratio of map's grid-points:
if doResidents and (resGridCountRatio > 0) do
(
local allGridsCount
-- Find out total number of unique grid-points covered by lod-props:
(
local allGrids = #()
for idx = 1 to usedRefGrids.count do
(
-- Make sub-lists unique:
usedRefGrids[idx] = makeUniqueArray usedRefGrids[idx]
join allGrids usedRefGrids[idx]
)
allGridsCount = (makeUniqueArray allGrids).count
allGrids.count = 0
)
for idx = 1 to usedRefNums.count do
(
local refNum = usedRefNums[idx]
-- No need to check models that have already been marked as Resident:
if (not residentRefNums[refNum]) do
(
local refObjItems = usedRefObjItems[idx]
local refObjGrids = usedRefGrids[idx]
local thisRatio = (float refObjGrids.count / allGridsCount)
if (thisRatio > resGridCountRatio) do
(
refObjItems.isResidentProp = True
residentRefNums[refNum] = True
_residentReasons[refNum] = gridCountReason
)
)
)
)
-- These objects will be reblocked:
local reblockObjItems = #()
-- Set up list of objects that will need to be reblocked now:
-- Collect all objects that are marked as Resident, or are in Resident lod-blocks:
join reblockObjItems (for objItem in mapObjItems where objItem.isResidentProp or (objItem.GetParentData()).isResidentBlock collect objItem)
--reblockObjItems = makeUniqueArray reblockObjItems
popPrompt()
-- Find objects in mixed-name lod-blocks that shouldn't be so:
findMixedNameBlocks mapObjItems:mapObjItems reblockObjItems:reblockObjItems
-- Clear this array, we don't need it for now...
mapObjItems.count = 0
-------------------------------------
-- Reblock marked object-items --
-------------------------------------
this.ReblockObjLods reblockObjItems
reblockObjItems.count = 0
-------------------------------------
-------------------------------------------------------------------
-- Now find non-resident models that appear in a lot of --
-- one/two-prop blocks, and make those resident too: --
-------------------------------------------------------------------
if doResidents and (resTinyBlockCount > 0) do
(
-- Get new obj-items list, ignoring already-resident models:
local mapObjItems = for objItem in (getObjs info:True) where (not objItem.isResidentProp) collect objItem
local refsNumsUsed = #()
local refCounts = #()
local checkedLodBlocks = #()
local totalLodBlockCount = 0.0
for objItem in mapObjItems do
(
local parentBlock = (objItem.GetParentData())
if (findItem checkedLodBlocks parentBlock == 0) do
(
append checkedLodBlocks parentBlock
local lodObjItems = for lodObjItem in (parentBlock.getObjs info:True) where (lodObjItem.sourceHash != 0) collect lodObjItem
if (lodObjItems.count > 0) do
(
totalLodBlockCount += 1
)
-- Count models for blocks with 1 or two props:
if (lodObjItems.count <= resTinyBlockSize) do
(
local lodRefNums = #()
for objItem in lodObjItems do
(
local refDef = objItem.node.refDef
-- Only count a refDef once per block:
if (appendIfUnique lodRefNums refDef) do
(
local refNum = objItem.refNum
local findNum = findItem refsNumsUsed refNum
if (findNum == 0) do
(
append refsNumsUsed refNum
append refCounts (dataPair refNum:refNum count:0)
findNum = refsNumsUsed.count
)
refCounts[findNum].count += 1
)
)
)
)
)
checkedLodBlocks.count = 0
-- Find models that show in a lot of small lod-blocks, and set them to Resident:
local newResidentsCount = 0
for item in refCounts where (item.count > resTinyBlockCount) do
(
residentRefNums[item.refNum] = True
_residentReasons[item.refNum] = tinyBlockReason
newResidentsCount += 1
)
refCounts.count = 0
-- Set matching objects as Resident:
if (newResidentsCount != 0) do
(
local newResidents = for objItem in mapObjItems where (objItem.refNum != 0) and residentRefNums[objItem.refNum] collect objItem
newResidents.isResidentProp = True
)
mapObjItems.count = 0
)
----------------------------------------------------------------------
-- Do a final reblocking session,
-- if non-resident objects are found in Resident blocks:
----------------------------------------------------------------------
(
local mapObjItems = (getObjs info:True)
-- Setup up distance-limits for lod-distance index:
(
-- Reset object lodDistIdx values:
residentDistLimits.count = 0
mapObjItems.lodDistIdx = 0
-- Find lod-distances for all of map's Residents:
local lodDistVals = for objItem in mapObjItems where objItem.isResidentProp collect (objItem.getLodDistance())
-- Work out distance-limits, so as to avoid having lod-blocks with distance-ranges greater than maxDistRange
if (lodDistVals.count != 0) do
(
local maxDistRange = gRsLodCombinerVals.resMaxDistRange
local minLodDist = aMin lodDistVals
local maxLodDist = aMax lodDistVals
local lodDistRange = (maxLodDist - minLodDist)
if (lodDistRange > maxDistRange) do
(
local idxCount = ceil (lodDistRange / maxDistRange)
local distIncrement = lodDistRange / idxCount
-- Set distance-increments:
residentDistLimits = for lodDist = (minLodDist + distIncrement) to (maxLodDist - distIncrement) by distIncrement collect lodDist
)
)
-- Set lodDistIdx values for all Residents, using newly-collected residentDistLimits:
for objItem in mapObjItems where objItem.isResidentProp do
(
objItem.lodDistIdx = getPropLodDistIdx objItem.node
)
)
-- Collect objItems where object doesn't match parent's Resident flag, or its lodDistIdx doesn't match:
local reblockObjItems = for objItem in mapObjItems where
(
local isResidentProp = objItem.isResidentProp
local isResidentBlock = (objItem.GetParentData()).isResidentBlock
(isResidentProp != isResidentBlock) or (isResidentProp and (objItem.lodDistIdx != (objItem.GetParentData()).lodDistIdx))
) collect objItem
mapObjItems.count = 0
-- Assign objects to different lod-blocks if they don't match their current block-type:
if (reblockObjItems.count != 0) do
(
this.ReblockObjLods reblockObjItems
reblockObjItems.count = 0
)
)
return OK
),
-- Gets number lod-objects for each props-per-lod-group count...
fn SetPropsPerLodCounts =
(
_propsPerLodCounts = #()
for grp in _groupList do
(
local grpCounts = grp.GetPropsPerLodCounts()
for n = 1 to grpCounts.count do
(
local thisVal = grpCounts[n]
if (thisVal != undefined) do
(
if (_propsPerLodCounts[n] == undefined) do
(
_propsPerLodCounts[n] = 0
)
_propsPerLodCounts[n] += thisVal
)
)
)
return OK
),
-- Remove gridded subgroups if they have no objects:
fn RemoveEmptyGridBlocks =
(
for grpItem in _groupList do
grpItem.RemoveEmptyGridBlocks()
),
-- Update container's group-list sync values:
fn UpdateVals refreshObjVals:False removeEmpty:True =
(
local timeStart = timeStamp()
-- Update group-values:
for grp in _groupList do
grp.UpdateVals refreshObjVals:refreshObjVals
-- Used to track overall changes to sub-nodes:
local treeHashes = for item in _groupList collect item.treeHash
treeHash = getHashValue treeHashes 1
-- Find/organise Memory Resident meshes for map, reblocking if necessary:
this.SetupResidents()
-- Remove any empty lod-blocks:
if removeEmpty do
this.RemoveEmptyGridBlocks()
-- Set object-counts, used to properly update progress-bar during lod-generation:
this.SetLodLevelsCount()
-- Collect props-per-lod counts from subgroups, used to show 1-prop/2-prop block-values:
this.SetPropsPerLodCounts()
-- Update map's bounds:
this.UpdateBounds updateGrpBounds:False
-- Set lodSynced false if any subitems are false, or true if any are true.
-- Stays as undefined if none are specified.
lodsSynced = undefined
for item in _groupList where (item.lodsSynced != undefined) while (lodsSynced != false) do
(
lodsSynced = item.lodsSynced
)
),
-- Returns list of lod-objects generated by map's groups:
fn getLodObjs info:false recurse:true =
(
local lodItems = #()
if recurse do
(
for item in _groupList do
(
join lodItems (item.getLodObjs info:info recurse:true)
)
)
return lodItems
),
-- Used by viewport-draw callback to draw container's overlays:
fn viewportDraw selected:false tagItem: showBoxes:True showShapes:True =
(
if (_groupList != undefined) and (tagIdx != undefined) do
(
if (tagItem == unsupplied) do
(
tagItem = ::RsLodCombinerTool.dataTags[tagIdx]
)
if tagItem.selected do
(
selected = true
)
for item in _groupList do
(
item.viewportDraw showBoxes:showBoxes showShapes:showShapes -- selected:selected
)
)
),
-- Set filename for xml-file used to store Memory Resident object-list:
fn getResidentFilename =
(
if (mapName == undefined) then (return undefined) else
(
return (gRsMemResidentLists.GetMetafilePath() + mapName + ".meta")
)
),
-- Work out bounding-box for use in Memory Resident metafile:
fn getResidentBounds =
(
-- Get list of all sub-grps:
local subGrps = #()
Join subGrps (this.GetGroupList())
local grpNum = 0
while (grpNum < subGrps.count) do
(
grpNum += 1
join subGrps (subGrps[grpNum].GetGroupList())
)
-- Collect lod-objects for Resident Model lod-nodes:
local resLodObjs = #()
for grp in subGrps where grp.isResidentBlock do
(
join resLodObjs (grp.getLodObjs recurse:False)
)
subGrps.count = 0
-- Collect min/max values for lod-spheres:
local minMaxList = #()
for obj in resLodObjs do
(
local lodPos = obj.pos
local lodRadius = (getattr obj idxChildLodDistance)
local lodOffset = ([1,1,1] * lodRadius)
join minMaxList #((lodPos + lodOffset), (lodPos - lodOffset))
)
-- Find overall bounding-box:
local minVal = undefined
local maxVal = undefined
local success = RsGetBBox minMaxList &minVal &maxVal
-- If that failed, use node-bounds as fallback:
if not success do
(
minVal = boundsMin
maxVal = boundsMax
)
return (dataPair min:minVal max:maxVal)
),
-- Write Memory Resident objects-list out to xml:
fn storeResidents =
(
local xmlFilename = GetResidentFilename()
if (xmlFilename == undefined) do return False
if (RsIsFileReadOnly xmlFilename) do
(
format "Can't export Residents List, file is readonly: %\n" xmlFilename
return False
)
-- Get bounds for resident-models used in scene
local resBounds = GetResidentBounds()
-- Get list of prop-names, with prefix added for DLC-only propss
local propNames = #()
for refNum in residentRefNums do
(
local refDef = RsRefData.refs[refNum]
local propName = refDef.name
-- If this is a DLC prop, add the project-prefix to its name
if RSrefData.fileIsDLC[refdef.maxfileNum] do
propName = (gRsProject.assetPrefix.mapPrefix + propName)
AppendIfUnique propNames propName
)
-- Generate ResidentZone struct for export:
local ResidentZone = (RsMemRes_Zone ZoneName:MapName Filename:XmlFilename MinPos:ResBounds.Min MaxPos:ResBounds.Max PropNames:PropNames)
ResidentZone.Export()
),
-- Store groups to XML string in dataNode's paramblock:
fn store =
(
local timeStart = timeStamp()
format "Storing ComboLod data:\n..%\n" mapName
undo off
(
-- Delete store-node if it has nothing to store, or its container-node is invalid:
if (_groupList == undefined) or (_groupList.count == 0) or (not isValidNode contNode) then
(
if (isValidNode dataNode) do
(
delete dataNode
)
)
else
(
-- Create data-node, if struct doesn't have one already:
if (not isValidNode dataNode) do
(
local newPos = copy contNode.pos
newPos.Z -= 10
dataNode = RsGenericDataNode dataClass:"ComboLodder" name:(uniqueName "_ComboLodder_") isHidden:true isFrozen:true pos:newPos
-- Put datanode in correct container, and make sure it's in the default layer:
(LayerManager.GetLayer 0).addNode dataNode
contNode.addNodeToContent dataNode
)
-- Create xml-writer:
local stringBuilder = dotNetObject "System.Text.StringBuilder"
local stringWriter = dotNetObject "System.IO.StringWriter" stringBuilder
local xmlWriter = dotNetObject "System.Xml.XmlTextWriter" stringWriter
xmlWriter.Formatting = xmlWriter.Formatting.Indented
xmlWriter.Namespaces = False
local newObjsList = #()
-- Create root-node:
xmlWriter.WriteStartElement "root"
-- Store header-data node:
(
xmlWriter.WriteStartElement "info"
xmlWriter.WriteAttributeString "mapname" (contNode.name)
-- Only save this if non-default:
if (doResidents != (RsLodCombinerMap()).doResidents) do
(
xmlWriter.WriteAttributeString "doResidents" (doResidents as string)
)
-- Store current version-number:
xmlWriter.WriteAttributeString "version" (gRsLodCombinerVals.dataVersion as string)
-- Find and write out the number of group-nodes being saved, for use by progress-bar on loading:
local grpNum = 0
local allGrps = for item in _groupList collect item
while grpNum < allGrps.count do
(
grpNum += 1
join allGrps (allGrps[grpNum].GetGroupList())
)
allGrps = undefined
xmlWriter.WriteAttributeString "groupCount" (grpNum as string)
xmlWriter.WriteEndElement()
)
-- Store map's slod-groups:
(
for groupItem in _groupList do
(
groupItem.store xmlWriter objsList:newObjsList
)
)
-- Close root-node:
xmlWriter.WriteEndElement()
xmlWriter.close()
stringWriter.close()
-- Store data to paramblock:
dataNode.setNodeList newObjsList
dataNode.dataString = stringBuilder.ToString()
)
)
format "..[took %s]\n" ((timeStamp() - timeStart) / 1000.0)
),
-- Parses dataNode's xml-string, retrieving group-data:
fn restore &cancelled: =
(
_groupList = #()
if (not isValidNode dataNode) or (dataNode.dataString == undefined) do
(
return false
)
local timeStart = timeStamp()
format "Parsing ComboLod data:\n..%\n" mapName
-- Read xml-data from dataString string:
local textReader = dotNetObject "System.IO.StringReader" dataNode.dataString
local xmlReader = dotNetObject "System.Xml.XmlTextReader" textReader
pushPrompt ("Parsing ComboLod group-data: " + mapName)
local maxLodLevel = gRsLodCombinerVals.lodSuffixes.count
local readVersion = 0
-- Reconstitute group-data from xml:
xmlReader.Read()
local doInfoPrint = True
if (xmlReader.NodeType == xmlReader.NodeType.Element) do
(
-- Read first element:
xmlReader.Read()
local doProgress = False
local progressVal = 0
local progressMax = 0
local stopLoop = False
local blankLoopNum = 0
while (not stopLoop) do
(
local nodeType = xmlReader.NodeType
stopLoop = (nodeType == nodeType.EndElement) or (nodeType == nodeType.None) or (blankLoopNum == 2)
if not stopLoop do
(
case xmlReader.name of
(
-- Map/version data:
"info":
(
blankLoopNum = 0
local getVal = xmlReader.Item["version"]
if (getVal != undefined) do
(
readVersion = (getVal as number)
if (readVersion > gRsLodCombinerVals.VersionNum) do
(
local msg = stringStream ""
format "WARNING: %'s ComboLodder data was saved with a later version of the tool!\n\n" mapName to:msg
format "Your version: %\nSaved version: %\n" gRsLodCombinerVals.VersionNum readVersion to:msg
format "\nPlease update to latest Wildwest files to get latest version of ComboLodder tool!" mapName to:msg
messageBox (msg as string) title:"WARNING: Out-of-date ComboLodder tool"
)
)
local getVal = xmlReader.Item["groupCount"]
if (getVal != undefined) do
(
doProgress = True
progressMax = (getVal as number)
format "..[% group-nodes]\n" progressMax
progressStart ("Parsing ComboLodder data:")
)
local getVal = xmlReader.Item["doResidents"]
if (getVal != undefined) do
(
doResidents = (getVal as booleanClass)
)
)
-- SLOD/LOD-groups:
"group":
(
blankLoopNum = 0
if doInfoPrint do
(
doInfoPrint = false
if (readVersion != gRsLodCombinerVals.dataVersion) do
(
format "..[Loading older-version data: Data:% Tool:%]\n" readVersion gRsLodCombinerVals.dataVersion
)
)
local doReblock = False
local newGroup = RsLodCombinerGroup.restore xmlReader objsList:(dataNode.getNodeList()) parentData:This readVersion:readVersion progressVal:&progressVal progressMax:progressMax cancelled:&cancelled doReblock:&doReblock
if (newGroup != DontCollect) do
Append _groupList newGroup
)
"":
(
-- Used to avoid infinite-looping errors:
blankLoopNum += 1
if (blankLoopNum == 2) do
(
print "POTENTIAL INFINITE LOOP AVOIDED"
)
)
)
-- Read next element:
xmlReader.Read()
)
stopLoop = stopLoop or cancelled
)
if doProgress do
(
progressEnd(); pushPrompt ""; popPrompt()
)
)
xmlReader.Close()
textReader.Close()
if cancelled then
(
format "..[CANCELLED]\n"
_groupList.count = 0
)
else
(
-- Make sure that group-hierarchy matches their genLodLevel values:
pushPrompt "Correcting old data-tree hierarchies..."
RsLodCombinerGroup.treeLevelCorrector maxLodLevel grp:This
popPrompt()
this.RemoveEmptyGridBlocks()
local mapSlodGrps = for grp in (this.GetSubGroups recurse:True) where (grp.groupName != Undefined) collect grp
-- Clamp group-names that are too long:
if (mapName != undefined) do
(
local maxLength = (30 - mapName.count - 8)
pushPrompt "Renaming long-named groups..."
local longNameGrps = for grp in mapSlodGrps where (grp.groupName.count > maxLength ) collect grp
for grp in longNameGrps do
(
local newName = RsStripSuffixNums (subString grp.groupName 1 maxLength)
format "Renaming long-named group:\n %...\n" grp.groupName
grp.SetGrpNames #(grp) newName recurse:False
format " ...%\n" grp.groupName
)
popPrompt()
)
-- Fix any duplicated SLOD2/SLOD group-names
(
local usedGroupNames = #()
local renamed = False
for grp in mapSlodGrps where (grp.groupName != Undefined) do
(
local testName = grp.groupName + "|" + (grp.genLodLevel as String)
if (not AppendIfUnique usedGroupNames testName) do
(
local oldName = grp.ShowName()
-- Rename this lod-group
-- (this'll automatically pick a name with an unused index)
RsLodCombinerGroup.SetGrpNames #(grp) grp.groupName
local testName = grp.groupName + "|" + (grp.genLodLevel as String)
AppendIfUnique usedGroupNames testName
if (not renamed) do
Format "\nRenaming non-unique groups:\n"
renamed = True
Format " % => %\n " oldName (grp.ShowName())
)
)
)
)
popPrompt()
format "..[took %s]\n" ((timeStamp() - timeStart) / 1000.0)
return (not cancelled)
),
-- Remove objects from map's groups:
fn RemoveObjs remObjs removed:#{} checkAll:true removeEmpty:true updateBnds:True =
(
local remCount = removed.numberSet
for grpInfo in _groupList while checkAll or (removed.numberSet != remObjs.count) do
grpInfo.RemoveObjs remObjs removed:removed checkAll:checkAll removeEmpty:removeEmpty updateBnds:False
-- Reset bounds-data if any objects were removed from group or subgroups
if updateBnds and (removed.numberSet != remCount) do
this.UpdateBounds updateGrpBounds:True
return OK
),
-- Add new group(s) to map:
fn createGroups objLists doRemove:true parentData: grpName: =
(
-- Reconstitute groupList from dataNode if needed:
if (_groupList == undefined) do
this.Restore()
-- Get default group-name from map-struct:
if (grpName == unsupplied) do
(
grpName = groupName
)
local grpParentData = if (parentData == unsupplied) then This else parentData
local tempGrp = RsLodCombinerGroup()
tempGrp.CreateGroups objLists grpName:grpName grpParent:This doRemove:doRemove
)
)
-- Restart tool UI with new data-definitions, if open:
if (RsLodCombinerTool != undefined) and (RsLodCombinerTool.open) do
(
--RsLodCombinerTool.allowStore = False
RsLodCombinerTool.create()
)
if FALSE do
(
-- Tests restore-function on selected data-node:
local cancelled = false
(RsLodCombinerMap mapName:$.parent.name contNode:$.parent dataNode:$).restore cancelled:&cancelled
)