Difference between revisions of "Arcane University:World space synchronization"
Line 46: | Line 46: | ||
=== References === | === References === | ||
+ | |||
+ | The process with references will be to first make a plugin that just contains overrides of all the references that need copying (without actually changing anything yet), then turning those into new references, and finally applying a script to move them to the destination world space. | ||
+ | |||
+ | Load the origin esm (you don't need to load the destination esm yet at this point). To filter for references within the rectangular area, apply the following custom script; it doesn't matter on what, as it will check all records across all currently loaded files (feel free to improve the script and make it faster). Before applying, edit the values of xmin, xmax, ymin and ymax inside the Initizalize function to match the unit coordinates of your rectangle (again, feel free to improve in order to make it interactive). The script is a crude mishmash of code by km816 and Zilav. | ||
+ | |||
+ | <nowiki>unit FilterByPosition; | ||
+ | |||
+ | var | ||
+ | xmin, xmax, ymin, ymax: float; | ||
+ | |||
+ | |||
+ | function Filter(e: IInterface): Boolean; | ||
+ | var | ||
+ | stat: IInterface; | ||
+ | xp, yp: float; | ||
+ | begin | ||
+ | |||
+ | if GetIsDeleted(e) then exit; | ||
+ | |||
+ | if (Signature(e)<>'REFR') and (Signature(e)<>'ACHR') then exit; | ||
+ | |||
+ | xp := StrToFloat(GetElementEditValues(e,'DATA\Position\X')); | ||
+ | if (xp<xmin) or (xp>xmax) then exit; | ||
+ | |||
+ | yp := StrToFloat(GetElementEditValues(e,'DATA\Position\Y')); | ||
+ | if (yp<ymin) or (yp>ymax) then exit; | ||
+ | |||
+ | Result := True; | ||
+ | end; | ||
+ | |||
+ | function Initialize: Integer; | ||
+ | begin | ||
+ | |||
+ | xmin := 0; | ||
+ | xmax := 4096; | ||
+ | ymin := 0; | ||
+ | ymax := 4096; | ||
+ | |||
+ | |||
+ | FilterConflictAll := False; | ||
+ | FilterConflictThis := False; | ||
+ | FilterByInjectStatus := False; | ||
+ | FilterInjectStatus := False; | ||
+ | FilterByNotReachableStatus := False; | ||
+ | FilterNotReachableStatus := False; | ||
+ | FilterByReferencesInjectedStatus := False; | ||
+ | FilterReferencesInjectedStatus := False; | ||
+ | FilterByEditorID := False; | ||
+ | FilterEditorID := ''; | ||
+ | FilterByName := False; | ||
+ | FilterName := ''; | ||
+ | FilterByBaseEditorID := False; | ||
+ | FilterBaseEditorID := ''; | ||
+ | FilterByBaseName := False; | ||
+ | FilterBaseName := ''; | ||
+ | FilterScaledActors := False; | ||
+ | FilterByPersistent := False; | ||
+ | FilterPersistent := False; | ||
+ | FilterUnnecessaryPersistent := False; | ||
+ | FilterMasterIsTemporary := False; | ||
+ | FilterIsMaster := False; | ||
+ | FilterPersistentPosChanged := False; | ||
+ | FilterDeleted := False; | ||
+ | FilterByVWD := False; | ||
+ | FilterVWD := False; | ||
+ | FilterByHasVWDMesh := False; | ||
+ | FilterHasVWDMesh := False; | ||
+ | FilterBySignature := True; // | ||
+ | FilterSignatures := 'REFR'; // refs only | ||
+ | FilterByBaseSignature := False; | ||
+ | FilterBaseSignatures := ''; | ||
+ | FlattenBlocks := True; | ||
+ | FlattenCellChilds := True; | ||
+ | AssignPersWrldChild := False; | ||
+ | InheritConflictByParent := False; // color conflicts | ||
+ | FilterScripted := True; // use custom Filter() function | ||
+ | |||
+ | ApplyFilter; | ||
+ | Result := 1; | ||
+ | end; | ||
+ | |||
+ | end.</nowiki> | ||
+ | |||
+ | Alt+Click the expand icon next to the world space, select one reference, press Ctrl+Alt to select all references in the world space, and "Copy as override into" a new plugin. This may take a while. If you have more rectangles, then right click and Remove filter, and run the script again. This time, "copy as override" into the plugin you previously created for convenience's sake. | ||
+ | |||
+ | '''This paragraph pertains to copying references for LOD only.''' Areas that are far enough away from the playable border to never be in the loaded area, may still be visible from a distance and thus will have to be copied over to be included in LOD that is generated for the destination world space. Here, you may like to exclude references that won't be visible in LOD. Once you have your .esp from the previous step, apply a Filter for Base record signature "Tree" (shrubs and other small flora will also be lumped in). Copy these as override from your previous .esp into a new one. Remove the filter. Then do another filter by running the below script by Zilav, which retains only references whose base object has LOD meshes configured. Copy all remaining references from the same previous .esp into the same second .esp you created. You may now discard the first .esp and continue with the second one. | ||
+ | |||
+ | <nowiki>unit ApplyCustomScripted; | ||
+ | |||
+ | function Filter(e: IInterface): Boolean; | ||
+ | var | ||
+ | stat: IInterface; | ||
+ | begin | ||
+ | stat := WinningOverride(BaseRecord(e)); | ||
+ | if Signature(stat) = 'STAT' then | ||
+ | Result := ElementExists(stat, 'MNAM'); | ||
+ | end; | ||
+ | |||
+ | function Initialize: Integer; | ||
+ | begin | ||
+ | FilterConflictAll := False; | ||
+ | FilterConflictThis := False; | ||
+ | FilterByInjectStatus := False; | ||
+ | FilterInjectStatus := False; | ||
+ | FilterByNotReachableStatus := False; | ||
+ | FilterNotReachableStatus := False; | ||
+ | FilterByReferencesInjectedStatus := False; | ||
+ | FilterReferencesInjectedStatus := False; | ||
+ | FilterByEditorID := False; | ||
+ | FilterEditorID := ''; | ||
+ | FilterByName := False; | ||
+ | FilterName := ''; | ||
+ | FilterByBaseEditorID := False; | ||
+ | FilterBaseEditorID := ''; | ||
+ | FilterByBaseName := False; | ||
+ | FilterBaseName := ''; | ||
+ | FilterScaledActors := False; | ||
+ | FilterByPersistent := False; | ||
+ | FilterPersistent := False; | ||
+ | FilterUnnecessaryPersistent := False; | ||
+ | FilterMasterIsTemporary := False; | ||
+ | FilterIsMaster := False; | ||
+ | FilterPersistentPosChanged := False; | ||
+ | FilterDeleted := False; | ||
+ | FilterByVWD := False; | ||
+ | FilterVWD := False; | ||
+ | FilterByHasVWDMesh := False; | ||
+ | FilterHasVWDMesh := False; | ||
+ | FilterBySignature := True; // | ||
+ | FilterSignatures := 'REFR'; // refs only | ||
+ | FilterByBaseSignature := False; | ||
+ | FilterBaseSignatures := ''; | ||
+ | FlattenBlocks := False; | ||
+ | FlattenCellChilds := False; | ||
+ | AssignPersWrldChild := False; | ||
+ | InheritConflictByParent := False; // color conflicts | ||
+ | FilterScripted := True; // use custom Filter() function | ||
+ | |||
+ | ApplyFilter; | ||
+ | Result := 1; | ||
+ | end; | ||
+ | |||
+ | end.</nowiki> | ||
+ | |||
+ | Once your patch .esp has all the needed references from all rectangles, save and close xEdit and make a backup. Load it again, apply a filter that only does "Flatten blocks/sub-blocks" and "flatten cell children", select all references in the plugin using the Alt+Click/Ctrl+Click method, right click, and "Change FormID". Select the current plugin. This will change all the overrides into new references, but it will take some time. | ||
+ | |||
+ | Save, close, backup, and reload, this time including the destination esm. Run the same filter to flatten blocks and cell children, select all references in the patch plugin, right click, and apply the built-in script "Worldspace move references into another worldspace". Indicate the destination world space; check "Move persistent references" and "only from current plugin". Then enter the offset values expressed in units, which you should've calculated earlier. | ||
+ | |||
+ | After the script is done, your plugin should be ready, unless there were references whose base object comes from the origin esm. In that case, your plugin will depend on both esms and must be tweaked and cleaned before it can be merged into the destination esm. | ||
=== Final checks === | === Final checks === |
Revision as of 18:18, 4 November 2021
In this article, world space synchronization refers to the process of copying a part of one world space into another, so that the borders match up in both. This is needed for Beyond Skyrim projects whose provinces border each other. Once a project has their border region more or less finalized, it needs to be copied over to the other project so that they can level design their border region while accounting for what's directly on the other side; and once that's done, this border region needs to be copied back the other way.
This article will demonstrate how to do one such copying operation using xEdit, without the larger-scale project management involved; it is also useful for other situations where this may have to be done, and not just in Beyond Skyrim (many scripts discussed here were originally created for Skywind).
Preparation
You need two world spaces, one containing the landscape to copy (henceforth origin world space) and one to copy to (destination world space). For clarity's sake, this article will assume that they are contained in masters (.esm) to distinguish them from plugins (.esp), but the process is the same if they are in plugins. The two world spaces may be part of the same master file, which makes the process a bit simpler, but often will be in separate masters.
The copy happens in two passes that are independent and can be done in any order: copying of landscape, and copying of references. Both methods work on rectangular regions in a world space, so you must figure out what those rectangular regions are (there can be multiple that combine into a more complex shape, even just one cell large if necessary). Write down the cell coordinates of the northeast and southwest corners of every rectangle according to the origin world space's coordinate system. Then, also calculate what those coordinates amount to in Units (precise formulas forthcoming). It helps to have a grid map or similar on hand.
Next, find out the difference between the coordinate systems of the two world spaces. Pick a location that can be pinpointed in both world spaces, note its cell coordinates in the origin world spaces, and its cell coordinates in the destination world space. Subtract the X and Y coordinates in the origin world space from the X and Y coordinates in the destination world space, respectively. This will give you the offset, or the number of cells to move the copy by along the X and Y axes. Also calculate the X and Y offsets in units by multiplying them with 4096 (as that is the number of units comprising the edge of a Skyrim world space cell). If the offset is not a clean multiple of a cell, then the method for copying landscape will not work, as the method for that works cell by cell. The method for copying references is unit-based, but will be of little use if the landscape cannot be accurately copied. Lastly, if the sea level is not at the same height in the two world spaces, write down the difference between them in units (this does not have to be a multiple of 4096).
If the two world spaces belong to different masters, and the destination world space's file is not a master of the origin world space's file, then the copied area must be free of land textures and references whose base objects come from the origin world space's file. This can be resolved in advance (such as by re-implementing any non-vanilla assets in a shared master, and switching all references to use those shared base objects; for Beyond Skyrim, this is BSAssets) but can also be arranged at the end of the process; this article will show how to contain the copy in a patch plugin which can be tweaked before being sent off to be merged into the destination world space's master. If it is too difficult to integrate land textures, these can optionally not be copied as detailed further below.
To redirect all references of a certain base object in the origin file to a base object that is compatible with the destination file, one can use the "Search and replace" function in the Creation Kit, or the following xEdit script by Zilav, which has the advantage of also covering land textures (just replace OldFormID and NewFormID as needed):
unit UserScript; const OldFormID = $01012345; NewFormID = $01054321; function Process(e: IInterface): integer; begin CompareExchangeFormID(e, OldFormID, NewFormID); end; end.
Landscape
Here, landscape refers to the LAND record in each cell, containing land height (i.e. heightmap data) and the land textures used (which refer to LTEX base objects).
If you wish to copy land textures, and some of them still come from the origin world space's file, then you must temporarily make the origin file a master of the destination file for the copying to work; the copying will also not happen in a patch plugin, but is applied directly on the destination file. However, a later step will extract the edits into a patch plugin after all.
Apply a script on the origin world space (right click on the world space in the origin file, then Apply script). Choose the built-in script "Worldspace copy landscape area to another worldspace". If you want to copy land textures, edit the script so that bCopyLayers = True
.
A pop-up window appears. Configure the two world spaces; enter the cell coordinates of the southwest corner of the current rectangle, and sizes of the rectangle along both axes, which is the difference between the two X and Y coordinates respectively. A rectangle comprised of one cell will have sizes 1 and 1; a square of 2x2 cells will be 2 and 2, and so on. Then enter the southwest cell coordinates in the destination world space by summing the cell offsets with the origin world space's coordinates. You must leave "Create patch plugin" unchecked unless you have no problematic land textures or forego copying them entirely, and if you check it, every rectangle will go in its own patch plugin which you will want to merge afterward.
Then allow the script to run. LAND records are heavy and so this will take some time. For a large rectangle, xEdit may run out of memory, so consider using the 64bit version. After a rectangle has been copied, you can save to free the memory back up.
Once you are done with all rectangles, and if you didn't have "Create patch plugin" checked, save and close xEdit just in case so you can make a back-up of your destination esm. Put a fresh copy of your destination esm in your data folder, one free of any land copying. In xEdit, load only this fresh esm. Once loaded, right click it, select "Compare to..." and select your modified esm. This will make a new load order in xEdit where the modified esm appears to override the records of the fresh one. Right click anywhere and select "Apply filter"; in the pop-up, check "by Base record signature" and check "LAND". Also make sure "by conflict status overall" is still checked, and deselect the top three options. Also check "Flatten Blocks/Sub-Blocks" and "Flatten Cell Children" further below. Run the filter and wait a (long) while. After it's done, only the changed LAND records should still be visible in xEdit. In your modified esm, Alt+Click the expand icon next to the destination world space to instantly open all cells, select one LAND record, press Ctrl+A to select all other LAND records as well, right click and select "Copy as override into...". Make it a new plugin. This plugin now contains all the results of the process, and you can test it alongside the fresh esm.
References
The process with references will be to first make a plugin that just contains overrides of all the references that need copying (without actually changing anything yet), then turning those into new references, and finally applying a script to move them to the destination world space.
Load the origin esm (you don't need to load the destination esm yet at this point). To filter for references within the rectangular area, apply the following custom script; it doesn't matter on what, as it will check all records across all currently loaded files (feel free to improve the script and make it faster). Before applying, edit the values of xmin, xmax, ymin and ymax inside the Initizalize function to match the unit coordinates of your rectangle (again, feel free to improve in order to make it interactive). The script is a crude mishmash of code by km816 and Zilav.
unit FilterByPosition; var xmin, xmax, ymin, ymax: float; function Filter(e: IInterface): Boolean; var stat: IInterface; xp, yp: float; begin if GetIsDeleted(e) then exit; if (Signature(e)<>'REFR') and (Signature(e)<>'ACHR') then exit; xp := StrToFloat(GetElementEditValues(e,'DATA\Position\X')); if (xp<xmin) or (xp>xmax) then exit; yp := StrToFloat(GetElementEditValues(e,'DATA\Position\Y')); if (yp<ymin) or (yp>ymax) then exit; Result := True; end; function Initialize: Integer; begin xmin := 0; xmax := 4096; ymin := 0; ymax := 4096; FilterConflictAll := False; FilterConflictThis := False; FilterByInjectStatus := False; FilterInjectStatus := False; FilterByNotReachableStatus := False; FilterNotReachableStatus := False; FilterByReferencesInjectedStatus := False; FilterReferencesInjectedStatus := False; FilterByEditorID := False; FilterEditorID := ''; FilterByName := False; FilterName := ''; FilterByBaseEditorID := False; FilterBaseEditorID := ''; FilterByBaseName := False; FilterBaseName := ''; FilterScaledActors := False; FilterByPersistent := False; FilterPersistent := False; FilterUnnecessaryPersistent := False; FilterMasterIsTemporary := False; FilterIsMaster := False; FilterPersistentPosChanged := False; FilterDeleted := False; FilterByVWD := False; FilterVWD := False; FilterByHasVWDMesh := False; FilterHasVWDMesh := False; FilterBySignature := True; // FilterSignatures := 'REFR'; // refs only FilterByBaseSignature := False; FilterBaseSignatures := ''; FlattenBlocks := True; FlattenCellChilds := True; AssignPersWrldChild := False; InheritConflictByParent := False; // color conflicts FilterScripted := True; // use custom Filter() function ApplyFilter; Result := 1; end; end.
Alt+Click the expand icon next to the world space, select one reference, press Ctrl+Alt to select all references in the world space, and "Copy as override into" a new plugin. This may take a while. If you have more rectangles, then right click and Remove filter, and run the script again. This time, "copy as override" into the plugin you previously created for convenience's sake.
This paragraph pertains to copying references for LOD only. Areas that are far enough away from the playable border to never be in the loaded area, may still be visible from a distance and thus will have to be copied over to be included in LOD that is generated for the destination world space. Here, you may like to exclude references that won't be visible in LOD. Once you have your .esp from the previous step, apply a Filter for Base record signature "Tree" (shrubs and other small flora will also be lumped in). Copy these as override from your previous .esp into a new one. Remove the filter. Then do another filter by running the below script by Zilav, which retains only references whose base object has LOD meshes configured. Copy all remaining references from the same previous .esp into the same second .esp you created. You may now discard the first .esp and continue with the second one.
unit ApplyCustomScripted; function Filter(e: IInterface): Boolean; var stat: IInterface; begin stat := WinningOverride(BaseRecord(e)); if Signature(stat) = 'STAT' then Result := ElementExists(stat, 'MNAM'); end; function Initialize: Integer; begin FilterConflictAll := False; FilterConflictThis := False; FilterByInjectStatus := False; FilterInjectStatus := False; FilterByNotReachableStatus := False; FilterNotReachableStatus := False; FilterByReferencesInjectedStatus := False; FilterReferencesInjectedStatus := False; FilterByEditorID := False; FilterEditorID := ''; FilterByName := False; FilterName := ''; FilterByBaseEditorID := False; FilterBaseEditorID := ''; FilterByBaseName := False; FilterBaseName := ''; FilterScaledActors := False; FilterByPersistent := False; FilterPersistent := False; FilterUnnecessaryPersistent := False; FilterMasterIsTemporary := False; FilterIsMaster := False; FilterPersistentPosChanged := False; FilterDeleted := False; FilterByVWD := False; FilterVWD := False; FilterByHasVWDMesh := False; FilterHasVWDMesh := False; FilterBySignature := True; // FilterSignatures := 'REFR'; // refs only FilterByBaseSignature := False; FilterBaseSignatures := ''; FlattenBlocks := False; FlattenCellChilds := False; AssignPersWrldChild := False; InheritConflictByParent := False; // color conflicts FilterScripted := True; // use custom Filter() function ApplyFilter; Result := 1; end; end.
Once your patch .esp has all the needed references from all rectangles, save and close xEdit and make a backup. Load it again, apply a filter that only does "Flatten blocks/sub-blocks" and "flatten cell children", select all references in the plugin using the Alt+Click/Ctrl+Click method, right click, and "Change FormID". Select the current plugin. This will change all the overrides into new references, but it will take some time.
Save, close, backup, and reload, this time including the destination esm. Run the same filter to flatten blocks and cell children, select all references in the patch plugin, right click, and apply the built-in script "Worldspace move references into another worldspace". Indicate the destination world space; check "Move persistent references" and "only from current plugin". Then enter the offset values expressed in units, which you should've calculated earlier.
After the script is done, your plugin should be ready, unless there were references whose base object comes from the origin esm. In that case, your plugin will depend on both esms and must be tweaked and cleaned before it can be merged into the destination esm.