×
Меню
Индекс

LiveMeshEdit script

How to make the tests faster!
 
No need to restart Morrowind every time!
You can modify a NIF in Nifskope, and instantly see new result ingame!
So this tool allows Morrowind to reload the targeted NIF file every time you save it in Nifskope.
It is an MWSE script made by Greatness7
 
0. Install MWSE for Morrowind
1. Download LiveMeshEdit script (from nexusmods,MMC discord,..-.)
(скрипты приведены ниже! Т.е. копируем текст в новый текстовый файл, сохраняем с правильным названием и кладем по правильному адресу).
2. Put "main.lua" file in Morrowind\Data Files\MWSE\mods\LiveMeshEdit folder
3. Open "main.lua" file with text editor(notepad).
    you'll see:
 
-- user variables
local mesh = "my_mesh_collection\\test.nif"
 
-- internal variables
local path = lfs.currentdir() .. "\\data files\\meshes\\"
 
     Example : the NIF you're editing is located in "C:\Games\Morrowind\Data Files\Meshes\my_mesh_collection\test.nif"
    
3. Set the location of the NIF file you want to edit, by just rewriting the local mesh value: the "red text part", than save main.lua
    Take care to use double "\\" for folders.
 
4. Launch Morrowind. If you press 'x', the script will spawn the NIF in front of you, so you can quickly start 'sculpting it' (works once per Morrowind launch)
    If the NIF you located is used by the game (for example a weapon, a creature, spell, or object), it will be also refreshed!
        example : targeted NIF is a weapon: If you re-draw the weapon it will be reloaded with the new NIF model
        example : targeted NIF is a magic effect: If you re-cast the spell it will be reloaded with the new NIF model
        example : targeted NIF is a creature: If you re-spawn the creature, it will be reloaded with the new NIF model
        example : targeted NIF is a world object :  If you re-enter the cell where the object is placed, it will be reloaded with the new NIF model
 
 
note: similar editing is possible in OpenMW too thanks to Wazabear
         https://gitlab.com/glassmancody.info/openmw/-/pipelines/491740071
         https://gitlab.com/glassmancody.info/openmw/-/commits/live_reload_nifs

Data Files -> MWSE -> mods -> liveMeshEditing -> main.lua
 Live Mesh Editing script:
main.lua
--[[
    Live Mesh Editing.
    Usage:
        update the path variable to point your file
        go in-game and press CTRL+X to spawn your object
        the game object will refresh when file is edited
--]]
 
-- file path of the target mesh
local relpath = "g7\\export.nif"
 
-- absolute path (in lowercase)
local abspath = string.lower(tes3.installDirectory .. "\\data files\\meshes\\" .. relpath)
 
local filewatcher = require("filewatcher")
filewatcher.registerWatcher(abspath)
 
local function update()
    timer.delayOneFrame(function()
        timer.delayOneFrame(function()
            local ref = tes3.getReference(relpath)
            if not ref then return end
 
            local src = tes3.loadMesh(relpath, false)
            local dst = ref.sceneNode
            if not (src and dst) then return end
 
            -- transfer properties
            dst.fogProperty = src.fogProperty
            dst.alphaProperty = src.alphaProperty
            dst.stencilProperty = src.stencilProperty
            dst.zBufferProperty = src.zBufferProperty
            dst.materialProperty = src.materialProperty
            dst.texturingProperty = src.texturingProperty
            dst.vertexColorProperty = src.vertexColorProperty
 
            -- transfer children
            dst:detachAllChildren()
            for i, child in ipairs(src.children) do
                if child then
                    dst:attachChild(child, true)
                end
            end
 
            dst:update()
            dst:updateEffects()
            dst:updateProperties()
 
            tes3.messageBox("MESH RELOADED")
        end)
    end)
end
event.register("fileWatcher:fileModified", update, { filter = abspath })
 
local function onKeyDown(e)
    if not e.isControlDown then return end
 
    local ref = tes3.player.tempData.liveMeshEditingRef
    if ref then
        return
    end
 
    local rayhit = tes3.rayTest{
        position = tes3.getPlayerEyePosition(),
        direction = tes3.getPlayerEyeVector(),
        ignore = {tes3.player},
    }
    if not rayhit then return end
 
    local cell = tes3.player.cell
    cell_modified = cell.modified
 
    -- create the reference
    ref = tes3.createReference({
        cell = cell,
        position = rayhit.intersection,
        object = tes3.createObject({
            objectType = tes3.objectType.activator,
            id = relpath,
            name = relpath,
            mesh = relpath,
            getIfExists = true,
        }),
    })
    tes3.player.tempData.liveMeshEditingRef = ref
 
    -- avoid zfighting with ground
    local z = math.max(math.abs(ref.object.boundingBox.min.z), 1.0)
    ref.position = ref.position + tes3vector3.new(0.0, 0.0, z)
 
    ref.object.sourceless = true
    ref.hasNoCollision = true
    ref.modified = false
    tes3.player.cell.modified = cell_modified
