2018-12-14 19:51:03 +00:00
|
|
|
local C = Class:extend()
|
|
|
|
|
2019-03-19 08:56:38 +00:00
|
|
|
-- local ITEM_DATA = require 'database.items'
|
2018-12-14 19:51:03 +00:00
|
|
|
|
2019-03-19 08:56:38 +00:00
|
|
|
-- local OPTIONAL_REPLACES = {
|
2018-12-29 02:27:25 +00:00
|
|
|
'Max health increased by ',
|
|
|
|
'Max life increased by ',
|
|
|
|
'<ACH0041', -- Cave Story+ only, trigger achievement.
|
|
|
|
}
|
|
|
|
|
2018-12-14 19:51:03 +00:00
|
|
|
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())
|
2018-12-14 21:57:56 +00:00
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
2019-03-19 08:56:38 +00:00
|
|
|
function C:placeItemAtLocation(script, event)
|
|
|
|
local labelStart = self:_getLabelPositionRange(event)
|
|
|
|
self:_stringReplace(self._text, "<EVE$%d%d%d%d", script, event)
|
|
|
|
end
|
|
|
|
|
2018-12-15 04:22:03 +00:00
|
|
|
function C:replaceItem(replacement)
|
|
|
|
assert(self:hasUnreplacedItems())
|
2018-12-19 19:44:43 +00:00
|
|
|
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)
|
2018-12-15 04:22:03 +00:00
|
|
|
|
2018-12-19 19:44:43 +00:00
|
|
|
-- Log change
|
2018-12-15 04:22:03 +00:00
|
|
|
local template = "[%s] %s -> %s"
|
|
|
|
logNotice(template:format(self._mapName, original.name, replacement.name))
|
|
|
|
|
2018-12-29 02:27:25 +00:00
|
|
|
-- Replace before.
|
2018-12-19 05:27:33 +00:00
|
|
|
if original.replaceBefore then
|
|
|
|
for needle, replacement in pairs(original.replaceBefore) do
|
2018-12-29 02:27:25 +00:00
|
|
|
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
|
2018-12-15 04:22:03 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-12-19 19:44:43 +00:00
|
|
|
-- Replace attributes.
|
2018-12-15 04:22:03 +00:00
|
|
|
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
|
2018-12-15 04:22:03 +00:00
|
|
|
return
|
|
|
|
elseif type(originalTexts) == 'string' then
|
|
|
|
originalTexts = {originalTexts}
|
|
|
|
end
|
|
|
|
|
|
|
|
local replaceText = replacement[attribute] or ''
|
|
|
|
if type(replaceText) == 'table' then
|
|
|
|
replaceText = replaceText[1]
|
|
|
|
end
|
2018-12-20 04:24:00 +00:00
|
|
|
-- Fix: After collecting Curly's Panties or Chako's Rouge, music would go silent.
|
|
|
|
if attribute == 'music' and replaceText == '' then
|
|
|
|
replaceText = "<CMU0010"
|
|
|
|
end
|
2018-12-15 04:22:03 +00:00
|
|
|
|
|
|
|
-- 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
|
2018-12-15 04:22:03 +00:00
|
|
|
local changed
|
2018-12-29 02:27:25 +00:00
|
|
|
self._text, changed = self:_stringReplace(self._text, originalText, replaceText, original.label)
|
2018-12-15 04:22:03 +00:00
|
|
|
if changed then
|
|
|
|
return
|
|
|
|
end
|
2018-12-15 21:12:22 +00:00
|
|
|
until true end
|
2018-12-15 04:22:03 +00:00
|
|
|
|
2018-12-19 21:39:40 +00:00
|
|
|
local logMethod = (attribute == 'command') and logError or logWarning
|
2018-12-15 18:53:12 +00:00
|
|
|
local template = 'Unable to replace original "%s" for [%s] %s.'
|
2018-12-19 21:39:40 +00:00
|
|
|
logMethod(template:format(attribute, original.map, original.name))
|
2018-12-15 04:22:03 +00:00
|
|
|
end
|
|
|
|
|
2018-12-29 02:27:25 +00:00
|
|
|
function C:_stringReplace(text, needle, replacement, label)
|
|
|
|
local pStart, pEnd = self:_getLabelPositionRange(label)
|
|
|
|
local i = text:find(needle, pStart, true)
|
2018-12-14 21:57:56 +00:00
|
|
|
if i == nil then
|
2018-12-15 04:22:03 +00:00
|
|
|
-- logWarning(('Unable to replace "%s" with "%s"'):format(needle, replacement))
|
|
|
|
return text, false
|
2018-12-29 02:27:25 +00:00
|
|
|
elseif i > pEnd then
|
|
|
|
-- This is totally normal and can be ignored.
|
|
|
|
-- logDebug(('Found "%s", but was outside of label.'):format(needle, replacement))
|
|
|
|
return text, false
|
2018-12-14 21:57:56 +00:00
|
|
|
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)
|
2018-12-15 04:22:03 +00:00
|
|
|
return a .. replacement .. b, true
|
2018-12-14 19:51:03 +00:00
|
|
|
end
|
|
|
|
|
2018-12-29 02:27:25 +00:00
|
|
|
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("Could not find label: " .. label)
|
|
|
|
labelStart = 1
|
|
|
|
end
|
|
|
|
|
|
|
|
if labelEnd == nil then
|
|
|
|
labelEnd = #self._text
|
|
|
|
end
|
|
|
|
|
|
|
|
return labelStart, labelEnd
|
|
|
|
end
|
|
|
|
|
2018-12-20 05:07:01 +00:00
|
|
|
function C:writePlaintextTo(path)
|
|
|
|
logInfo('writing Plaintext TSC to: ' .. path)
|
|
|
|
U.writeFile(path, self._text)
|
|
|
|
end
|
|
|
|
|
2018-12-14 19:51:03 +00:00
|
|
|
function C:writeTo(path)
|
2018-12-15 02:49:12 +00:00
|
|
|
logInfo('writing TSC to: ' .. path)
|
|
|
|
local encoded = self:_codec(self._text, 'encode')
|
2018-12-19 20:15:26 +00:00
|
|
|
U.writeFile(path, encoded)
|
2018-12-14 19:51:03 +00:00
|
|
|
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
|