cave-story-randomizer/src/tsc_file.lua

252 lines
6.9 KiB
Lua
Raw Normal View History

local C = Class:extend()
-- local ITEM_DATA = require 'database.items'
local OPTIONAL_REPLACES = {
'Max health increased by ',
'Max life increased by ',
'<ACH0041', -- Cave Story+ only, trigger achievement.
}
function C:new(path)
logInfo('reading TSC: ' .. path)
local file = lf.newFile(path)
assert(file:open('r'))
local contents, size = file:read()
self._text = self:_codec(contents, 'decode')
assert(file:close())
assert(file:release())
-- Determine set of items which can be replaced later.
--[[
self._unreplaced = {}
self._mapName = path:match("^.+/(.+)$")
for k, v in pairs(ITEM_DATA) do repeat
if (v.map .. '.tsc') ~= self._mapName then
break -- continue
end
local item = _.clone(v)
table.insert(self._unreplaced, item)
until true end
self._unreplaced = _.shuffle(self._unreplaced)
]]
end
function C:hasUnreplacedItems()
return #self._unreplaced >= 1
end
function C:placeItemAtLocation(item, location)
local wasChanged
self._text, wasChanged = self:_stringReplace(self._text, "<EVE....", item.script, location.event)
if not wasChanged then
local template = 'Unable to place [%s] "%s" at "%s".'
logError(template:format(location.map, item.name, location.name))
end
end
function C:replaceItem(replacement)
assert(self:hasUnreplacedItems())
local key = self._unreplaced[#self._unreplaced].key
self:replaceSpecificItem(key, replacement)
end
function C:replaceSpecificItem(originalKey, replacement)
-- Fetch item with key matching originalKey.
local original
for index, item in ipairs(self._unreplaced) do
if item.key == originalKey then
original = item
table.remove(self._unreplaced, index)
break
end
end
assert(original, 'No unreplaced item with key: ' .. originalKey)
-- Log change
local template = "[%s] %s -> %s"
logNotice(template:format(self._mapName, original.name, replacement.name))
-- Replace before.
if original.replaceBefore then
for needle, replacement in pairs(original.replaceBefore) do
local wasChanged
self._text, wasChanged = self:_stringReplace(self._text, needle, replacement, original.label)
-- Log error if replace was not optional.
if wasChanged == false then
local wasOptional = false
for _, pattern in ipairs(OPTIONAL_REPLACES) do
if needle:find(pattern, 1, true) then
wasOptional = true
break
end
end
if wasOptional == false then
local template = 'Unable to replace [%s] "%s" with "%s".'
logError(template:format(original.map, needle, replacement))
end
end
end
end
-- Replace attributes.
self:_replaceAttribute(original, replacement, 'command')
self:_replaceAttribute(original, replacement, 'getText')
self:_replaceAttribute(original, replacement, 'displayCmd')
self:_replaceAttribute(original, replacement, 'music')
end
function C:_replaceAttribute(original, replacement, attribute)
local originalTexts = original[attribute]
2018-12-15 19:10:40 +00:00
if originalTexts == nil or originalTexts == '' then
return
elseif type(originalTexts) == 'string' then
originalTexts = {originalTexts}
end
local replaceText = replacement[attribute] or ''
if type(replaceText) == 'table' then
replaceText = replaceText[1]
end
-- Fix: After collecting Curly's Panties or Chako's Rouge, music would go silent.
if attribute == 'music' and replaceText == '' then
replaceText = "<CMU0010"
end
-- Loop through each possible original value until we successfully replace one.
2018-12-15 21:12:22 +00:00
for _, originalText in ipairs(originalTexts) do repeat
if originalText == "" then
break -- continue
end
local changed
self._text, changed = self:_stringReplace(self._text, originalText, replaceText, original.label)
if changed then
return
end
2018-12-15 21:12:22 +00:00
until true end
local logMethod = (attribute == 'command') and logError or logWarning
local template = 'Unable to replace original "%s" for [%s] %s.'
logMethod(template:format(attribute, original.map, original.name))
end
function C:_stringReplace(text, needle, replacement, label)
local pStart, pEnd = self:_getLabelPositionRange(label)
local i = text:find(needle, pStart)
if i == nil then
logNotice(('Unable to replace "%s" with "%s"'):format(needle, replacement))
return text, false
elseif i > pEnd then
-- This is totally normal and can be ignored.
logNotice(('Found "%s", but was outside of label.'):format(needle, replacement))
return text, false
end
local len = needle:len()
local j = i + len - 1
assert((i % 1 == 0) and (i > 0) and (i <= j), tostring(i))
assert((j % 1 == 0), tostring(j))
local a = text:sub(1, i - 1)
local b = text:sub(j + 1)
return a .. replacement .. b, true
end
function C:_getLabelPositionRange(label)
local labelStart, labelEnd
-- Recursive shit for when label is a table...
if type(label) == 'table' then
labelStart, labelEnd = math.huge, 0
for _, _label in ipairs(label) do
local _start, _end = self:_getLabelPositionRange(_label)
labelStart = math.min(labelStart, _start)
labelEnd = math.max(labelEnd, _end)
end
return labelStart, labelEnd
end
assert(type(label) == 'string')
assert(#label == 4)
assert(tonumber(label) >= 1)
assert(tonumber(label) <= 9999)
local i = 1
local labelPattern = "#%d%d%d%d\r\n"
while true do
local j = self._text:find(labelPattern, i)
if j == nil then
break
end
i = j + 1
if labelStart then
labelEnd = j - 1
break
end
local _label = self._text:sub(j + 1, j + 4)
if label == _label then
labelStart = j
end
end
if labelStart == nil then
logError(("%s: Could not find label: %s"):format(self.mapName, label))
labelStart = 1
end
if labelEnd == nil then
labelEnd = #self._text
end
return labelStart, labelEnd
end
function C:writePlaintextTo(path)
logInfo('writing Plaintext TSC to: ' .. path)
U.writeFile(path, self._text)
end
function C:writeTo(path)
logInfo('writing TSC to: ' .. path)
local encoded = self:_codec(self._text, 'encode')
U.writeFile(path, encoded)
end
function C:_codec(text, mode)
-- Create array of chars.
local chars = {}
text:gsub(".", function(c) table.insert(chars, c) end)
-- Determine encoding char value
local encodingCharPosition = math.floor(#chars / 2) + 1
local encodingChar = chars[encodingCharPosition]:byte()
if mode == 'decode' then
encodingChar = encodingChar * -1
elseif mode == 'encode' then
-- OK!
else
error('Unknown codec mode: ' .. tostring(mode))
end
logDebug(" filesize", #chars)
logDebug(" encoding char:", encodingChar)
logDebug(" encoding char position:", encodingCharPosition)
-- Encode or decode.
for pos, char in ipairs(chars) do
if pos ~= encodingCharPosition then
local byte = (char:byte() + encodingChar) % 256
chars[pos] = string.char(byte)
end
end
local decoded = table.concat(chars)
return decoded
end
return C