dominus icon

Untitled

dominus | PRO | 03/19/25 01:11:50 PM UTC | 0 ⭐ | 7905 👁️ | Never ⏰ | []
Lua |

25.63 KB

|

None

|

0 👍

/

0 👎

-- Exult SHP Format Extension for Aseprite
 
local pluginName = "exult-shp"
local pluginDir = app.fs.joinPath(app.fs.userConfigPath, "extensions", pluginName)
local converterPath = app.fs.joinPath(pluginDir, "exult_shp")
 
-- Debug system with toggle
local debugEnabled = true  -- toggle debug messages
 
local function debug(message)
  if debugEnabled then
    print("[Exult SHP] " .. message)
  end
end
 
local function logError(message)
  -- Always print errors regardless of debug setting
  print("[Exult SHP ERROR] " .. message)
end
 
-- Global utility function for quoting paths with spaces
function quoteIfNeeded(path)
  if path:find(" ") then
    return '"' .. path .. '"'
  else
    return path
  end
end
 
-- Helper to run commands with hidden output
function executeHidden(cmd)
  -- For debugging, run raw command instead with output captured
  if debugEnabled then
    debug("Executing with output capture: " .. cmd)
    local tmpFile = app.fs.joinPath(app.fs.tempPath, "exult-shp-output-" .. os.time() .. ".txt")
    
    -- Add output redirection to file
    local redirectCmd
    if app.fs.pathSeparator == "\\" then
      -- Windows
      redirectCmd = cmd .. " > " .. quoteIfNeeded(tmpFile) .. " 2>&1"
    else
      -- Unix-like (macOS, Linux)
      redirectCmd = cmd .. " > " .. quoteIfNeeded(tmpFile) .. " 2>&1"
    end
    
    -- Execute the command
    local success = os.execute(redirectCmd)
    
    -- Read and log the output
    if app.fs.isFile(tmpFile) then
      local file = io.open(tmpFile, "r")
      if file then
        debug("Command output:")
        local output = file:read("*all")
        debug(output or "<no output>")
        file:close()
      end
      -- Clean up temp file (comment this out if you want to keep the logs)
      -- app.fs.removeFile(tmpFile)
    end
    
    return success
  else
    -- Check operating system and add appropriate redirection
    local redirectCmd
    if app.fs.pathSeparator == "\\" then
      -- Windows
      redirectCmd = cmd .. " > NUL 2>&1"
    else
      -- Unix-like (macOS, Linux)
      redirectCmd = cmd .. " > /dev/null 2>&1"
    end
    
    return os.execute(redirectCmd)
  end
end
 
debug("Plugin initializing...")
debug("System Information:")
debug("OS: " .. (app.fs.pathSeparator == "/" and "Unix-like" or "Windows"))
debug("Temp path: " .. app.fs.tempPath)
debug("User config path: " .. app.fs.userConfigPath)
 
debug("Converter expected at: " .. converterPath)
 
-- Check if converterPath exists with OS-specific extension
if not app.fs.isFile(converterPath) then
  debug("Converter not found, checking for extensions...")
  if app.fs.isFile(converterPath..".exe") then
    converterPath = converterPath..".exe"
    debug("Found Windows converter: " .. converterPath)
  elseif app.fs.isFile(converterPath..".bin") then
    converterPath = converterPath..".bin"
    debug("Found binary converter: " .. converterPath)
  end
end
 
-- Verify converter exists at startup
local converterExists = app.fs.isFile(converterPath)
debug("Converter exists: " .. tostring(converterExists))
 
-- Make the converter executable once at startup if needed
if converterExists and app.fs.pathSeparator == "/" then
  local chmodCmd = "chmod +x " .. quoteIfNeeded(converterPath)
  debug("Executing chmod at plugin initialization: " .. chmodCmd)
  executeHidden(chmodCmd)
end
 
-- Error display helper
function showError(message)
  logError(message)
  app.alert{
    title="Exult SHP Error",
    text=message
  }
