Sunday, December 21, 2008

Programming: MAME roms cleaner

A ruby script for cleaning up a collection of MAME roms.
It essentially keeps the subset of roms necessary to play the games, and deletes the clones or non working games; it also separates some systems to ease management.
This may sound an offense to anally retentive people, which use to keep the roms for the sake of keeping them, or thinking that maybe in a thousand year a nonworking rom could be emulated correctly.

Notes:
* it's rule based, so one can actually write its own rules
* it only works in linux
* it caches (Marshal class) the data during the first run, as parsing the xml is darn slow
* it's written in ruby, so it requires the ruby interprer, plus the 'hpricot' gem
* it's redundant, for clarity


require 'rubygems'
require 'hpricot'
require 'fileutils'

################################################################################
# CLASS DEFINITIONS
################################################################################

class Game
PRELIMINARY = 'preliminary'
# GOOD = 'good'
# IMPERFECT = 'imperfect'

attr_reader :name, :sourcefile, :cloneof, :romof, :sampleof
attr_reader :description
attr_reader :status, :emulation, :color, :sound, :graphic
attr_accessor :parent, :sampleparent, :clones # clones not actually assigned, just modified

def year
(parent.year if parent) || @year || (sampleparent.year if sampleparent)
end

def has_chd?
@has_chd
end

def initialize(name, sourcefile, cloneof, romof, sampleof, description, year, status, emulation, color, sound, graphic, has_chd)
raise sprintf("%-8s: CO[%-8s] RO [%-8s]", name, cloneof, romof) if cloneof && (cloneof != romof)

@name, @sourcefile, @cloneof, @romof, @sampleof = name, sourcefile, cloneof, romof, sampleof
@description, @year = description, year
@status, @emulation, @color, @sound, @graphic = status, emulation, color, sound, graphic
@has_chd = has_chd
@parent, @sampleparent, @clones = nil, nil, []
end

def working?
@emulation != PRELIMINARY
end

def non_working?
! working?
end

def parent?
@cloneof.nil?
end

def clone?
! parent?
end

def self.decode(game_node)
driver_node = (game_node/'driver').first
description_node = (game_node/'description').first
year_node = (game_node/'year').first
has_chd = ((game_node/'disk')).size > 0

Game.new(game_node['name'], game_node['sourcefile'], game_node['cloneof'], game_node['romof'], game_node['sampleof'],
description_node.inner_html, (year_node.inner_html if year_node),
driver_node['status'], driver_node['emulation'], driver_node['color'], driver_node['sound'], driver_node['graphic'],
has_chd)
end
end

################################################################################
# ROUTINES
################################################################################

def create_dirs(roms_dirs)
puts '... creating dirs'
roms_dirs.each { |sym, dir| FileUtils.mkdir_p(dir)}
end

def load_games(mame_executable, cache_file)
if File.exists?(cache_file)
puts '... loading cached games'
open(cache_file, 'r') { |file| Marshal.load(file) }
else
puts '... piping mame xml games list into hpricot'
list_xml = IO.popen("#{mame_executable} -listxml") { |pipe| Hpricot::XML(pipe) }

games = parse_games(list_xml)

puts '... caching games'
open(cache_file, 'w') { |file| Marshal.dump(games, file) }

games
end
end

def load_xml(mame_executable)
end

def parse_games(list_xml)
games = {}

print '... decoding games'
(list_xml/'/mame/game').each do |xml_game|
game = Game.decode(xml_game)
games[game.name] = game
# print '.'
end
puts

print '... linking parents/clones'
games.each do |name, game|
if game.cloneof
parent = games[game.cloneof]
parent.clones << game if parent
game.parent = parent
end

if game.sampleof
sampleparent = games[game.sampleof]
game.sampleparent = sampleparent
end
# print '.'
end
puts

games
end

def filter_games(games, rules, roms_dirs)
puts '... filtering games'
games.each do |name, game|
if File.exist?(File.join(roms_dirs[:base], "#{name}.zip"))
filter = rules.call(game, roms_dirs)
puts filter if filter
end
end
end

################################################################################
# USER DEFINITIONS
################################################################################

MAME_EXECUTABLE = 'sdlmame'
MARSHALLED_FILE = '/tmp/list_mame_xml.rms'

ROMS_DIRS = {
:base => '/home/saverio/docs/roms/mame',
:neogeo => '/home/saverio/docs/roms/mame_neogeo',
:modern => '/home/saverio/docs/roms/mame_modern',
:chd => '/home/saverio/docs/roms/mame_chd'
}

# laser games aren't included, as currently there aren't the mame disk dumps, so
# it's better to use DAPHNE ones.
MODERN_SYSTEMS_DRIVERS = %w{
cps3.c # Capcom CPS3 => CPS3 Emulator
model1.c # Sega Model 1 => ???
model2.c # Sega Model 2 => Model 2 Emulator
namcos11.c # Namco System 11 => ZiNc
namcos12.c # Namco System 12 => ZiNc
naomi.c # Sega NAOMI => nullDC
}
#MODERN_OTHER = %w{
# stv.c # Sega Titan Video
# vegas.c # Atary/Midway Vegas
#}
#MODERN_UNEMULATED = %w{
# model3.c # Sega Model 3
# viper.c # Konami Viper
#}

# if a game is a preserved clone, don't delete it
# delete the very old games
# delete the nonworking clones, or the clones with a working parent
# keep/move the games better emulated by other emulators ('modern'), even if nonworking
# delete the parents of entirely nonworking gamesets
# move chd games
# other games are kept in the base dir
RULES_CLEAN = lambda do |game, roms_dirs|
filename = File.join(roms_dirs[:base], game.name) + "{,.zip}"

if game.cloneof == 'sf2ce'
# do nothing
elsif (! game.year) || game.year.to_i < 1980
sprintf "%-32s # Delete seventies game", "rm #{filename}"
elsif game.clone? && game.parent.working?
sprintf "%-32s # Delete clone of working parent", "rm #{filename}"
elsif game.clone? && game.non_working?
sprintf "%-32s # Delete nonworking clone", "rm #{filename}"
elsif game.sourcefile == 'neodrvr.c'
sprintf "%-32s # Move neo geo game", "mv #{filename} #{roms_dirs[:neogeo]}"
elsif MODERN_SYSTEMS_DRIVERS.include?(game.sourcefile)
sprintf "%-32s # Move modern game", "mv #{filename} #{roms_dirs[:modern]}"
elsif game.parent? && game.non_working? && ! game.clones.any? { |clone| clone.working? }
sprintf "%-32s # Delete parent of non working gameset", "rm #{filename}"
elsif game.has_chd?
sprintf "%-32s # Move chd game", "mv #{filename} #{roms_dirs[:chd]}"
end
end

# example, for displaying modern games with chd
RULES_MODERN_CHD = lambda do |game, roms_dirs|
puts game.name if game.has_chd?&& MODERN_SYSTEMS_DRIVERS.include?(game.sourcefile)
end

################################################################################
# RUNNER
################################################################################

create_dirs(ROMS_DIRS)
games = load_games(MAME_EXECUTABLE, MARSHALLED_FILE)

filter_games(games, RULES_CLEAN, ROMS_DIRS)

#filter_games(games, RULES_MODERN_CHD , :base => '/home/saverio/docs/roms/mame_modern')

No comments: