-- Memory Resident Zones: -- Functions for building/exporting/loading files for setting up memory-resident model-lists -- -- Neal D Corbett (R* Leeds) 04/2014 -- Struct will be instanced to this global: global gRsMemResidentLists = undefined struct RsMemResidentLists ( -- Used to tag metafiles with format-version: VersionNum = 1, -- Exported prop-lists will a maximum of this many model-names: MaxPropCount = 200, -- Area-zone metafiles are given this prefix: AreaFilePrefix = "AreaZone_", -- Data loaded from scenexmls will be cached to here: SceneXmlCachePath = (RsMakeSafeSlashes ((RsConfigGetStreamDir core:True) + "/MemResCache/")), -- Returns the path where Memory Resident lists are stored fn GetMetaFilePath dlc: = ( if (dlc == Unsupplied) do dlc = RsIsDLCProj() return (RsMakeSafePath (RsConfigGetCommonDir core:(not dlc) + "/data/levels/" + (RsConfigGetProjectName core:(not dlc)) + "/resident/")) ), -- Returns folder that current project is exported to fn GetProjExportRoot dlc: = ( if (dlc == Unsupplied) do dlc = RsIsDLCProj() return (RsMakeSafePath ((RsConfigGetExportLevelsDir core:(not dlc)) + ((RsConfigGetProjectName core:(not dlc))) )) ), ------------------------------------------------------------------------------------------ -- Returns list of Xml-paths for Exterior maps: ------------------------------------------------------------------------------------------ fn GetMapXmlPaths dlc: = ( -- Get content-tree nodes for all scenes in current project: local mapFileNodes = RsContentTree.ContentTreeHelper.GetAllMapExportNodes() local projExportRoot = this.GetProjExportRoot dlc:dlc -- Get list of map-metafiles: local MapXmlPaths = for mapMaxNode in mapFileNodes collect ( local metafilepath = dontCollect -- Only read certain maptypes: local mapType = (RsContentTree.GetMapType mapMaxNode) as name if (mapType == #map_container) do ( local ExportZipNode = (RsContentTree.GetMapExportZipNode MapMaxNode) local ProcessedZipNode = (RsContentTree.GetMapExportZipNode ExportZipNode) if (ProcessedZipNode != undefined) do ( local XmlNode = RsContentTree.GetMapExportSceneXMLNode ExportZipNode local XmlPath = XmlNode.AbsolutePath --if (not matchPattern XmlPath pattern:"*_prologue*") do ( -- Find relative path from ProjExportRoot to this xml-file: local XmlRelPath = pathConfig.convertPathToRelativeTo XmlPath ProjExportRoot -- This pattern matches if xml is under ProjExportRoot, and the xml-file actually exists: if (matchPattern XmlRelPath pattern:".\\*") and (doesFileExist XmlPath) do ( -- Collect matching xml: metafilepath = XmlPath ) ) ) ) metafilepath ) return MapXmlPaths ), ------------------------------------------------------------------------------------------ -- Load per-map position-lists for each prop used in game: ------------------------------------------------------------------------------------------ fn LoadPropData force:False dlc: = ( if (dlc == Unsupplied) do dlc = RsIsDLCProj() -- Get list of 'Exterior' map-xmls: local mapXmlPaths = this.GetMapXmlPaths() if (mapXmlPaths.Count == 0) do return #() PushPrompt "Checking Perforce for updated XMLs..." local ChangedXmls = gRsPerforce.ChangedFiles MapXmlPaths PopPrompt() -- Sync latest xml-files: if (ChangedXmls.Count != 0) do ( local queryText = stringstream "" local singular = (ChangedXmls.count == 1) format "% a newer version on Perforce.\n\nWould you like to sync % now?" (if singular then "This file has" else "These files have") (if singular then "it" else "them") to:queryText local SyncChecked = #{1..ChangedXmls.Count} local queryVal = RsQueryBoxMultiBtn (queryText as string) title:"Updated RSref-files found on Perforce:" timeout:15 width:540 \ labels:#("Yes", "No", "Cancel Process") defaultBtn:2 cancelBtn:3 \ tooltips:#("Update SceneXml files from Perforce", "Don't update SceneXml files from Perforce", "Cancel load/generation process") \ listCheckStates:SyncChecked listItems:ChangedXmls listTitles:#("Sync", "Filename") case queryVal of ( -- Sync checked filenames: 1: ( local SyncFiles = for n = SyncChecked collect ChangedXmls[n] if (SyncFiles.Count != 0) do ( local success = gRsPerforce.Sync ChangedXmls clobberAsk:True silent:True showProgress:True progressTitle:"Syncing exterior-map sceneXml files..." -- Abort if sync was cancelled: if (not success) do return False ) ) -- Abort if 'Cancel' was pressed: 3:(return False) ) ) local MapDataList -- Load scenexml data (or cachefiles if valid) ( local TimeStart = TimeStamp() local Success = True ProgressStart "Loading Prop Positions..." local FileCount = MapXmlPaths.Count local MapDataList = for n = 1 to FileCount while (Success = ProgressUpdate (100.0 * n / FileCount)) collect ( local XmlPath = MapXmlPaths[n] ::RsMemRes_MapData Filename:XmlPath isDlc:dlc ) ProgressEnd() -- Return False if load was cancelled: if (not Success) do return False ) format "Time taken to parse prop-positions: %s\n" (FormattedPrint ((TimeStamp() - TimeStart) / (1000.0)) Format:".3g") return MapDataList ), ------------------------------------------------------------------------------------------ -- Returns wildcard-descriptor for area-zone metafiles: ------------------------------------------------------------------------------------------ fn GetAreaZoneMetaPath dlc: = ( (this.GetMetafilePath dlc:dlc) + this.areaFilePrefix + "*.meta" ), ------------------------------------------------------------------------------------------ -- Get position-lists for each map-area: ------------------------------------------------------------------------------------------ fn GetPropsByArea = ( -- Load prop-position data from SceneXmls: local mapDataList = this.LoadPropData() if (not IsKindOf mapDataList Array) do return False if (mapDataList.count == 0) do return #() PushPrompt "Compiling props per area..." local AreaNames = #() local AreaDataList = #() struct MapAreaData (AreaName, PropNames = #(), PropPositions = #()) for MapItem in MapDataList do ( local MapArea = MapItem.AreaName local AreaIdx = FindItem AreaNames MapItem.AreaName if (AreaIdx == 0) do ( append AreaNames MapItem.AreaName append AreaDataList (MapAreaData AreaName:MapItem.AreaName) AreaIdx = AreaNames.Count ) local AreaPropNames = AreaDataList[AreaIdx].PropNames local AreaPropPositions = AreaDataList[AreaIdx].PropPositions local MapPropNames = MapItem.PropNames local MapPropPositions = MapItem.PropPositions for n = 1 to MapPropNames.Count do ( local PropIdx = FindItem AreaPropNames MapPropNames[n] if (PropIdx == 0) do ( append AreaPropNames MapPropNames[n] append AreaPropPositions #() PropIdx = AreaPropNames.Count ) join AreaPropPositions[PropIdx] MapPropPositions[n] ) ) PopPrompt() return AreaDataList ), ------------------------------------------------------------------------------------------ -- Update Memory Resident area-zone metafiles, based on props found within: ------------------------------------------------------------------------------------------ fn UpdateAreaZones dlc: = ( -- Load prop-position data from SceneXmls: local mapDataList = this.LoadPropData() if (not IsKindOf mapDataList Array) do return False if (mapDataList.count == 0) do return #() -- Compile master-list of prop-positions: PushPrompt "Compiling prop-position lists..." local PropNames = #() local PropPositions = #() for MapData in MapDataList do ( for MapPropIdx = 1 to MapData.PropNames.Count do ( local PropName = MapData.PropNames[MapPropIdx] local PropIdx = FindItem PropNames PropName if (PropIdx == 0) do ( append PropNames PropName append PropPositions #() PropIdx = PropNames.Count ) join PropPositions[PropIdx] MapData.PropPositions[MapPropIdx] ) ) local PropDataList = for n = 1 to PropNames.Count collect (DataPair Name:PropNames[n] Positions:PropPositions[n]) PropNames.Count = 0 PropPositions.Count = 0 PopPrompt() local metafilePath = (this.GetAreaZoneMetaPath dlc:dlc) -- Sync latest Area-zone metafiles: gRsPerforce.SyncChanged #(MetafilePath) clobberAsk:False silent:True -- Checkout area-zone metafiles: local AreaZoneFiles = GetFiles MetafilePath gRsPerforce.Add_Or_Edit AreaZoneFiles -- Import metafile boxes: -- (don't both loading model-lists) local AreaZones = for Filename in AreaZoneFiles collect ( local LoadedArea = RsMemRes_Zone.Import Filename Models:False if (LoadedArea == undefined) then DontCollect else LoadedArea ) local AreaPropCounts = for AreaZone in AreaZones collect ( DataPair Zone:AreaZone Props:#() ) -- Run through each prop-list in turn, work out which prop is in each area, and how many of each: for ThisProp in PropDataList do ( local PropName = ThisProp.Name for AreaItem in AreaPropCounts do ( local AreaPropItem = (DataPair Name:PropName Count:0) append AreaItem.Props AreaPropItem local ThisZone = AreaItem.Zone -- Check all positions to see if they're in the box: for ThisPos in ThisProp.Positions do ( if (ThisZone.IsInsideBox ThisPos) do ( AreaPropItem.Count += 1 ) ) ) ) fn Sorter v1 v2 = (v2.Count - v1.Count) -- Update area-metafiles: for AreaItem in AreaPropCounts do ( -- Filter out props with fewer than minimum number of instances in area: AreaItem.Props = for PropItem in AreaItem.Props where (PropItem.Count > 1) collect PropItem -- Sort props in ascending order of instance-counts: qsort AreaItem.Props Sorter -- Cut list down to maximum-allowed length: if (AreaItem.Props.Count > MaxPropCount) do ( AreaItem.Props = for n = 1 to MaxPropCount collect ( AreaItem.Props[n] ) ) format "%'s residents-list has % model-names:\n" AreaItem.Zone.ZoneName AreaItem.Props.Count for PropItem in AreaItem.Props do ( format " %: %\n" PropItem.Name PropItem.Count ) -- Update zone's propname-list: AreaItem.Zone.PropNames = for PropItem in AreaItem.Props collect PropItem.Name ) -- Re-export area-metafiles with their updated model-lists: for AreaItem in AreaPropCounts do ( AreaItem.Zone.Export() ) return AreaPropCounts ), ------------------------------------------------------------------------------------------ -- Finds exported Area zones that intersect BoxMin/BoxMax, -- and returns list of resident models combined from each zone found: ------------------------------------------------------------------------------------------ fn GetAreaPropsForBox BoxMin BoxMax = ( PushPrompt "Memory Residents: Searching for intersecting Area zones..." -- Sync Area zone-metafiles: local CheckPath = (GetAreaZoneMetaPath()) gRsPerforce.SyncChanged #(CheckPath) clobberAsk:True silent:True showProgress:True progressTitle:"Syncing Area Resident Lists..." -- Get list of metafiles, and import zones: local LoadFiles = (getFiles CheckPath) local AreaZones = for ThisFilename in LoadFiles collect (::RsMemRes_Zone.Import ThisFilename) local PropNames = #() for ThisZone in AreaZones where (ThisZone != undefined) do ( local BoxIntersects = True -- Compare x/y/z values: for n = 1 to 3 while BoxIntersects do ( MinA = BoxMin[n] MaxA = BoxMax[n] MinB = ThisZone.MinPos[n] MaxB = ThisZone.MaxPos[n] -- False if boxes don't coincide on this axis: BoxIntersects = not ((MaxA < MinB) or (MinA > MaxB)) ) -- Collect props from intersecting area-zone: if BoxIntersects do ( local LogMsg = StringStream "" format "Box intersects area-zone: % (% resident models)" ThisZone.ZoneName ThisZone.PropNames.Count To:LogMsg gRsUlog.LogMessage (LogMsg as String) context:"MemoryResidents" join PropNames ThisZone.PropNames ) ) -- Remove duplicate prop-names: PropNames = (MakeUniqueArray PropNames) PopPrompt() gRsUlog.LogMessage ("PropNames loaded from Area-Zone Memory Resident lists: " + (PropNames.Count as String)) context:"MemoryResidents" return PropNames ) ) ------------------------------------------------------------------------------------------ -- Defines information on props used in a particular map-scene: -- (Must be instantiated with 'Filename' xml-path argument) ------------------------------------------------------------------------------------------ struct RsMemRes_MapData ( PropNames = #(), PropPositions = #(), Filename, AreaName, FileHash = 0, isDlc = False, fn GetCacheFilename = ( gRsMemResidentLists.SceneXmlCachePath + (GetFilenameFile Filename) + ".txt" ), ------------------------------------------------------------------------------------------ -- Store data to XML cache-file: ------------------------------------------------------------------------------------------ fn StoreDataCache = ( makeDir gRsMemResidentLists.SceneXmlCachePath local CacheFilename = (GetCacheFilename()) if (DoesFileExist CacheFilename) do (SetFileAttribute CacheFilename #readOnly False) local SaveStream = stringStream "" -- Store basic details: file-hash, source-filename, and version-number: format "%\n%\nVersion:%\n" FileHash Filename gRsMemResidentLists.VersionNum to:SaveStream -- Store names and positions of each prop: for n = 1 to PropNames.Count do ( local PropName = PropNames[n] local PosList = PropPositions[n] format "%\n" PropName to:SaveStream for ThisPos in PosList do ( format "%\n" ThisPos to:SaveStream ) ) try ( local Fstream = CreateFile CacheFilename format "%" (SaveStream as string) to:Fstream close Fstream gRsUlog.LogMessage ("Successfully saved Memory Resident list: " + CacheFilename) context:"MemoryResidents" ) catch ( gRsUlog.LogWarning ("Failed to save Memory Resident list! " + CacheFilename) context:"MemoryResidents" messageBox ("Failed to save Memory Resident list!\n\n" + CacheFilename) title:"XML file-save failure" ) ), ------------------------------------------------------------------------------------------ -- Reload pre-processed data, if found: ------------------------------------------------------------------------------------------ fn LoadCachedData = ( local CacheFilename = (GetCacheFilename()) if (not DoesFileExist CacheFilename) do return False -- Reset lists: PropNames.Count = 0 PropPositions.Count = 0 local FStream = openFile CacheFilename local CachedHash = readLine FStream -- Return if cached hash doesn't match file: if (FileHash != CachedHash) do ( return False ) -- Skip sourcefile-path and cache's version-number, they're just intended for human debugging: skipToNextLine FStream skipToNextLine FStream -- Return if file has no props named: if (eof FStream) do return True -- Get first prop-name... local PropName = readLine FStream -- Loop through the rest of the file: while (not eof FStream) do ( append PropNames PropName local ThisPropPositions = #() append PropPositions ThisPropPositions PropName = undefined while (not eof Fstream) and (PropName == undefined) do ( local ThisLine = readLine Fstream -- This line will either be a position for the current propname, or will be the next propname: if (ThisLine[1] == "[") then ( append ThisPropPositions (execute ThisLine) ) else ( PropName = ThisLine ) ) ) close FStream return True ), ------------------------------------------------------------------------------------------ -- Load prop-positions for 'Filename' ------------------------------------------------------------------------------------------ fn LoadPropsFromXml = ( -- Reset lists: PropNames.Count = 0 PropPositions.Count = 0 -- Get hashvalue from file-data/version: FileHash = (GetHashValue (GetFileModDate Filename) gRsMemResidentLists.VersionNum) as String -- Attempt to load cached prop-list for this map: local ValidCache = LoadCachedData() -- Return if cache was valid: if ValidCache do return True -- If not, load the map's latest scenexml: local ObjectsOnly = (dotNetClass "RSG.SceneXml.LoadOptions").Objects local SceneXml = dotnetobject "RSG.SceneXml.Scene" Filename ObjectsOnly True -- Get flat list of scene's object-elements: local SceneObjs = #() join SceneObjs SceneXml.Objects for ThisObj in SceneXml.Objects do ( join SceneObjs (ThisObj.GetChildrenRecursive()) ) -- Filter out non-ref objects: local RefObjs = for Obj in SceneObjs where (Obj.IsRefObject() and (not Obj.DontExport())) collect Obj -- Collect positions of suitable props: for Obj in RefObjs do ( local ObjName = ToLower (Obj.GetObjectName()) local PropNum = findItem PropNames ObjName if (PropNum == 0) do ( append PropNames ObjName append PropPositions #() PropNum = PropNames.Count ) ( -- Get prop's position: local ObjXformPos = Obj.ObjectTransform.D local ObjPos = [ObjXformPos.X, ObjXformPos.Y, ObjXformPos.Z] append PropPositions[PropNum] ObjPos ) --format "%: %\n" ObjName ObjPos ) -- Store newly-loaded data to cache-file: StoreDataCache() return True ), ------------------------------------------------------------------------------------------ -- Automatically load data from Filename when this struct is instanced: ------------------------------------------------------------------------------------------ on Create do ( if (Filename == undefined) or (not DoesFileExist Filename) do return False -- Get AreaName from name of scenexml's off-the-root parent-folder: ( -- Find xml's path relative to Project Export Root: -- ('GetMapXmlPaths' will have already filtered path to ensure it's under here) local projExportRoot = RsMemResidentLists.GetProjExportRoot dlc:this.isDlc local XmlRelPath = (pathConfig.convertPathToRelativeTo Filename ProjExportRoot) AreaName = pathConfig.stripPathToTopParent (pathConfig.removePathTopParent XmlRelPath) ) LoadPropsFromXml() ) ) ------------------------------------------------------------------------------------------ -- Defines a Memory Resident Props zone, which is used to export/import data: ------------------------------------------------------------------------------------------ struct RsMemRes_Zone ( ZoneName, -- Name of zone Filename, -- Filename for zone's metafile PropNames = #(), -- Names of models that will be made memory-resident when player is in this zone MinPos, MaxPos, -- Zone's bounding-box ------------------------------------------------------------------------------------------ -- Returns 'True' if 'Pos' is within zone-box: ------------------------------------------------------------------------------------------ fn IsInsideBox Pos = ( local IsInside = True -- Test x/y/z values: for n = 1 to 3 while IsInside do ( IsInside = not ((Pos[n] < MinPos[n]) or (Pos[n] > MaxPos[n])) ) return IsInside ), ------------------------------------------------------------------------------------------ -- Create box-object to show area-zone: ------------------------------------------------------------------------------------------ fn CreateBoxObj = ( -- Set random-seed based on ZoneName, so boxes with same name will be same colour: Seed (GetHashValue ZoneName 1) local Clr = RsGetRandomColour() local BoxSize = (MaxPos - MinPos) local BoxPos = minPos + ([BoxSize.X, BoxSize.Y, 0] / 2) Box name:ZoneName pos:BoxPos width:BoxSize.X length:BoxSize.Y height:BoxSize.Z WireColor:Clr ), ------------------------------------------------------------------------------------------ -- Initialise this zone-descriptor with values taken from 'MapAreaData' struct: ------------------------------------------------------------------------------------------ fn InitAreaBlock AreaData dlc:False = ( -- Generate zone-name and metafile-export path from area-name: ZoneName = (gRsMemResidentLists.AreaFilePrefix + AreaData.AreaName) Filename = (gRsMemResidentLists.GetMetafilePath dlc:dlc + ZoneName + ".meta") -- Pair off the prop-names/positions for models with enough instances: local AreaPropNames = AreaData.PropNames local AreaPropPositions = AreaData.PropPositions local AreaProps = for n = 1 to AreaPropNames.Count collect ( if (AreaPropPositions[n].Count < gRsMemResidentLists.MinInstances) then DontCollect else ( DataPair Name:AreaPropNames[n] Positions:AreaPropPositions[n] ) ) -- Sort props in ascending order of instance-counts: fn Sorter v1 v2 = (v2.Positions.Count - v1.Positions.Count) qsort AreaProps Sorter format "%: % props\n" ZoneName AreaProps.Count for item in AreaProps do ( --format " %:%\n" Item.Name Item.Positions.Count ) -- Find bounding-box for zone: if (AreaProps.Count != 0) do ( local PropPositions = #() for Item in AreaProps do ( append PropNames Item.Name join PropPositions Item.Positions ) RsGetBBox PropPositions &MinPos &MaxPos -- Apply padding: local Padding = 300 MinPos -= Padding MaxPos += Padding --CreateBoxObj() ) return OK ), ------------------------------------------------------------------------------------------ -- Export this memory-resident zone to metafile: ------------------------------------------------------------------------------------------ fn Export = ( gRsUlog.LogMessage ("Storing Memory Resident list: " + Filename) context:"MemoryResidents" quiet:False print This -- Init xml doc: local xmlDoc = XmlDocument() xmlDoc.init() local xmlRoot = xmlDoc.createElement "CZonedAssets" appendTo:xmlDoc.Document local xmlElem = xmlDoc.createElement "Name" appendTo:xmlRoot xmlElem.innerText = ZoneName -- Add map-extents: ( local extentsElem = xmlDoc.createElement "Extents" appendTo:xmlRoot local xmlElem = RsCreateXmlElement "min" #(dataPair name:"x" value:(MinPos.X), dataPair name:"y" value:(MinPos.Y), dataPair name:"z" value:(MinPos.Z)) xmlDoc extentsElem.AppendChild(xmlElem) local xmlElem = RsCreateXmlElement "max" #(dataPair name:"x" value:(MaxPos.X), dataPair name:"y" value:(MaxPos.Y), dataPair name:"z" value:(MaxPos.Z)) xmlDoc extentsElem.AppendChild(xmlElem) ) -- Add model-names: local filesElem = xmlDoc.createElement "Models" appendTo:xmlRoot for PropName in PropNames do ( local xmlElem = xmlDoc.createElement "Item" appendTo:filesElem xmlElem.innerText = PropName ) -- Set version-number: local xmlElem = RsCreateXmlElement "Version" #(dataPair name:"value" value:gRsMemResidentLists.VersionNum) xmlDoc xmlRoot.AppendChild(xmlElem) try ( xmlDoc.save Filename gRsUlog.LogMessage ("Successfully saved Memory Resident list " + Filename) context:"MemoryResidents" ) catch ( gRsUlog.LogWarning ("Failed to save Memory Resident list! " + Filename) context:"MemoryResidents" messageBox ("Failed to save Memory Resident list!\n\n" + Filename) title:"XML file-save failure" ) ), ------------------------------------------------------------------------------------------ -- Import 'RsMemRes_Zone' instance from a metafile: ------------------------------------------------------------------------------------------ fn Import ImportFilename Models:True = ( if (not DoesFileExist ImportFilename) do return undefined local NewZone = RsMemRes_Zone Filename:ImportFilename try ( local XmlReader = dotNetObject "System.Xml.XmlTextReader" ImportFilename while (XmlReader.Read()) do ( if (XmlReader.IsStartElement()) do ( case (XmlReader.Name as Name) of ( #Item: ( if Models do ( XmlReader.Read() local PropName = XmlReader.Value append NewZone.PropNames PropName ) ) #Name: ( XmlReader.Read() NewZone.ZoneName = XmlReader.Value ) #Min: ( NewZone.MinPos = [0,0,0] for n = 1 to 3 do ( local Val = XmlReader.GetAttribute (n - 1) NewZone.MinPos[n] = (Val as Float) ) ) #Max: ( NewZone.MaxPos = [0,0,0] for n = 1 to 3 do ( local Val = XmlReader.GetAttribute (n - 1) NewZone.MaxPos[n] = (Val as Float) ) ) ) ) ) XmlReader.Close() ) catch ( return undefined ) -- Return undefined if certain values weren't loaded: if (NewZone.ZoneName == undefined) or (NewZone.MinPos == undefined) or (NewZone.MaxPos == undefined) do ( NewZone = undefined ) return NewZone ) ) gRsMemResidentLists = RsMemResidentLists() --gRsMemResidentLists.GetAreaPropsForBox [0,0,0] [100,100,100] --global blah = gRsMemResidentLists.UpdateAreaZones()