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