end
event.register("keyDown", onKeyDown, {filter=tes3.scanCode.x})
 
 
Data Files -> MWSE -> lib ->  filewatcher.lua
 filewatcher.lua
--[[
    filewatcher.lua
 
    Example:
        local filewatcher = require("filewatcher")
 
        local file = string.lower(lfs.currentdir() .. "\\test.txt")
        filewatcher.registerWatcher(file)
 
        local function onFileModified(e)
            tes3ui.log(io.open(e.path, "rb"):read("*a"))
        end
        event.register("fileWatcher:fileModified", onFileModified, { filter = file })
--]]
 
local this = {}
 
--- @alias Path string
--- @alias TimeStamp number
--- @alias NotificationHandle number
 
--- @class RegistryEntry
--- @field handle NotificationHandle
--- @field mtimes table<Path, TimeStamp>
 
--- @type table<Path, RegistryEntry>
this.registry = {}
 
 
local ffi = require("ffi")
ffi.cdef[[
    typedef int BOOL;
    typedef unsigned long DWORD;
    typedef void HANDLE;
 
    DWORD GetLastError();
    HANDLE* FindFirstChangeNotificationA(const char* path, BOOL recursive, DWORD filter);
    BOOL FindNextChangeNotification(HANDLE*);
    BOOL FindCloseChangeNotification(HANDLE*);
    DWORD WaitForSingleObject(HANDLE*, DWORD);
]]
 
 
--- @param dir Path
--- @return NotificationHandle
local function openNotificationHandle(dir)
    return ffi.C.FindFirstChangeNotificationA(dir, false, 0x11F)
end
 
 
--- @param handle NotificationHandle
--- @return boolean
local function closeNotificatioNHandle(handle)
    return ffi.C.FindCloseChangeNotification(handle)
end
 
 
--- @param handle NotificationHandle
--- @return boolean
local function getDirectoryModified(handle)
    if ffi.C.WaitForSingleObject(handle, 0) == 0 then
        ffi.C.FindNextChangeNotification(handle)
        return true
    end
    return false
end
 
local function dispatchEvents()
    for _, entry in pairs(this.registry) do
        -- use windows api to check if any directory contents have been modified
        if getDirectoryModified(entry.handle) then
            -- the directory was modified, now figure out which particuler files
            for path, prevTime in pairs(entry.mtimes) do
                local currTime = lfs.attributes(path, "modification")
                if currTime ~= prevTime then
                    entry.mtimes[path] = currTime
                    --- @diagnostic disable-next-line: undefined-field
                    timer.frame.delayOneFrame(function()
                        event.trigger("fileWatcher:fileModified", { path = path }, { filter = path })
                    end)
                end
            end
        end
    end
end
 
--- @param abspath Path
--- @return Path
function this.registerWatcher(abspath)
    abspath = abspath:lower()
 
    local dir = abspath:match("(.-)([^\\]+)$")
    local mtime = lfs.attributes(abspath, "modification")
 
    if this.registry[dir] then
        this.registry[dir].mtimes[abspath] = mtime
    else
        this.registry[dir] = {
            handle = openNotificationHandle(dir),
            mtimes = { [abspath] = mtime }
        }
    end
 
    if not event.isRegistered("simulate", dispatchEvents) then
        event.register("simulate", dispatchEvents)
    end
 
    return abspath
end
 
--- @param abspath Path
--- @return Path
function this.unregisterWatcher(abspath)
    abspath = abspath:lower()
 
    -- make sure no other file is using hte same directory before clearing it
    local dir = abspath:match("(.-)[^\\]+$")
    local entry = this.registry[dir]
    if entry then
        entry.mtimes[abspath] = nil
 
        -- close the handle if no other files in this directory are being watched
        if table.empty(entry.mtimes) then
            closeNotificatioNHandle(entry.handle)
            this.registry[dir] = nil
        end
 
        if table.empty(this.registry) then
            if event.isRegistered("simulate", dispatchEvents) then
                event.unregister("simulate", dispatchEvents)
            end
        end
    end
 
    return abspath
end
 
return this