# frozen_string_literal: true
require 'zlib'
class Hash
def insert_at_index(index, key, value)
return self[key] = value if index >= size
temp_array = to_a
temp_array.insert(index, [key, value])
replace(temp_array.to_h)
end
end
# @param [String] string A parsed scripts code string, containing raw Ruby code
# @return [IndexSet<String>] Hash of parsed from code strings and their start indices
def self.extract_quoted_strings(string)
result = IndexSet.new
skip_block = false
in_quotes = false
quote_type = nil
buffer = []
string.each_line do |line|
stripped = line.strip
next if stripped[0] == '#' ||
!stripped.match?(/["']/) ||
stripped.start_with?(/(Win|Lose)|_Fanfare/) ||
stripped.match?(/eval\(/)
skip_block = true if stripped.start_with?('=begin')
skip_block = false if stripped.start_with?('=end')
next if skip_block
buffer.push('\#') if in_quotes
line.each_char do |char|
if %w[' "].include?(char)
unless quote_type.nil? || char == quote_type
buffer.push(char)
next
end
quote_type = char
in_quotes = !in_quotes
result.add(buffer.join)
buffer.clear
next
end
buffer.push(char) if in_quotes
end
end
result
end
# @param [Integer] code
# @param [String] parameter
# @param [String] game_type
# @return [String]
def self.parse_parameter(code, parameter, game_type)
unless game_type.nil?
case code
when 401, 405
case game_type
when 'lisa'
match = parameter.scan(/^(\\et\[[0-9]+\]|\\nbt)/)
parameter = parameter.slice((match[0].length)..) unless match.empty?
else
nil
end
when 102
# Implement some custom parsing
when 356
# Implement some custom parsing
else
return nil
end
end
parameter
end
# @param [String] variable
# @param [String] game_type
# @return [String]
def self.parse_variable(variable, game_type)
unless game_type.nil?
lines_count = variable.count("\n")
if lines_count.positive?
variable = variable.gsub(/\r?\n/, '\#')
case game_type
when 'lisa'
return nil unless variable.split('\#').all? { |line| line.match?(/^<.*>\.?$/) || line.empty? }
else
nil
end
end
end
variable
end
# @param [Array<String>] maps_files_paths
# @param [String] output_path
# @param [Boolean] logging
# @param [String] game_type
# @param [String] processing_type
def self.read_map(maps_files_paths, output_path, logging, game_type, processing_type)
maps_output_path = File.join(output_path, 'maps.txt')
names_output_path = File.join(output_path, 'names.txt')
maps_trans_output_path = File.join(output_path, 'maps_trans.txt')
names_trans_output_path = File.join(output_path, 'names_trans.txt')
if processing_type == :default && (File.exist?(maps_trans_output_path) || File.exist?(names_trans_output_path))
puts 'maps_trans.txt or names_trans.txt file already exists. If you want to forcefully re-read all files, use --force flag, or --append if you want append new text to already existing files.'
return
end
maps_object_map = Hash[maps_files_paths.map do |filename|
[File.basename(filename), Marshal.load(File.binread(filename))]
end]
maps_lines = IndexSet.new
names_lines = IndexSet.new
maps_translation_map = nil
names_translation_map = nil
if processing_type == :append
if File.exist?(maps_trans_output_path)
maps_translation_map = Hash[File.readlines(maps_output_path, chomp: true)
.zip(File.readlines(maps_trans_output_path, chomp: true))]
names_translation_map = Hash[File.readlines(names_output_path, chomp: true)
.zip(File.readlines(names_trans_output_path, chomp: true))]
else
puts "Files aren't already parsed. Continuing as if --append flag was omitted."
processing_type = :default
end
end
# 401 - dialogue lines
# 102 - dialogue choices array
# 402 - one of the dialogue choices from the array
# 356 - system lines/special texts (do they even exist before mv?)
allowed_codes = [401, 102, 402, 356].freeze
maps_object_map.each do |filename, object|
display_name = object.display_name
if display_name.is_a?(String)
display_name = display_name.strip
unless display_name.empty?
names_translation_map.insert_at_index(names_lines.length, display_name, '') if processing_type == :append &&
!names_translation_map.include?(display_name)
names_lines.add(display_name)
end
end
events = object.events
next if events.nil?
events.each_value do |event|
pages = event.pages
next if pages.nil?
pages.each do |page|
list = page.list
next if list.nil?
in_sequence = false
line = []
list.each do |item|
code = item.code
unless allowed_codes.include?(code)
if in_sequence
joined = line.join('\#').strip
parsed = parse_parameter(401, joined, game_type)
maps_translation_map.insert_at_index(maps_lines.length, parsed, '') if processing_type == :append &&
!maps_translation_map.include?(parsed)
maps_lines.add(parsed)
line.clear
in_sequence = false
end
next
end
parameters = item.parameters
if code == 401
next unless parameters[0].is_a?(String) && !parameters[0].empty?
in_sequence = true
line.push(parameters[0])
elsif parameters[0].is_a?(Array)
parameters[0].each do |subparameter|
next unless subparameter.is_a?(String)
subparameter = subparameter.strip
next if subparameter.empty?
parsed = parse_parameter(code, subparameter, game_type)
next if parsed.nil?
maps_translation_map.insert_at_index(maps_lines.length, parsed, '') if processing_type == :append &&
!maps_translation_map.include?(parsed)
maps_lines.add(parsed)
end
elsif parameters[0].is_a?(String)
parameter = parameters[0].strip
next if parameter.empty?
parsed = parse_parameter(code, parameter, game_type)
next if parsed.nil?
parsed = parsed.gsub(/\r?\n/, '\#')
maps_translation_map.insert_at_index(maps_lines.length, parsed, '') if processing_type == :append &&
!maps_translation_map.include?(parsed)
maps_lines.add(parsed)
end
end
end
end
puts "Parsed #{filename}" if logging
end
maps_original_content,
maps_translated_content,
names_original_content,
names_translated_content = if processing_type == :append
[maps_translation_map.keys.join("\n"),
maps_translation_map.values.join("\n"),
names_translation_map.keys.join("\n"),
names_translation_map.values.join("\n")]
else
[maps_lines.join("\n"),
"\n" * (maps_lines.empty? ? 0 : maps_lines.length - 1),
names_lines.join("\n"),
"\n" * (names_lines.empty? ? 0 : names_lines.length - 1)]
end
File.binwrite(maps_output_path, maps_original_content)
File.binwrite(maps_trans_output_path, maps_translated_content)
File.binwrite(names_output_path, names_original_content)
File.binwrite(names_trans_output_path, names_translated_content)
end
# @param [Array<String>] other_files_paths
# @param [String] output_path
# @param [Boolean] logging
# @param [String] game_type
# @param [String] processing_type
def self.read_other(other_files_paths, output_path, logging, game_type, processing_type)
other_object_array_map = Hash[other_files_paths.map do |filename|
[File.basename(filename), Marshal.load(File.binread(filename))]
end]
inner_processing_type = processing_type
# 401 - dialogue lines
# 405 - credits lines
# 102 - dialogue choices array
# 402 - one of the dialogue choices from the array
# 356 - system lines/special texts (do they even exist before mv?)
allowed_codes = [401, 405, 102, 402, 356].freeze
other_object_array_map.each do |filename, other_object_array|
processed_filename = File.basename(filename, '.*').downcase
other_output_path = File.join(output_path, "#{processed_filename}.txt")
other_trans_output_path = File.join(output_path, "#{processed_filename}_trans.txt")
if processing_type == :default && File.exist?(other_trans_output_path)
puts "#{processed_filename}_trans.txt file already exists. If you want to forcefully re-read all files, use --force flag, or --append if you want append new text to already existing files."
next
end
other_lines = IndexSet.new
other_translation_map = nil
if processing_type == :append
if File.exist?(other_trans_output_path)
inner_processing_type == :append
other_translation_map = Hash[File.readlines(other_output_path, chomp: true)
.zip(File.readlines(other_trans_output_path, chomp: true))]
else
puts "Files aren't already parsed. Continuing as if --append flag was omitted."
inner_processing_type = :default
end
end
if !filename.start_with?(/Common|Troops/)
other_object_array.each do |object|
next if object.nil?
name = object.name
nickname = object.nickname
description = object.description
note = object.note
[name, nickname, description, note].each do |variable|
next unless variable.is_a?(String)
variable = variable.strip
next if variable.empty?
parsed = parse_variable(variable, game_type)
next if parsed.nil?
parsed = parsed.gsub(/\r?\n/, '\#')
other_translation_map.insert_at_index(other_lines.length, parsed, '') if inner_processing_type == :append &&
!other_translation_map.include?(parsed)
other_lines.add(parsed)
end
end
else
other_object_array.each do |object|
next if object.nil?
pages = object.pages
pages_length = pages.nil? ? 1 : pages.length
(0..pages_length).each do |i|
list = pages.nil? ? object.list : pages[i].instance_variable_get(:@list)
next if list.nil?
in_sequence = false
line = []
list.each do |item|
code = item.code
unless allowed_codes.include?(code)
if in_sequence
joined = line.join('\#').strip
other_translation_map.insert_at_index(other_lines.length, joined, '') if inner_processing_type == :append &&
!other_translation_map.include?(joined)
other_lines.add(joined)
line.clear
in_sequence = false
end
next
end
parameters = item.parameters
if [401, 405].include?(code)
next unless parameters[0].is_a?(String) && !parameters[0].empty?
in_sequence = true
line.push(parameters[0].gsub(/\r?\n/, '\#'))
elsif parameters[0].is_a?(Array)
parameters[0].each do |subparameter|
next unless subparameter.is_a?(String)
subparameter = subparameter.strip
next if subparameter.empty?
other_translation_map.insert_at_index(other_lines.length, subparameter, '') if inner_processing_type == :append &&
!other_translation_map.include?(subparameter)
other_lines.add(subparameter)
end
elsif parameters[0].is_a?(String)
parameter = parameters[0].strip
next if parameter.empty?
parameter = parameter.gsub(/\r?\n/, '\#')
other_translation_map.insert_at_index(other_lines.length, parameter, '') if inner_processing_type == :append &&
!other_translation_map.include?(parameter)
other_lines.add(parameter)
elsif parameters[1].is_a?(String)
parameter = parameters[1].strip
next if parameter.empty?
parameter = parameter.gsub(/\r?\n/, '\#')
other_translation_map.insert_at_index(other_lines.length, parameter, '') if inner_processing_type == :append &&
!other_translation_map.include?(parameter)
other_lines.add(parameter)
end
end
end
end
end
puts "Parsed #{filename}" if logging
original_content, translated_content = if processing_type == :append
[other_translation_map.keys.join("\n"),
other_translation_map.values.join("\n")]
else
[other_lines.join("\n"),
"\n" * (other_lines.empty? ? 0 : other_lines.length - 1)]
end
File.binwrite(other_output_path, original_content)
File.binwrite(other_trans_output_path, translated_content)
end
end
# @param [String] ini_file_path
def self.read_ini_title(ini_file_path)
file_lines = File.readlines(ini_file_path, chomp: true)
file_lines.each do |line|
if line.downcase.start_with?('title')
parts = line.partition('=')
break parts[2].strip
end
end
end
# @param [String] system_file_path
# @param [String] ini_file_path
# @param [String] output_path
# @param [Boolean] logging
# @param [String] processing_type
def self.read_system(system_file_path, ini_file_path, output_path, logging, processing_type)
system_filename = File.basename(system_file_path)
system_basename = File.basename(system_file_path, '.*').downcase
system_output_path = File.join(output_path, "#{system_basename}.txt")
system_trans_output_path = File.join(output_path, "#{system_basename}_trans.txt")
if processing_type == :default && File.exist?(system_trans_output_path)
puts 'system_trans.txt file already exists. If you want to forcefully re-read all files, use --force flag, or --append if you want append new text to already existing files.'
return
end
system_object = Marshal.load(File.binread(system_file_path))
system_lines = IndexSet.new
system_translation_map = nil
if processing_type == :append
if File.exist?(system_trans_output_path)
system_translation_map = Hash[File.readlines(system_output_path, chomp: true)
.zip(File.readlines(system_trans_output_path, chomp: true))]
else
puts "Files aren't already parsed. Continuing as if --append flag was omitted."
processing_type = :default
end
end
elements = system_object.elements
skill_types = system_object.skill_types
weapon_types = system_object.weapon_types
armor_types = system_object.armor_types
currency_unit = system_object.currency_unit
terms = system_object.terms || system_object.words
[elements, skill_types, weapon_types, armor_types].each do |array|
next if array.nil?
array.each do |string|
next unless string.is_a?(String)
string = string.strip
next if string.empty?
system_translation_map.insert_at_index(system_lines.length, string, '') if processing_type == :append &&
!system_translation_map.include?(string)
system_lines.add(string)
end
end
if currency_unit.is_a?(String)
currency_unit = currency_unit.strip
unless currency_unit.empty?
system_translation_map.insert_at_index(system_lines.length, currency_unit, '') if processing_type == :append &&
!system_translation_map.include?(currency_unit)
system_lines.add(currency_unit)
end
end
terms.instance_variables.each do |variable|
value = terms.instance_variable_get(variable)
if value.is_a?(String)
value = value.strip
unless value.empty?
system_translation_map.insert_at_index(system_lines.length, value, '') if processing_type == :append &&
!system_translation_map.include?(value)
system_lines.add(value)
end
next
end
value.each do |string|
next unless string.is_a?(String)
string = string.strip
next if string.empty?
system_translation_map.insert_at_index(system_lines.length, string, '') if processing_type == :append &&
!system_translation_map.include?(string)
system_lines.add(string)
end
end
# Game title from System file and ini file may differ, but requesting user request to determine which line do they want is LAME
# So just throw that ini ass and continue
ini_game_title = read_ini_title(ini_file_path).strip
system_translation_map.insert_at_index(system_lines.length, ini_game_title, '') if processing_type == :append &&
!system_translation_map.include?(ini_game_title)
system_lines.add(ini_game_title)
puts "Parsed #{system_filename}" if logging
original_content, translated_content = if processing_type == :append
[system_translation_map.keys.join("\n"),
system_translation_map.values.join("\n")]
else
[system_lines.join("\n"),
"\n" * (system_lines.empty? ? 0 : system_lines.length - 1)]
end
File.binwrite(system_output_path, original_content)
File.binwrite(system_trans_output_path, translated_content)
end
# @param [String] scripts_file_path
# @param [String] output_path
# @param [Boolean] logging
# @param [String] processing_type
def self.read_scripts(scripts_file_path, output_path, logging, processing_type)
scripts_filename = File.basename(scripts_file_path)
scripts_basename = File.basename(scripts_file_path, '.*').downcase
scripts_plain_output_path = File.join(output_path, "#{scripts_basename}_plain.txt")
scripts_output_path = File.join(output_path, "#{scripts_basename}.txt")
scripts_trans_output_path = File.join(output_path, "#{scripts_basename}_trans.txt")
if processing_type == :default && File.exist?(scripts_trans_output_path)
puts 'scripts_trans.txt file already exists. If you want to forcefully re-read all files, use --force flag, or --append if you want append new text to already existing files.'
return
end
script_entries = Marshal.load(File.binread(scripts_file_path))
scripts_lines = IndexSet.new
scripts_translation_map = nil
if processing_type == :append
if File.exist?(scripts_trans_output_path)
scripts_translation_map = Hash[File.readlines(scripts_output_path, chomp: true)
.zip(File.readlines(scripts_trans_output_path, chomp: true))]
else
puts "Files aren't already parsed. Continuing as if --append flag was omitted."
processing_type = :default
end
end
codes_content = []
# This code was fun before `that` game used Windows-1252 degree symbol
script_entries.each do |script|
code = Zlib::Inflate.inflate(script[2]).force_encoding('UTF-8')
# we're fucking cloning because of encoding issue
codes_content.push(code.clone)
# I figured how String#encode works - now everything is good
unless code.valid_encoding?
[Encoding::UTF_8, Encoding::WINDOWS_1252, Encoding::SHIFT_JIS].each do |encoding|
encoded = code.encode(code.encoding, encoding)
if encoded.valid_encoding?
code.force_encoding(encoding)
break
end
rescue Encoding::InvalidByteSequenceError
next
end
end
extract_quoted_strings(code).each do |string|
# Removes the U+3000 Japanese typographical space to check if string, when stripped, is truly empty
string = string.strip.delete(' ')
next if string.empty?
# Maybe this mess will remove something that mustn't be removed, but it needs to be tested
next if string.start_with?(/([#!?$@]|(\.\/)?(Graphics|Data|Audio|CG|Movies|Save)\/)/) ||
string.match?(/^[^\p{L}]+$/) ||
string.match?(/^\d+$/) ||
string.match?(/%.*(\d|\+|\*)d\]?:?$/) ||
string.match?(/^\[(ON|OFF)\]$/) ||
string.match?(/^\[\]$/) ||
string.match?(/^(.)\1{2,}$/) ||
string.match?(/^(false|true)$/) ||
string.match?(/^[wr]b$/) ||
string.match?(/^(?=.*\d)[A-Za-z0-9\-]+$/) ||
string.match?(/^[a-z\-()\/ +'&]*$/) ||
string.match?(/^[A-Za-z]+[+-]$/) ||
string.match?(/^[.()+-:;\[\]^~%&!*\/→×??x%▼|]$/) ||
string.match?(/^Tile.*[A-Z]$/) ||
string.match?(/^[a-zA-Z][a-z]+([A-Z][a-z]*)+$/) ||
string.match?(/^Cancel Action$|^Invert$|^End$|^Individual$|^Missed File$|^Bitmap$|^Audio$/) ||
string.match?(/\.(mp3|ogg|jpg|png|ini|txt)$/i) ||
string.match?(/\/(\d.*)?$/) ||
string.match?(/FILE$/) ||
string.match?(/#\{/) ||
string.match?(/(?<!\\)\\(?![\\G#])/) ||
string.match?(/\+?=?=/) ||
string.match?(/[}{_<>]/) ||
string.match?(/r[vx]data/) ||
string.match?(/No such file or directory/) ||
string.match?(/level \*\*/) ||
string.match?(/Courier New|Comic Sans|Lucida|Verdana|Tahoma|Arial|Times New Roman/) ||
string.match?(/Player start location/) ||
string.match?(/Common event call has exceeded/) ||
string.match?(/se-/) ||
string.match?(/Start Pos/) ||
string.match?(/An error has occurred/) ||
string.match?(/Define it first/) ||
string.match?(/Process Skill/) ||
string.match?(/Wpn Only/) ||
string.match?(/Don't Wait/) ||
string.match?(/Clear image/) ||
string.match?(/Can Collapse/)
scripts_translation_map.insert_at_index(scripts_lines.length, string, '') if processing_type == :append &&
!scripts_translation_map.include?(string)
scripts_lines.add(string)
end
end
puts "Parsed #{scripts_filename}" if logging
File.binwrite(scripts_plain_output_path, codes_content.join("\n"))
original_content, translated_content = if processing_type == :append
[scripts_translation_map.keys.join("\n"),
scripts_translation_map.values.join("\n")]
else
[scripts_lines.join("\n"),
"\n" * (scripts_lines.empty? ? 0 : scripts_lines.length - 1)]
end
File.binwrite(scripts_output_path, original_content)
File.binwrite(scripts_trans_output_path, translated_content)
end