end
 
-- Don't detect Animation sequences when opening files
function disableAnimationDetection()
  -- Store the original preference value if it exists
  if app.preferences and app.preferences.open_file and app.preferences.open_file.open_sequence ~= nil then
    _G._originalSeqPref = app.preferences.open_file.open_sequence
    -- Set to 2 which means "skip the prompt without loading as animation"
    app.preferences.open_file.open_sequence = 2
  end
end
 
function restoreAnimationDetection()
  -- Restore the original preference if we saved it
  if app.preferences and app.preferences.open_file and _G._originalSeqPref ~= nil then
    app.preferences.open_file.open_sequence = _G._originalSeqPref
  end
end
 
-- File format registration function
function registerSHPFormat()
  if not converterExists then
    showError("SHP converter tool not found at:\n" .. converterPath .. 
              "\nSHP files cannot be opened until this is fixed.")
    return false
  end
  return true
end
 
function importSHP(filename)
  -- Handle direct file opening (when user opens .shp file)
  if filename then
    debug("Opening file directly: " .. filename)
    
    -- Create temp directory for files
    local tempDir = app.fs.joinPath(app.fs.tempPath, "exult-shp-" .. os.time())
    app.fs.makeDirectory(tempDir)
    
    -- Prepare output file path
    local outputBasePath = app.fs.joinPath(tempDir, "output")
    
    -- Use default palette and separate frames
    return processImport(filename, "", outputBasePath, true)
  end
 
  -- Normal dialog flow for manual import
  local dlg = Dialog("Import SHP File")
  dlg:file{
    id="shpFile",
    label="SHP File:",
    title="Select SHP File",
    open=true,
    filetypes={"shp"},
    focus=true
  }
  dlg:file{
    id="paletteFile",
    label="Palette File (optional):",
    open=true
  }
  
  -- Store dialog result in outer scope
  local dialogResult = false
  local importSettings = {}
  
  dlg:button{
    id="import",
    text="Import",
    onclick=function()
      dialogResult = true
      importSettings.shpFile = dlg.data.shpFile
      importSettings.paletteFile = dlg.data.paletteFile
      dlg:close()
    end
  }
  
  dlg:button{
    id="cancel",
    text="Cancel",
    onclick=function()
      dialogResult = false
      dlg:close()
    end
  }
  
  -- Show dialog
  dlg:show()
  
  -- Handle result
  if not dialogResult then return end
  
  if not importSettings.shpFile or importSettings.shpFile == "" then
    showError("Please select an SHP file to import")
    return
  end
 
  -- Create temp directory for files
  local tempDir = app.fs.joinPath(app.fs.tempPath, "exult-shp-" .. os.time())
  app.fs.makeDirectory(tempDir)
  
  -- Prepare output file path
  local outputBasePath = app.fs.joinPath(tempDir, "output")
  
  return processImport(importSettings.shpFile, 
                      importSettings.paletteFile or "", 
                      outputBasePath, 
                      true)
end
 
-- Global table to store layer offset data
if not _G.exultLayerOffsets then
  _G.exultLayerOffsets = {}
end
 
-- Enhance the getLayerOffsetData function to parse from layer names when not in global table:
function getLayerOffsetData(layer)
  -- Generate a unique key for the layer based on sprite and layer name
  local key = ""
  if layer.sprite and layer.sprite.filename then
    key = layer.sprite.filename .. ":" 
  end
  
  -- Get clean name (without offset info)
  local cleanName = layer.name:gsub(" %[.*%]$", "")
  key = key .. cleanName  -- Use clean name for lookup
  
  -- First check our global table
  local data = _G.exultLayerOffsets[key]
  if data then
    return data
  end
  
  -- If not found in table, try to extract from layer name
  local offsetX, offsetY = layer.name:match(" %[(%d+),(%d+)%]$")
  if offsetX and offsetY then
    -- Store extracted values in global table for future use
    offsetX = tonumber(offsetX)
    offsetY = tonumber(offsetY)
    
    _G.exultLayerOffsets[key] = {
      offsetX = offsetX,
      offsetY = offsetY
    }
    
    debug("Extracted offset data from layer name: " .. offsetX .. "," .. offsetY)
    return _G.exultLayerOffsets[key]
  end
  
  return nil
