2019-03-19 08:56:38 +00:00
local Items = require ' database.items '
2018-12-19 04:06:41 +00:00
local TscFile = require ' tsc_file '
2019-03-19 08:56:38 +00:00
local WorldGraph = require ' database.world_graph '
2020-03-03 05:02:23 +00:00
local Music = require ' database.music '
2018-12-19 04:06:41 +00:00
local C = Class : extend ( )
local TSC_FILES = { }
do
2019-03-20 12:37:07 +00:00
for key , location in ipairs ( WorldGraph ( Items ( ) ) : getLocations ( ) ) do
if location.map ~= nil and location.event ~= nil then
local filename = location.map
if not _.contains ( TSC_FILES , filename ) then
table.insert ( TSC_FILES , filename )
end
2018-12-19 04:06:41 +00:00
end
end
2020-03-03 05:02:23 +00:00
for key , cue in pairs ( Music ( ) : getCues ( ) ) do
local filename = cue.map
if not _.contains ( TSC_FILES , filename ) then
table.insert ( TSC_FILES , filename )
end
end
2018-12-19 04:06:41 +00:00
end
2019-09-11 10:39:10 +00:00
local csdirectory
2019-03-25 08:23:29 +00:00
local function mkdir ( path )
local mkdir_str
if package.config : sub ( 1 , 1 ) == ' \\ ' then -- Windows
mkdir_str = ' mkdir "%s" '
else -- *nix
mkdir_str = " mkdir -p '%s' "
end
os.execute ( mkdir_str : format ( path ) ) -- HERE BE DRAGONS!!!
end
2018-12-29 08:23:24 +00:00
function C : new ( )
self._isCaveStoryPlus = false
2019-03-19 08:56:38 +00:00
self.itemDeck = Items ( )
2019-03-20 12:37:07 +00:00
self.worldGraph = WorldGraph ( self.itemDeck )
2020-03-03 05:02:23 +00:00
self.music = Music ( )
2019-09-12 02:51:48 +00:00
self.customseed = nil
2020-02-26 04:54:19 +00:00
self.puppy = false
2020-02-28 00:21:21 +00:00
self.obj = " "
2020-02-28 09:20:05 +00:00
self.sharecode = " "
2020-02-28 13:48:45 +00:00
self.mychar = " "
2020-03-03 05:52:39 +00:00
self.shuffleMusic = false
2018-12-29 08:23:24 +00:00
end
2019-09-11 10:39:10 +00:00
function C : setPath ( path )
csdirectory = path
end
2019-09-11 23:03:27 +00:00
function C : ready ( )
return csdirectory ~= nil
end
2019-09-11 10:39:10 +00:00
function C : randomize ( )
2018-12-19 21:39:40 +00:00
resetLog ( )
2018-12-29 02:27:25 +00:00
logNotice ( ' === Cave Story Randomizer v ' .. VERSION .. ' === ' )
2019-09-11 10:39:10 +00:00
local success , dirStage = self : _mountDirectory ( csdirectory )
2018-12-19 04:06:41 +00:00
if not success then
return " Could not find \" data \" subfolder. \n \n Maybe try dropping your Cave Story \" data \" folder in directly? "
end
2019-03-24 16:16:13 +00:00
2020-02-28 09:20:05 +00:00
self : _logSettings ( )
2019-09-12 02:51:48 +00:00
local seed = self : _seedRngesus ( )
2020-02-28 09:20:05 +00:00
self : _updateSharecode ( seed )
2018-12-19 04:06:41 +00:00
local tscFiles = self : _createTscFiles ( dirStage )
2020-03-03 05:52:39 +00:00
2019-03-19 08:56:38 +00:00
self : _shuffleItems ( tscFiles )
2020-03-03 05:52:39 +00:00
if self.shuffleMusic then self.music : shuffleMusic ( tscFiles ) end
2020-03-03 05:02:23 +00:00
2019-03-20 12:37:07 +00:00
self : _writeModifiedData ( tscFiles )
2018-12-29 19:28:20 +00:00
self : _writePlaintext ( tscFiles )
2018-12-19 21:39:40 +00:00
self : _writeLog ( )
2020-02-28 13:48:45 +00:00
self : _copyMyChar ( )
2019-09-11 10:39:10 +00:00
self : _unmountDirectory ( csdirectory )
2020-02-28 05:54:37 +00:00
self : _updateSettings ( )
2020-02-28 09:20:05 +00:00
return self : _getStatusMessage ( seed , self.sharecode )
2018-12-19 04:06:41 +00:00
end
function C : _mountDirectory ( path )
local mountPath = ' mounted-data '
assert ( lf.mount ( path , mountPath ) )
local dirStage = ' / ' .. mountPath
local items = lf.getDirectoryItems ( dirStage )
local containsData = _.contains ( items , ' data ' )
if containsData then
dirStage = dirStage .. ' /data '
end
2018-12-19 23:39:08 +00:00
-- For Cave Story+
local items = lf.getDirectoryItems ( dirStage )
local containsBase = _.contains ( items , ' base ' )
if containsBase then
dirStage = dirStage .. ' /base '
2018-12-29 08:23:24 +00:00
self._isCaveStoryPlus = true
2018-12-19 23:39:08 +00:00
end
2018-12-19 04:06:41 +00:00
local items = lf.getDirectoryItems ( dirStage )
local containsStage = _.contains ( items , ' Stage ' )
if containsStage then
dirStage = dirStage .. ' /Stage '
else
return false , ' '
end
return true , dirStage
end
function C : _seedRngesus ( )
2019-09-15 23:38:09 +00:00
local seedstring = self.customseed or tostring ( os.time ( ) )
local seed = ld.encode ( ' string ' , ' hex ' , ld.hash ( ' sha256 ' , seedstring ) )
local s1 = tonumber ( seed : sub ( - 8 , - 1 ) , 16 ) -- first 32 bits (from right)
local s2 = tonumber ( seed : sub ( - 16 , - 9 ) , 16 ) -- next 32 bits
love.math . setRandomSeed ( s1 , s2 )
2019-09-12 03:04:14 +00:00
logNotice ( ( ' Offering seed "%s" to RNGesus ' ) : format ( seedstring ) )
return seedstring
2019-03-28 05:45:22 +00:00
end
2018-12-19 04:06:41 +00:00
function C : _createTscFiles ( dirStage )
local tscFiles = { }
for _ , filename in ipairs ( TSC_FILES ) do
2019-03-25 09:26:40 +00:00
local path = dirStage .. ' / ' .. filename .. " .tsc "
2018-12-19 04:06:41 +00:00
tscFiles [ filename ] = TscFile ( path )
2019-03-20 12:37:07 +00:00
tscFiles [ filename ] . mapName = filename
2018-12-19 04:06:41 +00:00
end
return tscFiles
end
2018-12-20 05:07:01 +00:00
function C : _writePlaintext ( tscFiles )
local sourcePath = lf.getSourceBaseDirectory ( )
-- Create /data/Plaintext if it doesn't already exist.
2019-03-25 08:23:29 +00:00
mkdir ( sourcePath .. ' /data/Plaintext ' )
2018-12-20 05:07:01 +00:00
-- Write modified files.
for filename , tscFile in pairs ( tscFiles ) do
2019-03-20 12:37:07 +00:00
local path = sourcePath .. ' /data/Plaintext/ ' .. filename .. ' .txt '
2018-12-20 05:07:01 +00:00
tscFile : writePlaintextTo ( path )
end
end
2020-02-28 00:21:21 +00:00
function C : getObjective ( )
return { self.itemDeck : getByKey ( self.obj ) }
2020-02-26 07:34:19 +00:00
end
2018-12-19 04:06:41 +00:00
function C : _shuffleItems ( tscFiles )
2020-02-28 23:23:21 +00:00
-- ensure unique randomization between settings with the same seed
local function shuffle ( t )
if self.obj == " objBadEnd " then return _.shuffle ( _.shuffle ( t ) ) end
if self.obj == " objNormalEnd " then return _.shuffle ( _.shuffle ( _.shuffle ( t ) ) ) end
if self.obj == " objAllBosses " then return _.shuffle ( _.shuffle ( _.shuffle ( _.shuffle ( t ) ) ) ) end
return _.shuffle ( t )
end
local obj = self : getObjective ( ) [ 1 ]
obj.name = obj.name .. ( " , %s " ) : format ( self.worldGraph . spawn )
obj.script = obj.script .. self.worldGraph : getSpawnScript ( )
2020-03-01 10:38:13 +00:00
if self.worldGraph . seqbreak and self.worldGraph . dboosts.rocket . enabled then obj.script = " <FL+6400 " .. obj.script end
2020-02-28 00:21:21 +00:00
-- place the objective scripts in Start Point
2020-02-28 23:23:21 +00:00
self : _fastFillItems ( { obj } , self.worldGraph : getObjectiveSpot ( ) )
2020-02-28 05:19:21 +00:00
2020-02-28 22:58:29 +00:00
if self.worldGraph : StartPoint ( ) then
-- first, fill one of the first cave spots with a weapon that can break blocks
2020-02-28 23:23:21 +00:00
_.shuffle ( self.worldGraph : getFirstCaveSpots ( ) ) [ 1 ] : setItem ( shuffle ( self.itemDeck : getItemsByAttribute ( " weaponSN " ) ) [ 1 ] )
2020-02-28 22:58:29 +00:00
elseif self.worldGraph : Camp ( ) then
-- give Dr. Gero a strong weapon... you'll need it
2020-02-29 10:29:03 +00:00
self.worldGraph : getCamp ( ) [ 1 ] : setItem ( shuffle ( self.itemDeck : getItemsByAttribute ( " weaponStrong " ) ) [ 1 ] )
-- and some HP once you fight your way past the first few enemies
self.worldGraph : getCamp ( ) [ 2 ] : setItem ( self.itemDeck : getByKey ( " capsule5G " ) )
2020-02-28 22:58:29 +00:00
end
2020-02-26 07:34:19 +00:00
2020-02-28 05:19:21 +00:00
-- place the bomb on MALCO for bad end
if self.obj == " objBadEnd " then
self.worldGraph : getMALCO ( ) [ 1 ] : setItem ( self.itemDeck : getByKey ( " bomb " ) )
end
2020-02-28 23:23:21 +00:00
local mandatory = _.compact ( shuffle ( self.itemDeck : getMandatoryItems ( true ) ) )
local optional = _.compact ( shuffle ( self.itemDeck : getOptionalItems ( true ) ) )
local puppies = _.compact ( shuffle ( self.itemDeck : getItemsByAttribute ( " puppy " ) ) )
2020-02-26 04:54:19 +00:00
if not self.puppy then
2020-02-28 00:53:22 +00:00
-- then fill puppies, for normal gameplay
2020-02-28 23:23:21 +00:00
self : _fastFillItems ( puppies , shuffle ( self.worldGraph : getPuppySpots ( ) ) )
2020-02-26 04:54:19 +00:00
else
-- for puppysanity, shuffle puppies in with the mandatory items
2020-02-28 23:23:21 +00:00
mandatory = shuffle ( _.append ( mandatory , puppies ) )
2020-02-28 00:53:22 +00:00
puppies = { }
2020-02-26 04:54:19 +00:00
end
2019-03-21 05:46:22 +00:00
2019-03-19 08:56:38 +00:00
-- next fill hell chests, which cannot have mandatory items
2020-02-28 23:23:21 +00:00
self : _fastFillItems ( optional , shuffle ( self.worldGraph : getHellSpots ( ) ) )
2018-12-19 23:49:40 +00:00
2020-02-28 00:53:22 +00:00
-- place mandatory items with assume fill
2020-02-28 23:23:21 +00:00
self : _fillItems ( mandatory , shuffle ( _.reverse ( self.worldGraph : getEmptyLocations ( ) ) ) )
2020-02-28 00:53:22 +00:00
-- place optional items with a simple random fill
local opt = # optional
local loc = # self.worldGraph : getEmptyLocations ( )
if opt > loc then
logWarning ( ( " Trying to fill more optional items than there are locations! Items: %d Locations: %d " ) : format ( opt , loc ) )
end
2020-02-28 23:23:21 +00:00
self : _fastFillItems ( optional , shuffle ( self.worldGraph : getEmptyLocations ( ) ) )
2019-03-10 05:41:37 +00:00
2019-03-20 12:37:07 +00:00
self.worldGraph : writeItems ( tscFiles )
2019-03-21 06:39:30 +00:00
self.worldGraph : logLocations ( )
2019-03-19 08:56:38 +00:00
end
2018-12-19 20:15:26 +00:00
2019-03-21 05:46:22 +00:00
function C : _fillItems ( items , locations )
assert ( # items > 0 , ( " No items provided! Trying to fill %s locations. " ) : format ( # locations ) )
2019-03-20 12:37:07 +00:00
assert ( # items <= # locations , string.format ( " Trying to fill more items than there are locations! Items: %d Locations: %d " , # items , # locations ) )
2019-03-15 04:05:08 +00:00
2019-03-21 05:46:22 +00:00
local itemsLeft = _.clone ( items )
2019-03-21 08:21:30 +00:00
repeat
local item = _.pop ( itemsLeft )
local assumed = self.worldGraph : collect ( itemsLeft )
2019-03-20 12:37:07 +00:00
local fillable = _.filter ( locations , function ( k , v ) return not v : hasItem ( ) and v : canAccess ( assumed ) end )
2019-03-21 10:14:56 +00:00
if # fillable > 0 then
logDebug ( ( " Placing %s at %s " ) : format ( item.name , fillable [ 1 ] . name ) )
fillable [ 1 ] : setItem ( item )
else
logError ( ( " No available locations for %s! Items left: %d " ) : format ( item.name , # itemsLeft ) )
end
2019-03-21 08:21:30 +00:00
until # itemsLeft == 0
2019-03-19 08:56:38 +00:00
end
2019-03-15 04:05:08 +00:00
2019-03-19 08:56:38 +00:00
function C : _fastFillItems ( items , locations )
2019-03-21 05:46:22 +00:00
assert ( # items > 0 , ( " No items provided! Attempting to fast fill %s locations. " ) : format ( # locations ) )
2019-03-20 12:37:07 +00:00
for key , location in ipairs ( locations ) do
2019-03-21 05:46:22 +00:00
local item = _.pop ( items )
2019-03-20 12:37:07 +00:00
if item == nil then break end -- no items left to place, but there are still locations open
location : setItem ( item )
2019-03-10 05:41:37 +00:00
end
2018-12-19 04:06:41 +00:00
end
function C : _writeModifiedData ( tscFiles )
2018-12-29 08:23:24 +00:00
local basePath = self : _getWritePathStage ( )
2018-12-19 04:06:41 +00:00
for filename , tscFile in pairs ( tscFiles ) do
2019-03-20 12:37:07 +00:00
local path = basePath .. ' / ' .. filename .. ' .tsc '
2018-12-19 04:06:41 +00:00
tscFile : writeTo ( path )
end
end
2018-12-19 20:15:26 +00:00
function C : _copyModifiedFirstCave ( )
2018-12-29 08:23:24 +00:00
local cavePxmPath = self : _getWritePathStage ( ) .. ' /Cave.pxm '
2018-12-19 20:15:26 +00:00
local data = lf.read ( ' database/Cave.pxm ' )
assert ( data )
U.writeFile ( cavePxmPath , data )
end
2018-12-19 21:39:40 +00:00
function C : _writeLog ( )
2018-12-29 08:23:24 +00:00
local path = self : _getWritePath ( ) .. ' /log.txt '
2018-12-19 21:39:40 +00:00
local data = getLogText ( )
U.writeFile ( path , data )
end
2020-02-28 13:48:45 +00:00
function C : _copyMyChar ( )
local path = self : _getWritePath ( ) .. ' /myChar.bmp '
local data = lf.read ( self.mychar )
U.writeFile ( path , data )
end
2018-12-29 08:23:24 +00:00
function C : _getWritePath ( )
return select ( 1 , self : _getWritePaths ( ) )
end
function C : _getWritePathStage ( )
return select ( 2 , self : _getWritePaths ( ) )
end
function C : _getWritePaths ( )
if self._writePath == nil then
local sourcePath = lf.getSourceBaseDirectory ( )
self._writePath = sourcePath .. ' /data '
self._writePathStage = ( self._isCaveStoryPlus )
and ( self._writePath .. ' /base/Stage ' )
or ( self._writePath .. ' /Stage ' )
end
2019-09-13 05:40:26 +00:00
-- Create /data(/base)/Stage if it doesn't already exist.
mkdir ( self._writePathStage )
2018-12-29 08:23:24 +00:00
return self._writePath , self._writePathStage
end
2018-12-19 04:06:41 +00:00
function C : _unmountDirectory ( path )
assert ( lf.unmount ( path ) )
2018-12-19 21:39:40 +00:00
end
2020-02-28 09:20:05 +00:00
function C : _logSettings ( )
2020-02-28 23:23:21 +00:00
-- these random calls are a hacky way to make sure that
-- randomization changes between seeds if settings change
2020-02-28 09:20:05 +00:00
local obj = " Best Ending "
if self.obj == " objBadEnd " then
obj = " Bad Ending "
elseif self.obj == " objNormalEnd " then
obj = " Normal Ending "
elseif self.obj == " objAllBosses " then
obj = " All Bosses "
end
2020-02-28 23:23:21 +00:00
local spawn = ( " , %s " ) : format ( self.worldGraph . spawn )
2020-02-28 09:20:05 +00:00
local puppy = self.puppy and " , Puppysanity " or " "
2020-02-28 23:23:21 +00:00
logNotice ( ( " Game settings: %s " ) : format ( obj .. spawn .. puppy ) )
2020-02-28 09:20:05 +00:00
end
2020-02-28 05:54:37 +00:00
function C : _updateSettings ( )
Settings.settings . puppy = self.puppy
Settings.settings . obj = self.obj
2020-02-28 13:48:45 +00:00
Settings.settings . mychar = self.mychar
2020-02-28 23:50:30 +00:00
Settings.settings . spawn = self.worldGraph . spawn
2020-03-01 09:32:16 +00:00
Settings.settings . seqbreaks = self.worldGraph . seqbreak
Settings.settings . dboosts = _.map ( self.worldGraph . dboosts , function ( k , v ) return v.enabled end )
2020-03-03 07:21:07 +00:00
Settings.settings . musicShuffle = self.shuffleMusic
Settings.settings . musicBeta = self.music . betaEnabled
Settings.settings . musicFlavor = self.music . flavor
2020-02-28 05:54:37 +00:00
Settings : update ( )
end
2020-02-28 09:20:05 +00:00
function C : _updateSharecode ( seed )
local settings = 0 -- 0b00000000
-- P: single bit used for puppysanity
2020-03-01 10:03:00 +00:00
-- O: three bits used for objective
-- S: three bits used for spawn location
-- B: single bit used for sequence breaks
-- 0bBSSSOOOP
2020-02-28 09:20:05 +00:00
2020-02-28 23:50:30 +00:00
-- bitshift intervals
2020-03-01 10:03:00 +00:00
local obj = 1
local pup = 0
local spn = 4
local brk = 7
2020-02-28 23:50:30 +00:00
2020-02-28 09:20:05 +00:00
if self.obj == " objBadEnd " then
2020-02-28 23:50:30 +00:00
settings = bit.bor ( settings , bit.blshift ( 1 , obj ) )
2020-02-28 09:20:05 +00:00
elseif self.obj == " objNormalEnd " then
2020-02-28 23:50:30 +00:00
settings = bit.bor ( settings , bit.blshift ( 2 , obj ) )
2020-02-28 09:20:05 +00:00
elseif self.obj == " objAllBosses " then
2020-02-28 23:50:30 +00:00
settings = bit.bor ( settings , bit.blshift ( 3 , obj ) )
end
if self.puppy then settings = bit.bor ( settings , bit.blshift ( 1 , pup ) ) end
if self.worldGraph : StartPoint ( ) then
settings = bit.bor ( settings , bit.blshift ( 0 , spn ) )
elseif self.worldGraph : Arthur ( ) then
settings = bit.bor ( settings , bit.blshift ( 1 , spn ) )
elseif self.worldGraph : Camp ( ) then
settings = bit.bor ( settings , bit.blshift ( 2 , spn ) )
2020-02-28 09:20:05 +00:00
end
2020-03-01 10:03:00 +00:00
local seq = 0
if self.worldGraph . seqbreak then
settings = bit.bor ( settings , bit.blshift ( 1 , brk ) )
if self.worldGraph . dboosts.cthulhu . enabled then seq = bit.bor ( seq , 1 ) end
if self.worldGraph . dboosts.chaco . enabled then seq = bit.bor ( seq , 2 ) end
if self.worldGraph . dboosts.paxChaco . enabled then seq = bit.bor ( seq , 4 ) end
if self.worldGraph . dboosts.flightlessHut . enabled then seq = bit.bor ( seq , 8 ) end
if self.worldGraph . dboosts.camp . enabled then seq = bit.bor ( seq , 16 ) end
if self.worldGraph . dboosts.sisters . enabled then seq = bit.bor ( seq , 32 ) end
if self.worldGraph . dboosts.plantation . enabled then seq = bit.bor ( seq , 64 ) end
if self.worldGraph . dboosts.rocket . enabled then seq = bit.bor ( seq , 128 ) end
end
2020-02-28 09:20:05 +00:00
if # seed < 20 then
seed = seed .. ( " " ) : rep ( 20 -# seed )
end
2020-03-01 10:03:00 +00:00
local packed = love.data . pack ( " data " , " sBB " , seed , settings , seq )
2020-02-28 09:20:05 +00:00
self.sharecode = love.data . encode ( " string " , " base64 " , packed )
logNotice ( ( " Sharecode: %s " ) : format ( self.sharecode ) )
end
function C : _getStatusMessage ( seed , sharecode )
2018-12-19 21:39:40 +00:00
local warnings , errors = countLogWarningsAndErrors ( )
local line1
if warnings == 0 and errors == 0 then
2020-02-28 09:20:05 +00:00
line1 = ( " Randomized data successfully created! \n Seed: %s \n Sharecode: %s " ) : format ( seed , sharecode )
2018-12-19 21:39:40 +00:00
elseif warnings ~= 0 and errors == 0 then
line1 = ( " Randomized data was created with %d warning(s). " ) : format ( warnings )
else
return ( " Encountered %d error(s) and %d warning(s) when randomizing data! " ) : format ( errors , warnings )
end
local line2 = " Next overwrite the files in your copy of Cave Story with the versions in the newly created \" data \" folder. Don't forget to save a backup of the originals! "
local line3 = " Then play and have a fun! "
2020-02-28 09:20:05 +00:00
local status = ( " %s \n %s \n %s " ) : format ( line1 , line2 , line3 )
2018-12-19 21:39:40 +00:00
return status
2018-12-19 04:06:41 +00:00
end
return C