From ea1d0720fccc41dd34e07f255197ed2a31904ec6 Mon Sep 17 00:00:00 2001 From: shru Date: Fri, 14 Dec 2018 14:51:03 -0500 Subject: [PATCH] Refactored to use TscFile class and read from dropped directory. --- docs/item locations.txt | 276 ++++++++++++++++++++++++++++++++++++++++ src/lib/classic.lua | 71 +++++++++++ src/main.lua | 198 ++++++---------------------- src/pxm_file.lua | 46 +++++++ src/tsc_file.lua | 83 ++++++++++++ 5 files changed, 515 insertions(+), 159 deletions(-) create mode 100644 docs/item locations.txt create mode 100644 src/lib/classic.lua create mode 100644 src/pxm_file.lua create mode 100644 src/tsc_file.lua diff --git a/docs/item locations.txt b/docs/item locations.txt new file mode 100644 index 0000000..420b843 --- /dev/null +++ b/docs/item locations.txt @@ -0,0 +1,276 @@ +--------------------------- +- 3-02 Upgrade Locations - +--------------------------- + +** LIFE CAPSULES ** +These will give a set Max HP boost, between 3 and 5, depending on the capsule. +You will have 50 life if you find them all, 55 if you include the bonus +capsule. Some of these are possible to miss. + + - First Cave: In plain sight as you descend toward the Polar Star, on the left +wall. +3 HP. + + - Mimiga Village: Found in Yamanshita Farm on the top of Mimiga Village. Head +to the far right, it's in the small pool. +3 HP. + + - Egg Corridor: At the very beginning, you have to drop down to the left just +as the Basil spark hits the wall then chase it so it extends its loop length. ++3 HP. + + - Egg Corridor: Go through Cthulhu's Abode and out the top door. Jump left. ++4 HP. + + - Bushlands: Just past where you found Santa's Key, go east to a set of two +horizontal rows of star blocks, jump on them and to the left. +5 HP. + + - Bushlands: In the Execution Chamber, the tall building with the skull on it +to the right of Kazuma's shack. +5 HP. + + - Sand Zone: East of Curly's House and past the Sun Stones, it's in the top of +the first thick pillar made up of star blocks. Try not to blow up all the star +blocks when fighting the Polishes to create a path and reach it. +5 HP. + + - Sand Zone: At the end of the hidden path behind a pawpad block on the far +right wall between the Sun Stones and Jenka's house, behind a line of star +blocks. Found next to a chest containing one of Jenka's dogs. +5 HP. + + - Labyrinth: Nestled next to the left wall of the first room of the labyrinth +when you're sent there, a ways up into the room. +5 HP. + + - Plantation: Sitting on a platform hanging from the far upper-left ceiling. ++4 HP. + + - Plantation: Talk to the puppy that appears on the left platform just under +the red skull signs on the top right section of the Plantation after Momorin's +finished the rocket. +5 HP. + + - Sanctuary: Bonus life capsule. Found in plain sight as you make your +initial descent. +5 HP. + + +** MISSILE EXPANSIONS ** +These each give +5 Max Missile Launcher Ammo. Your max ammo carries over to +the Super Missile Launcher. You'll have 30 missile ammo if you find them all, +54(!!) with the bonus. + + - Bushlands: To the left of the hut in the air above the shack where Kazuma's +trapped. Jump around the series of platforms around where the Save Point is +between the Power Supply Room and Kazuma's shack to reach it. + + - Bushlands: Inside the very hut that the previous expansion's sitting next +to. Opening it will trigger a fight with Kulala. + + - Egg Corridor?: When you go back to the Egg Corridor later in the game, +you'll find the eggs have hatched. The dragon where Egg 12 used to be is quite +pissed and will fervently guard the chest containing this expansion. + + - Egg Corridor?: In the ruins of the Egg observation Room, in plain sight. +Opening it will trigger a fight with The Sisters. + + - Sanctuary: Bonus expansion. Just before you fight the Heavy Press at the +far west end of Blood-Stained Sanctuary B3, just past the pillar with 3 Deletes +covering it, in the room filled with flying Butes and with a hanging platform +with an arrow Bute on either side. Above this platform is a single Star Block +concealing a chest containing this massive expansion of 24 Misisles. + + + + + +** KEY ITEMS ** + + - Map System +Allows you to press the - Button on the Wii Remote to bring up a map of the +room you're in. The map is tiny, doesn't show your current position and has a +strange graphical glitch that makes the rightmost section of mose large areas +unviewable, so it's of very little use except collectibility. + + - Silver Locket +An item found in the fishing hole to the west of Mimiga Village. It triggers +an event in the warehouse at the bottom right of the village. + + - Arthur's Key +Found at Arthur's grave in the Cemetary. Use it to access Arthur's house, at +the bottom-left of Mimiga Village. + + - ID Card +Found in Egg #06 in the Egg Corridor. Use it in Egg #01 to open the barrier to +the eastern edge of the Corridor. + + - Santa's Key +The key to Santa's house that he dropped to the east in the Bushlands. + + - Rusty Key +A key Kazuma found in the room he's stuck in. Gives access to the Power +Control Room in the Bushlands. + + - Gum Key +Another key conviniently located in Kazuma's captivity room. It opens up the +gum room in the very upper-right corner of the Bushlands. + + - Jellyfish Juice +A jar full of juice that you can find by defeating the huge jellyfish stuck in +the ceiling a bit east of Santa's house that only appears after talking to +Chako. Use it to put out fireplace fires; it's also one of three components +needed to create a bomb. It's the only Key Item you can receive more than +once, but you may only carry one at a time. + + - Chako's Rouge +Some red lipstick. Obtained by examining Chako's fireplace before you obtain +Jellyfish Jelly, causing her to walk up to you and explain some things, and +then sleeping in her bed while she's standing there. It has no use, other than +proof that you really, really, REALLY like Mimigas, you scoundrel. + + - Charcoal +A lump of charcoal found by putting out the fireplace in Santa's house. It's +one of three components needed to create a bomb. + + - Gum Base +A stick of chewing gum found in the room in the very top-right corner of the +Bushlands. It's one of three components needed to create a bomb. + + - Bomb +A bomb crafted by Malco, in the Power Generator Room of the Bushlands. Use it +to blow open the door of the building Kazuma's trapped inside. + + - Curly's Panties +Found in a hidden alcove behind the wall in the bedroom of Curly Brace's house +in the Sand Zone. They have no use, other than proof that you really, really, +REALLY like robots. If you own both this and Chako's Rouge, you are officially +the most envied man in Cave Story! Okay, I made that up. + + - Hajime +A dog that stays in Curly Brace's bedroom in the Sand Zone. One of Jenka's +five missing dogs. He's the leader of the bunch.. or so he claims. +Arf! + + - Mick +One of Jenka's dogs. He's supposedly an adept treasure hunter, and loves to +sleep in treasure chests. What he does with the contents is anyone's guess. +Found in a chest at the end of a hidden path between the Sun Stones and Jenka's +house. +Woof woof! + + - Shinobu +Another of Jenka's dogs. He has poor vision so he prefers to stay in dark +places. Found in an abandoned house at the end of a hidden road that begins in +a false ceiling along the path to the Sand Zone warehouse. +Bow wow wow! + + - Kakeru +Yet another dog of Jenka's. He loves bones and has buried them all over the +place. You can find him at the end of the sand-bottomed section of the path +toward the Sand Zone warehouse. +Woof woof woof woof! + + - Nene +A beautiful dog, the only female of the group. She sleeps all the time, and +can be found taking one of her long naps at the very end of the lower path of +the Sand Zone, right next to the warehouse. +Wan... + + - Life Pot +Restores all of your HP when used. It can only be used once, and there are +only two in the entire game, so use wisely! + + - Turbocharge +If you chose to take the Machine Gun from Curly in the Sand Zone, then you can +receive this for free from the Gaudi shopkeeper, Chaba, in the Labyrinth. It +speeds up the recovery rate of ammo for the Machine Gun. + + - Clinic Key +Opens the door to the Labyrinth Clinic, the building with the cross above it +up and west from the Camp. Obtained from Dr. Gero, the physician in the Camp. + + - Arms Barrier +Halves weapon EXP lost when you take damage. Found the top part of the Camp, +accessible via a hidden passageway in the ceiling in the large Labyrinth room +from which you can access the normal Camp entrance and the Clinic nearby. +Unless you took the Machine Gun, you'll have to come back to this area with the +Booster to be able to reach it. + + - Cure-All +A pill found in the abandoned Clinic in the Labyrinth. It supposedly has the +ability to cure anything. Give it to the Physician in the Labyrinth Camp. + + - Booster v0.8 +A jetpack that can be equipped and unequipped from the inventory menu. Once +it's equipped, press and hold Jump while airborne to launch upward for a few +moments. Can only be used once in the air until you hit the ground again. +Received in the Labyrinth, past the Clinic and Camp. + + - Booster v2.0 +A powerful jetpack that allows you to move freely for a time at high speeds in +midair if you press the jump button. If you want this instead of the Booster +v0.8, jump over the large gap in the Labyrinth room where Professor Booster +falls down by leaping just at the red marker on the ground. It's required if +you wish to access the hidden, super-difficult Sanctuary area at the end of the +game, but you may want to go for the normal ending with Booster v0.8 first. + + - Tow Rope +A tow cable found in the far lower-right corner of The Core (only appears if +you didn't take the Booster v0.8). It was used by the robots who came to the +island years ago to carry downed comrades. + + - Curly's Air Tank +Given to you by Curly Brace after the events at the Core. With it, you're +automatically encased in a bubble whenever you enter water, giving you infinite +air. + + - Alien Medal +Reward for defeating Ironhead (the boss of the Waterway) without taking any +damage. It's engraved with an image of Ikachan, the hero of a game named after +himself that was Studio Pixel's previous work before Cave STory. + + - Whimsical Star +A trinket that you can receive from Chaba, the Gaudi shopkeep in the Labyrinth, +if you talk to him with the Spur weapon in your posession. It will cause small +stars to float around you as a meagre shield when you charge the Spur to MAX. + + - Teleporter Room Key +This key is visible but unobtainable behind a cluster of instant-death spikes +in the pools at the bottom of the Plantation. You'll need to talk to Kanpachi, +the fishing Mimiga, to help you get it. Use it to unlock the room just west of +where it was fished up. + + - Sue's Letter +You'll find this stuffed in your inventory after you wake up inside the jail. +It's a leeter from Sue explaining a lot of what's been going on as far as the +story that you've had to just piece together by this point. It also contains +the password to enter the Safe House on the Plantation. + + - Mimiga Mask +A head covering that looks just like a Mimiga. Wearing it will allow you to +speak to the Mimiga in the Plantation who aren't allowed to speak with humans. + + - Broken Sprinkler +A broken water sprinkler from the Plantation. Useless by itself, but can be +traded to Megane in the Plantation Rest Area. + + - Sprinkler +It's new... isn't it? This can be taken back to Momorin in the Plantation Safe +House to help complete her rocket. + + - Controller +A technologically impressive device that could only have been made by a master +technician. Too bad he's a big fat coward. Obtained from Itoh after giving +Momorin the Sprinkler. + + - Mushroom Badge +Useless item given to you by Ma Pignon in hopes you'll leave him alone. I hope +you know how to answer his questions correctly even if you want this garbage. + + - Ma Pignon +Mystical, ultra-rare mushroom that's said to restore people's memories. Has a +smart-aleck personality and is known to be a surprisingly potent combatant, as +well. Found in the small storage room above the Mimiga Cemetar. + + - Mister Little +This guy's very, very hard to see and has gotten himself very, very lost. At +the end of the game, you can find him wandering around the Mimiga Cemetary. +It's said he is the proud owner of the world's most splendid gun. Has a +fascination with fine blades. + + - Medal of the Red Ogre (aka Clay Figure Medal) +Proof of the warrior who's finally able to put an end to the Red Ogre. It's +extremely heavy despite its size. diff --git a/src/lib/classic.lua b/src/lib/classic.lua new file mode 100644 index 0000000..2d6e0cc --- /dev/null +++ b/src/lib/classic.lua @@ -0,0 +1,71 @@ +-- Copyright (c) 2014, rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. + +local Object = {} +Object.__index = Object + +function Object:new() +end + +function Object:extend() + local cls = {} + for k, v in pairs(self) do + if k:find("__") == 1 then + cls[k] = v + end + end + cls.__index = cls + cls.super = self + setmetatable(cls, self) + return cls +end + +function Object:implement(...) + for _, cls in pairs({...}) do + for k, v in pairs(cls) do + if self[k] == nil and type(v) == "function" then + self[k] = v + end + end + end +end + +function Object:is(T) + local mt = getmetatable(self) + while mt do + if mt == T then + return true + end + mt = getmetatable(mt) + end + return false +end + +function Object:__tostring() + return "Object" +end + +function Object:__call(...) + local obj = setmetatable({}, self) + obj:new(...) + return obj +end + +return Object diff --git a/src/main.lua b/src/main.lua index 965ef91..af6bf22 100644 --- a/src/main.lua +++ b/src/main.lua @@ -2,60 +2,23 @@ io.stdout:setvbuf("no") require 'lib.strict' -local _ = require 'lib.moses' -local Serpent = require 'lib.serpent' +Class = require 'lib.classic' +_ = require 'lib.moses' +Serpent = require 'lib.serpent' -local lf = love.filesystem +lf = love.filesystem -local MODE_READ_BINARY = 'rb' -local MODE_WRITE_BINARY = 'wb' - -function readLittleEndian(file, bytes) - local string = file:read(2) - local bytes = string:byte() - -- print(string, type(bytes), bytes) - return bytes -end - --- https://gist.github.com/fdeitylink/3fded36e9187fe838eb18a412c712800 --- -=~ PXM File Data ~=- --- maps must be minimum of 21x16 -function readPXM(filename) - print('reading PXM: ' .. filename) - local file = assert(io.open(filename, MODE_READ_BINARY)) - -- First three bytes are PXM, then 0x10. - assert(file:read(3) == "PXM") - assert(file:read(1):byte() == 0x10) - - -- Then 0x_map_length - 2 bytes - -- Then 0x_map_height - 2 bytes - local length = readLittleEndian(file, 2) - local height = readLittleEndian(file, 2) - print('length:', length) - print('height:', height) - - -- Then 0x_map_tile_from_tileset for the rest of the file (numbered from 0, and going left to right, top to bottom) - 1 byte - local tiles = {} - for x = 1, length do - tiles[x] = {} - end - for y = 1, height do - for x = 1, length do - tiles[x][y] = file:read(1):byte() - end - end - -- Print result - local XXX = 3 - for y = 1, height do - local line = "" - for x = 1, length do - local tile = tiles[x][y] - local len = string.len(tile) - line = line .. string.rep(' ', XXX - len) .. tile - end - print(line) +local LOG_LEVEL = 5 +local function _log(level, prefix, text, ...) + if LOG_LEVEL >= level then + print(prefix .. text, ...) end end +function logError(...) _log(1, 'ERROR: ', ...) end +function logWarning(...) _log(2, 'WARNING: ', ...) end +function logNotice(...) _log(3, 'NOTICE: ', ...) end +function logInfo(...) _log(4, 'INFO: ', ...) end +function logDebug(...) _log(5, 'DEBUG: ', ...) end local ITEM_DATA = { -- Weapons @@ -77,123 +40,40 @@ local ITEM_DATA = { }, } -function stringReplace(text, needle, replacement) - local i = text:find(needle, 1, true) - if i == nil then - return text - 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 -end +local TSC_FILES = { + 'Pole.tsc', +} -function readTSC(filename, decodedFilename) - print('reading TSC: ' .. filename) - local file = assert(io.open(filename, MODE_READ_BINARY)) - local filesize = file:seek("end") - local encodingCharPosition = math.floor(filesize / 2) - file:seek("set", encodingCharPosition) - local encodingChar = file:read(1):byte() - print(" filesize", filesize) - print(" encoding char:", encodingChar) - print(" encoding char position:", encodingCharPosition) - - -- Decode - local chars, len = {}, 0 - file:seek("set") - for pos = 0, filesize - 1 do - local byte = file:read(1):byte() - if pos ~= encodingCharPosition then - -- print(pos, encodingCharPosition) - byte = (byte - encodingChar) % 256 - -- byte = byte - encodingChar - end - len = len + 1 - chars[len] = string.char(byte) - end - local decoded = table.concat(chars) - - local t = {} - decoded:gsub(".",function(c) table.insert(t,c) end) - -- for i, c in ipairs(t) do - local near = 10 - for i = 1270 - near, 1270 + near do - local c = t[i] - print (i, c, chars[i]) - -- print(string.byte(chars[i]) == string.byte(c), string.byte(chars[i]), string.byte(c)) - end - - -- -- Replace - -- decoded = stringReplace(decoded, ITEM_DATA.wPolar.command, ITEM_DATA.iPanties.command) - -- decoded = stringReplace(decoded, ITEM_DATA.wPolar.getText, ITEM_DATA.iPanties.getText) - -- decoded = stringReplace(decoded, ITEM_DATA.wPolar.displayCmd, ITEM_DATA.iPanties.displayCmd) - - -- Write - -- return decoded - local tmpFile, err = io.open(decodedFilename, MODE_WRITE_BINARY) - assert(err == nil, err) - tmpFile:write(decoded) - tmpFile:flush() - tmpFile:close() -end - -function writeTSC(filename) - print('writing TSC: ' .. filename) - local file = assert(io.open(filename, MODE_READ_BINARY)) - local filesize = file:seek("end") - local encodingCharPosition = math.floor(filesize / 2) - file:seek("set", encodingCharPosition) - local encodingChar = file:read(1):byte() - file:seek("set", encodingCharPosition) - print(file:read(1)) - -- local encodingChar = 32 - print(" filesize", filesize) - print(" encoding char:", encodingChar) - print(" encoding char position:", encodingCharPosition) - - -- Encode - local chars, len = {}, 0 - file:seek("set") - for pos = 0, filesize - 1 do - local byte = file:read(1):byte() - if pos ~= encodingCharPosition then - byte = (byte + encodingChar) % 255 - end - len = len + 1 - chars[len] = string.char(byte) - end - local decoded = table.concat(chars) - - -- Write - local tmpFile, err = io.open('TestingEncoded.tsc', MODE_WRITE_BINARY) - assert(err == nil, err) - tmpFile:write(decoded) - tmpFile:flush() - tmpFile:close() -end - -function love.load() - -- readPXM('Pole.pxm') - -- readTSC('Pole.tsc', 'Testing.tsc') - -- writeTSC('Testing.tsc') - -- readTSC('TestingEncoded.tsc', 'TestingDecoded.tsc') -end +-- function love.load() +-- -- readPXM('Pole.pxm') +-- -- readTSC('Pole.tsc', 'Testing.tsc') +-- -- writeTSC('Testing.tsc') +-- -- readTSC('TestingEncoded.tsc', 'TestingDecoded.tsc') +-- end function love.directorydropped(path) - local success = lf.mount(path, 'data') - assert(success) - + -- Mount + assert(lf.mount(path, 'data')) local items = lf.getDirectoryItems('/data') local containsStage = _.contains(items, 'Stage') assert(containsStage) local dirStage = '/data/Stage' - -- local items = lf.getDirectoryItems(dirStage) - -- print(Serpent.block(items)) + local tscFiles = {} + for _, filename in ipairs(TSC_FILES) do + local path = dirStage .. '/' .. filename + local TscFile = require 'tsc_file' + tscFiles[filename] = TscFile(path) + + -- decoded = stringReplace(decoded, ITEM_DATA.wPolar.command, ITEM_DATA.iPanties.command) + -- decoded = stringReplace(decoded, ITEM_DATA.wPolar.getText, ITEM_DATA.iPanties.getText) + -- decoded = stringReplace(decoded, ITEM_DATA.wPolar.displayCmd, ITEM_DATA.iPanties.displayCmd) + end + + tscFiles['Pole.tsc']:writeTo('Testing.tsc') + + -- Unmount + assert(lf.unmount(path)) end function love.keypressed(key) diff --git a/src/pxm_file.lua b/src/pxm_file.lua new file mode 100644 index 0000000..c9e898e --- /dev/null +++ b/src/pxm_file.lua @@ -0,0 +1,46 @@ +local function readLittleEndian(file, bytes) + local string = file:read(2) + local bytes = string:byte() + -- print(string, type(bytes), bytes) + return bytes +end + +-- https://gist.github.com/fdeitylink/3fded36e9187fe838eb18a412c712800 +-- -=~ PXM File Data ~=- +-- maps must be minimum of 21x16 +local function readPXM(filename) + print('reading PXM: ' .. filename) + local file = assert(io.open(filename, MODE_READ_BINARY)) + -- First three bytes are PXM, then 0x10. + assert(file:read(3) == "PXM") + assert(file:read(1):byte() == 0x10) + + -- Then 0x_map_length - 2 bytes + -- Then 0x_map_height - 2 bytes + local length = readLittleEndian(file, 2) + local height = readLittleEndian(file, 2) + print('length:', length) + print('height:', height) + + -- Then 0x_map_tile_from_tileset for the rest of the file (numbered from 0, and going left to right, top to bottom) - 1 byte + local tiles = {} + for x = 1, length do + tiles[x] = {} + end + for y = 1, height do + for x = 1, length do + tiles[x][y] = file:read(1):byte() + end + end + -- Print result + local XXX = 3 + for y = 1, height do + local line = "" + for x = 1, length do + local tile = tiles[x][y] + local len = string.len(tile) + line = line .. string.rep(' ', XXX - len) .. tile + end + print(line) + end +end diff --git a/src/tsc_file.lua b/src/tsc_file.lua new file mode 100644 index 0000000..e47f10f --- /dev/null +++ b/src/tsc_file.lua @@ -0,0 +1,83 @@ +local C = Class:extend() + +-- local MODE_READ_BINARY = 'rb' +local MODE_WRITE_BINARY = 'wb' + +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()) +end + +function C:writeTo(path) + local encoded = self:_codec(self._text, 'encode') + + local tmpFile, err = io.open(path, MODE_WRITE_BINARY) + assert(err == nil, err) + tmpFile:write(encoded) + tmpFile:flush() + tmpFile:close() +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) + + -- local t = {} + -- decoded:gsub(".",function(c) table.insert(t,c) end) + -- local near = 7 + -- for i = encodingCharPosition - near, encodingCharPosition + near do + -- local c = t[i] + -- logDebug(i, c, c:byte()) + -- end + + return decoded +end + +local function stringReplace(text, needle, replacement) + local i = text:find(needle, 1, true) + if i == nil then + return text + 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 +end + +return C