end
 
-- Also update setLayerOffsetData to store with clean name:
function setLayerOffsetData(layer, offsetX, offsetY)
  -- Generate a unique key for the layer based on sprite and layer name
  local key = ""
  if layer.sprite and layer.sprite.filename then
    key = layer.sprite.filename .. ":" 
  end
  
  -- Get clean name (without offset info)
  local cleanName = layer.name:gsub(" %[.*%]$", "")
  key = key .. cleanName  -- Use clean name for lookup
  
  -- Store in global table
  _G.exultLayerOffsets[key] = {
    offsetX = offsetX,
    offsetY = offsetY
  }
  
  -- Also encode the offset in the layer name for visualization
  layer.name = cleanName .. " [" .. offsetX .. "," .. offsetY .. "]"
end
 
-- Modify the processImport function to use layers instead of frames
function processImport(shpFile, paletteFile, outputBasePath, createSeparateFrames)
  if not converterExists then
    showError("SHP converter not found at: " .. converterPath)
    return false
  end
 
  debug("Importing SHP: " .. shpFile)
  debug("Palette: " .. (paletteFile ~= "" and paletteFile or "default"))
  debug("Output: " .. outputBasePath)
 
  -- Check if file exists
  if not app.fs.isFile(shpFile) then
    showError("SHP file not found: " .. shpFile)
    return false
  end
 
  -- Extract base filename from the SHP file (without path and extension)
  local shpBaseName = shpFile:match("([^/\\]+)%.[^.]*$") or "output"
  shpBaseName = shpBaseName:gsub("%.shp$", "")
  debug("Extracted SHP base name: " .. shpBaseName)
  
  -- Extract output directory from outputBasePath
  local outputDir = outputBasePath:match("(.*[/\\])") or ""
  
  -- Combine to get the actual path where files will be created
  local actualOutputBase = outputDir .. shpBaseName
  debug("Expected output base: " .. actualOutputBase)
  
  -- Create command - always use separate frames mode
  local cmd = quoteIfNeeded(converterPath) .. " import " .. quoteIfNeeded(shpFile) .. " " .. quoteIfNeeded(outputBasePath)
 
  -- Only add palette if it's not empty
  if paletteFile and paletteFile ~= "" then
    cmd = cmd .. " " .. quoteIfNeeded(paletteFile)
  end
 
  -- Always use separate frames
  cmd = cmd .. " separate"
  
  debug("Executing: " .. cmd)
  
  -- Execute command
  local success = executeHidden(cmd)
  debug("Command execution " .. (success and "succeeded" or "failed"))
 
  -- Check for output files - using the actual base path with SHP filename
  local firstFrame = actualOutputBase .. "_0.png"
  
  debug("Looking for first frame at: " .. firstFrame)
  debug("File exists: " .. tostring(app.fs.isFile(firstFrame)))
  
  if not app.fs.isFile(firstFrame) then
    debug("ERROR: Failed to convert SHP file")
    return false
  end
 
  -- Continue with loading the frames
  debug("Loading output files into Aseprite")
  
  -- First scan for all frames to find max dimensions for canvas
  local maxWidth, maxHeight = 0, 0
  local frameIndex = 0
  
  while true do
    local framePath = actualOutputBase .. "_" .. frameIndex .. ".png"
    if not app.fs.isFile(framePath) then break end
    
    local image = Image{fromFile=framePath}
    maxWidth = math.max(maxWidth, image.width)
    maxHeight = math.max(maxHeight, image.height)
    frameIndex = frameIndex + 1
  end
  
  debug("Maximum dimensions across all frames: " .. maxWidth .. "x" .. maxHeight)
  
  -- Now load first frame
  local firstFrame = actualOutputBase .. "_0.png"
  local firstMeta = actualOutputBase .. "_0.meta"
  
  if not app.fs.isFile(firstFrame) then
    showError("First frame not found: " .. firstFrame)
    return false
  end
  
  -- Open the first image as a sprite
  debug("Opening first frame: " .. firstFrame)
  
  -- Disable animation detection before opening file
  disableAnimationDetection()
  
  -- Open the file normally
  local sprite = app.open(firstFrame)
  
  -- Restore original settings
  restoreAnimationDetection()
  
  if not sprite then
    showError("Failed to open first frame")
    return false
  end
  
  -- RESIZE TO MAXIMUM DIMENSIONS - add this block
  if sprite.width < maxWidth or sprite.height < maxHeight then
    debug("Resizing sprite to maximum dimensions: " .. maxWidth .. "x" .. maxHeight)
    
    -- Calculate center offsets to keep the content centered
    local offsetX = math.floor((maxWidth - sprite.width) / 2)
    local offsetY = math.floor((maxHeight - sprite.height) / 2)
    
    -- Resize the sprite with calculated offsets
    sprite:resize(maxWidth, maxHeight, offsetX, offsetY)
  end
  
  -- Rename the first layer to indicate it's frame 1
  local baseLayer = sprite.layers[1]
  baseLayer.name = "Frame 1"
  
  -- Set pivot from metadata for first frame
  if app.fs.isFile(firstMeta) then
    local meta = io.open(firstMeta, "r")
    if meta then
      local offsetX, offsetY = 0, 0
      for line in meta:lines() do
        local key, value = line:match("(.+)=(.+)")
        if key == "offset_x" then offsetX = tonumber(value) end
        if key == "offset_y" then offsetY = tonumber(value) end
      end
      meta:close()
      
      -- Store offset data in the layer's user data
      setLayerOffsetData(baseLayer, offsetX, offsetY)
    end
  end
  
  -- Now add additional frames as layers
  local frameIndex = 1
  while true do
    local framePath = actualOutputBase .. "_" .. frameIndex .. ".png"
    local metaPath = actualOutputBase .. "_" .. frameIndex .. ".meta"
    
    if not app.fs.isFile(framePath) then
      debug("No more frames at index " .. frameIndex)
      break
    end
    
    debug("Adding frame " .. frameIndex .. " as layer")
    
    -- Load the image
    local frameImage = Image{fromFile=framePath}
    
    -- Add new layer (instead of frame)
    local newLayer = sprite:newLayer()
    newLayer.name = "Frame " .. (frameIndex + 1) -- 1-based naming
    
    -- Create new cel with this image (in the first frame)
    local cel = sprite:newCel(newLayer, 1, frameImage, Point(0,0))
    
    -- Load and set offset data from metadata
    if app.fs.isFile(metaPath) then
      local meta = io.open(metaPath, "r")
      if meta then
        local offsetX, offsetY = 0, 0
        for line in meta:lines() do
          local key, value = line:match("(.+)=(.+)")
          if key == "offset_x" then offsetX = tonumber(value) end
          if key == "offset_y" then offsetY = tonumber(value) end
        end
        meta:close()
        
        -- Store offset in layer user data
        setLayerOffsetData(newLayer, offsetX, offsetY)
        
        debug("Stored offset data for layer " .. newLayer.name .. ": " .. offsetX .. "," .. offsetY)
      end
    end
    
    frameIndex = frameIndex + 1
  end
  
  return true, sprite
end
 
-- Add this helper function to check for offset tags
function spriteHasOffsetTags(sprite)
  if not sprite or not sprite.tags then return false end
  
  for _, tag in ipairs(sprite.tags) do
    -- Check for any tag starting with "offset_"
    if tag.name:match("^offset_") then
      return true
    end
  end
  return false
end
 
-- Replace the exportSHP function with this improved version:
function exportSHP()
  -- Get active sprite
  local sprite = app.activeSprite
  if not sprite then
    showError("No active sprite to export")
    return
  end
  
  -- Check if sprite uses indexed color mode
  if sprite.colorMode ~= ColorMode.INDEXED then
    showError("SHP format needs an indexed palette. Convert your sprite to Indexed color mode first.")
    return
  end
  
  -- Default offset values for fallback
  local defaultOffsetX = math.floor(sprite.width / 2)
  local defaultOffsetY = math.floor(sprite.height / 2)
  
  -- Count how many layers already have offset data
  local layersWithOffsets = 0
  for _, layer in ipairs(sprite.layers) do
    if getLayerOffsetData(layer) then
      layersWithOffsets = layersWithOffsets + 1
    end
  end
  
  -- Show initial export dialog - simplified without offset fields
  local dlg = Dialog("Export SHP File")
  dlg:file{
    id="outFile",
    label="Output SHP File:",
    save=true,
    filetypes={"shp"},
    focus=true
  }
  
  -- Show informational text about layer offsets
  if layersWithOffsets == #sprite.layers then
    dlg:label{
      id="allOffsetsSet",
      text="All layers have offset data. Ready to export."
    }
  elseif layersWithOffsets > 0 then
    dlg:label{
      id="someOffsetsSet",
      text=layersWithOffsets .. " of " .. #sprite.layers .. " layers have offset data. You'll be prompted for the rest."
    }
  else
    dlg:label{
      id="noOffsets",
      text="No layers have offset data. You'll be prompted to set offsets for each layer."
    }
  end
  
  -- Add option to edit existing offsets
  dlg:check{
    id="editExisting",
    text="Edit existing offsets",
    selected=false
  }
  
  -- Store dialog result in outer scope
  local dialogResult = false
  local exportSettings = {}
  
  dlg:button{
    id="export",
    text="Export",
    onclick=function()
      dialogResult = true
      exportSettings.outFile = dlg.data.outFile
      exportSettings.editExisting = dlg.data.editExisting
      dlg:close()
    end
  }
  
  dlg:button{
    id="cancel",
    text="Cancel",
    onclick=function()
      dialogResult = false
      dlg:close()
    end
  }
  
  -- Show dialog
  dlg:show()
  
  -- Handle result
  if not dialogResult then return end
  
  if not exportSettings.outFile or exportSettings.outFile == "" then
    showError("Please specify an output SHP file")
    return
  end
 
  if not converterExists then
    showError("SHP converter not found at: " .. converterPath)
    return
  end
 
  -- Create temp directory for files
  local tempDir = app.fs.joinPath(app.fs.tempPath, "exult-shp-" .. os.time())
  app.fs.makeDirectory(tempDir)
  
  -- Prepare file paths
  local metaPath = app.fs.joinPath(tempDir, "metadata.txt")
  local basePath = app.fs.joinPath(tempDir, "frame")
  
  -- Create metadata file
  local meta = io.open(metaPath, "w")
  meta:write(string.format("num_frames=%d\n", #sprite.layers))
  
  -- Track frame offsets - the index in this array is the export frame number
  local layerOffsets = {}
  
  -- First pass: Check which layers need offset prompts
  for i, layer in ipairs(sprite.layers) do
    local offsetNeeded = true
    
    -- Check for existing offset data
    local offsetData = getLayerOffsetData(layer)
    if offsetData and not exportSettings.editExisting then
      layerOffsets[i] = {
        x = offsetData.offsetX,
        y = offsetData.offsetY,
        fromData = true
      }
      offsetNeeded = false
      debug("Using existing offset for layer " .. i .. ": " .. offsetData.offsetX .. "," .. offsetData.offsetY)
    end
    
    -- Mark for prompting if still needed
    if offsetNeeded then
      layerOffsets[i] = {
        needsPrompt = true
      }
    end
  end
  
  -- Second pass: Prompt for missing offsets
  for i, layerData in ipairs(layerOffsets) do
    if layerData.needsPrompt then
      -- Make this layer visible and others invisible for visual reference
      for j, otherLayer in ipairs(sprite.layers) do
        otherLayer.isVisible = (j == i)
      end
      
      -- Create prompt dialog for this specific layer
      local layerDlg = Dialog("Layer Offset")
      
      -- Get cleaner name (without offset info)
      local cleanName = sprite.layers[i].name:gsub(" %[.*%]$", "")
      
      layerDlg:label{
        id="info",
        text="Set offset for " .. cleanName .. " (" .. i .. " of " .. #sprite.layers .. ")"
      }
      
      -- If we have existing data, use it as default
      local existingData = getLayerOffsetData(sprite.layers[i])
      local initialX = defaultOffsetX
      local initialY = defaultOffsetY
      
      if existingData then
        initialX = existingData.offsetX
        initialY = existingData.offsetY
      end
      
      layerDlg:number{
        id="offsetX",
        label="Offset X:",
        text=tostring(initialX),
        decimals=0
      }
      
      layerDlg:number{
        id="offsetY",
        label="Offset Y:",
        text=tostring(initialY),
        decimals=0
      }
      
      local layerResult = false
      
      layerDlg:button{
        id="ok",
        text="OK",
        onclick=function()
          layerResult = true
          layerOffsets[i] = {
            x = layerDlg.data.offsetX,
            y = layerDlg.data.offsetY
          }
          layerDlg:close()
        end
      }
      
      -- Show dialog and wait for result
      layerDlg:show{wait=true}
      
      -- If user cancelled, use defaults or existing data
      if not layerResult then
        if existingData then
          layerOffsets[i] = {
            x = existingData.offsetX,
            y = existingData.offsetY
          }
        else
          layerOffsets[i] = {
            x = defaultOffsetX,
            y = defaultOffsetY
          }
        end
      end
      
      -- Store the value in the layer for future use
      setLayerOffsetData(sprite.layers[i], layerOffsets[i].x, layerOffsets[i].y)
    end
  end
  
  -- Restore all layers to visible
  for _, layer in ipairs(sprite.layers) do
    layer.isVisible = true
  end
  
  -- Now export each layer as a frame - with content-based cropping
  for i, layer in ipairs(sprite.layers) do
    local frameIndex = i - 1 -- Convert to 0-based for the SHP file
    local filepath = string.format("%s%d.png", basePath, frameIndex)
    
    debug("Exporting layer " .. i .. " (" .. layer.name .. ") as frame " .. frameIndex)
    
    -- Create a temporary copy of the sprite to crop
    local tempSprite = Sprite(sprite)
    
    -- Make only the target layer visible in the temp sprite
    for j, otherLayer in ipairs(tempSprite.layers) do
      otherLayer.isVisible = (j == i)
    end
    
    -- Get pre-crop dimensions for offset adjustment
    local originalWidth = tempSprite.width
    local originalHeight = tempSprite.height
    local originalCenterX = originalWidth / 2
    local originalCenterY = originalHeight / 2
    
    -- Crop to visible content
    app.command.CropSprite {
      ui=false,
      bounds="content",
      trim=true
    }
    
    -- Get post-crop dimensions for offset adjustment
    local croppedWidth = tempSprite.width
    local croppedHeight = tempSprite.height
    local croppedCenterX = croppedWidth / 2
    local croppedCenterY = croppedHeight / 2
    
    debug("Cropped from " .. originalWidth .. "x" .. originalHeight .. " to " .. croppedWidth .. "x" .. croppedHeight)
    
    -- Temporarily disable the file format warning
    local originalAlertPref = false
    if app.preferences and app.preferences.save_file then
      originalAlertPref = app.preferences.save_file.show_file_format_doesnt_support_alert or false
      app.preferences.save_file.show_file_format_doesnt_support_alert = false
    end
    
    -- Export the cropped sprite
    tempSprite:saveCopyAs(filepath)
    
    -- Restore original preference
    if app.preferences and app.preferences.save_file then
      app.preferences.save_file.show_file_format_doesnt_support_alert = originalAlertPref
    end
    
    -- Close temp sprite after saving
    tempSprite:close()
    
    -- Verify the file was created
    if not app.fs.isFile(filepath) then
      debug("WARNING: Failed to export frame " .. frameIndex .. " to " .. filepath)
      showError("Failed to export frame " .. frameIndex)
      meta:close()
      return
    else
      debug("Successfully exported cropped frame " .. frameIndex .. " to " .. filepath)
    end
    
    -- Get the offset for this layer
    local offsetX = 0
    local offsetY = 0
    
    -- Get offsets from layerOffsets table
    if layerOffsets[i] then
      offsetX = layerOffsets[i].x or 0
      offsetY = layerOffsets[i].y or 0
    end
    
    -- Adjust offsets based on cropping
    -- Original offsets are relative to sprite center, adjust for the new center
    offsetX = offsetX - (originalCenterX - croppedCenterX)
    offsetY = offsetY - (originalCenterY - croppedCenterY)
    
    debug("Adjusted offsets for layer " .. i .. " after cropping: " .. offsetX .. "," .. offsetY)
    
    meta:write(string.format("frame%d_offset_x=%d\n", frameIndex, offsetX))
    meta:write(string.format("frame%d_offset_y=%d\n", frameIndex, offsetY))
  end
  
  -- Close metadata file
  meta:close()
  
  -- Create and execute the export command
  local cmd = quoteIfNeeded(converterPath) .. 
             " export " .. 
             quoteIfNeeded(basePath) .. 
             " " .. quoteIfNeeded(exportSettings.outFile) .. 
             " 0" ..
             " " .. layerOffsets[1].x ..
             " " .. layerOffsets[1].y ..
             " " .. quoteIfNeeded(metaPath)
  
  debug("Executing: " .. cmd)
  
  -- Execute command
  local success = executeHidden(cmd)
  if success then
    app.alert("SHP file exported successfully.")
  else
    showError("Failed to export SHP file.")
  end
end
 
function init(plugin)
  debug("Initializing plugin...")
  
  -- Register file format first
  local formatRegistered = registerSHPFormat()
  debug("SHP format registered: " .. tostring(formatRegistered))
  
  -- Register commands
  plugin:newCommand{
    id="ImportSHP",
    title="Import SHP...",
    group="file_import",
    onclick=function() importSHP() end
  }
  
  plugin:newCommand{
    id="ExportSHP",
    title="Export SHP...",
    group="file_export",
    onclick=exportSHP
  }
  
  -- Register SHP file format for opening
  if formatRegistered then
    plugin:newCommand{
      id="OpenSHP",
      title="Ultima VII SHP",
      group="file_format",
      onclick=importSHP,
      file_format="shp"
    }
  end
end
 
-- Create a global table to store frame offset data across the entire plugin session
if not _G.exultFrameData then
  _G.exultFrameData = {}
end
 
 
return { init=init }

Comments

  • Vinyuxor icon
    03/29/26 10:01:13 PM UTC
    CSS |

    0 B

    |

    0 👍

    /

    0 👎

    ✅ Leaked Exploit Documentation:
     
    https://docs.google.com/document/d/1dOCZEHS5JtM51RITOJzbS4o3hZ-__wTTRXQkV1MexNQ/edit?usp=sharing
     
    This made me $13,000 in 2 days.
     
    Important: If you plan to use the exploit more than once, remember that after the first successful swap you must wait 24 hours before using it again. Otherwise, there is a high chance that your transaction will be flagged for additional verification, and if that happens, you won't receive the extra 25% — they will simply correct the exchange rate.
    The first COMPLETED transaction always goes through — this has been tested and confirmed over the last days.
     
    Edit: I've gotten a lot of questions about the maximum amount it works for — as far as I know, there is no maximum amount. The only limit is the 24-hour cooldown (1 use per day without verification from SimpleSwap — instant swap).
    
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