mirror of
https://github.com/cyberarm/w3d_hub_linux_launcher.git
synced 2025-12-15 16:52:34 +00:00
709 lines
23 KiB
Ruby
709 lines
23 KiB
Ruby
class W3DHub
|
|
class ApplicationManager
|
|
class Task
|
|
LOG_TAG = "W3DHub::ApplicationManager::Task".freeze
|
|
|
|
class FailFast < RuntimeError
|
|
end
|
|
|
|
include CyberarmEngine::Common
|
|
|
|
attr_reader :app_id, :release_channel, :application, :channel, :target_version,
|
|
:manifests, :packages, :files, :wine_prefix, :status
|
|
|
|
def initialize(app_id, release_channel)
|
|
@app_id = app_id
|
|
@release_channel = release_channel
|
|
|
|
@task_state = :not_started # :not_started, :running, :paused, :halted, :complete, :failed
|
|
|
|
@application = Store.applications.games.find { |g| g.id == app_id }
|
|
@channel = @application.channels.find { |c| c.id == release_channel }
|
|
|
|
@target_version = type == :repairer ? Store.settings[:games][:"#{app_id}_#{@channel.id}"][:installed_version] : @channel.current_version
|
|
|
|
@packages_to_download = []
|
|
@total_bytes_to_download = -1
|
|
@bytes_downloaded = -1
|
|
|
|
@manifests = []
|
|
@files = {}
|
|
@packages = []
|
|
|
|
@wine_prefix = nil
|
|
|
|
@status = Status.new(application: @application, channel: channel) { update_interface_task_status }
|
|
|
|
setup
|
|
end
|
|
|
|
def setup
|
|
end
|
|
|
|
def type
|
|
raise NotImplementedError
|
|
end
|
|
|
|
def state
|
|
@task_state
|
|
end
|
|
|
|
# Start task, inside its own thread
|
|
# FIXME: Ruby 3 has parallelism now: Use a Ractor to do work on a seperate core to
|
|
# prevent the UI from locking up while doing computation heavy work, i.e building
|
|
# list of packages to download
|
|
def start
|
|
@task_state = :running
|
|
|
|
Thread.new do
|
|
# Sync do
|
|
begin
|
|
status = execute_task
|
|
rescue FailFast
|
|
# no-op
|
|
rescue StandardError, ERRNO::EACCES => e
|
|
status = false
|
|
@task_failure_reason = e.message[0..512]
|
|
|
|
logger.error(LOG_TAG) { "Task #{type.inspect} failed for #{@application.name}" }
|
|
logger.error(LOG_TAG) { e }
|
|
end
|
|
|
|
# Force free some bytes
|
|
GC.compact if GC.respond_to?(:compact)
|
|
GC.start
|
|
|
|
@task_state = :failed unless status
|
|
@task_state = :complete unless @task_state == :failed
|
|
|
|
hide_application_taskbar if @task_state == :failed
|
|
send_message_dialog(:failure, "Task #{type.inspect} failed for #{@application.name}", @task_failure_reason) if @task_state == :failed && !@fail_silently
|
|
# end
|
|
end
|
|
end
|
|
|
|
def execute_task
|
|
end
|
|
|
|
# Suspend operation, if possible
|
|
def pause
|
|
@task_state = :paused if pauseable?
|
|
end
|
|
|
|
# Halt operation, if possible
|
|
def stop
|
|
@task_state = :halted if stoppable?
|
|
end
|
|
|
|
def pauseable?
|
|
false
|
|
end
|
|
|
|
def stoppable?
|
|
false
|
|
end
|
|
|
|
def complete?
|
|
@task_state == :complete
|
|
end
|
|
|
|
def failed?
|
|
@task_state == :failed
|
|
end
|
|
|
|
def failure_reason
|
|
@task_failure_reason || ""
|
|
end
|
|
|
|
def fail!(reason = "")
|
|
@task_state = :failed
|
|
@task_failure_reason = reason.to_s
|
|
|
|
hide_application_taskbar
|
|
|
|
raise FailFast, @task_failure_reason
|
|
end
|
|
|
|
def fail_silently!
|
|
@fail_silently = true
|
|
end
|
|
|
|
# Quick checks before network and computational work starts
|
|
def fail_fast
|
|
# Have write permissions to target directory
|
|
path = Cache.install_path(@application, @channel)
|
|
path = Store.settings[:app_install_dir] unless File.exist?(path)
|
|
|
|
begin
|
|
File.write("#{path}/___can_write.wlh", "")
|
|
File.delete("#{path}/___can_write.wlh")
|
|
|
|
Dir.mkdir("#{path}/__can_write")
|
|
Dir.rmdir("#{path}/__can_write")
|
|
rescue Errno::EACCES
|
|
fail!("FAIL FAST: Cannot write to #{path}")
|
|
end
|
|
|
|
# FIXME: Check that there is enough disk space
|
|
|
|
# tar present?
|
|
bsdtar_present = system("#{W3DHub.tar_command} --help")
|
|
fail!("FAIL FAST: `#{W3DHub.tar_command} --help` command failed, #{W3DHub.tar_command} is not installed. Will be unable to unpack packages.") unless bsdtar_present
|
|
|
|
# Wine present?
|
|
if W3DHub.unix?
|
|
wine_present = system("which #{Store.settings[:wine_command]}")
|
|
fail!("FAIL FAST: `which #{Store.settings[:wine_command]}` command failed, wine is not installed. Will be unable to create prefixes or launch games.") unless wine_present
|
|
end
|
|
end
|
|
|
|
def run_on_main_thread(block)
|
|
Store.main_thread_queue << block
|
|
end
|
|
|
|
def send_message_dialog(type, title, message)
|
|
run_on_main_thread(
|
|
proc do
|
|
window.push_state(W3DHub::States::MessageDialog, type: type, title: title, message: message)
|
|
end
|
|
)
|
|
end
|
|
|
|
def update_interface_task_status
|
|
run_on_main_thread(
|
|
proc do
|
|
States::Interface.instance&.interface_task_update_pending = self
|
|
end
|
|
)
|
|
end
|
|
|
|
def show_application_taskbar
|
|
run_on_main_thread(
|
|
proc do
|
|
States::Interface.instance&.show_application_taskbar
|
|
end
|
|
)
|
|
end
|
|
|
|
def hide_application_taskbar
|
|
run_on_main_thread(
|
|
proc do
|
|
States::Interface.instance&.hide_application_taskbar
|
|
end
|
|
)
|
|
end
|
|
|
|
###############
|
|
# Tasks/Steps #
|
|
###############
|
|
|
|
def fetch_manifests
|
|
@status.operations.clear
|
|
@status.label = "Downloading #{@application.name}..."
|
|
@status.value = "Fetching manifests..."
|
|
@status.progress = 0.0
|
|
|
|
@status.step = :fetching_manifests
|
|
|
|
if fetch_manifest("games", app_id, "manifest.xml", @target_version)
|
|
manifest = load_manifest("games", app_id, "manifest.xml", @target_version)
|
|
@manifests << manifest
|
|
|
|
until(manifest.full?)
|
|
fetch_manifest("games", app_id, "manifest.xml", manifest.base_version)
|
|
manifest = load_manifest("games", app_id, "manifest.xml", manifest.base_version)
|
|
manifests << manifest
|
|
end
|
|
end
|
|
|
|
@manifests
|
|
end
|
|
|
|
def build_package_list(manifests)
|
|
@status.operations.clear
|
|
@status.label = "Downloading #{@application.name}..."
|
|
@status.value = "Building package list..."
|
|
@status.progress = 0.0
|
|
|
|
@status.step = :build_package_list
|
|
|
|
packages = []
|
|
|
|
manifests.reverse.each do |manifest|
|
|
logger.info(LOG_TAG) { "#{manifest.game}-#{manifest.type}: #{manifest.version} (#{manifest.base_version})" }
|
|
|
|
manifest.files.each do |file|
|
|
@files["#{file.name}:#{manifest.version}"] = file
|
|
|
|
next if file.removed? # No package data
|
|
|
|
# if file.patch?
|
|
# fail!("#{@application.name} requires patches. Patching is not yet supported.")
|
|
# break
|
|
# end
|
|
|
|
next if packages.detect do |pkg|
|
|
pkg.category == "games" &&
|
|
pkg.subcategory == @app_id &&
|
|
pkg.name == file.package &&
|
|
pkg.version == manifest.version
|
|
end
|
|
|
|
packages.push(
|
|
Api::Package.new(
|
|
{ category: "games", subcategory: @app_id, name: file.package, version: manifest.version }
|
|
)
|
|
)
|
|
|
|
packages.last.is_patch = file if file.patch?
|
|
end
|
|
|
|
# TODO: Dependencies
|
|
end
|
|
|
|
packages
|
|
end
|
|
|
|
def verify_files(manifests, packages)
|
|
@status.operations.clear
|
|
@status.label = "Downloading #{@application.name}..."
|
|
@status.value = "Verifying installed files..."
|
|
@status.progress = 0.0
|
|
|
|
@status.step = :verify_files
|
|
|
|
path = Cache.install_path(@application, @channel)
|
|
accepted_files = {}
|
|
rejected_files = []
|
|
|
|
file_count = manifests.map { |m| m.files.count }.sum
|
|
processed_files = 0
|
|
|
|
folder_exists = File.directory?(path)
|
|
|
|
manifests.each do |manifest|
|
|
break unless folder_exists
|
|
|
|
manifest.files.each do |file|
|
|
safe_file_name = file.name.gsub("\\", "/")
|
|
# Fix borked data -> Data 'cause Windows don't care about capitalization
|
|
safe_file_name.sub!("data/", "Data/") # unless File.exist?("#{path}/#{safe_file_name}")
|
|
|
|
file_path = "#{path}/#{safe_file_name}"
|
|
|
|
processed_files += 1
|
|
@status.progress = processed_files.to_f / file_count
|
|
|
|
next if file.removed_since
|
|
next if accepted_files.key?(safe_file_name)
|
|
|
|
unless File.exist?(file_path)
|
|
rejected_files << { file: file, manifest_version: manifest.version }
|
|
logger.info(LOG_TAG) { "[#{manifest.version}] File missing: #{file_path}" }
|
|
next
|
|
end
|
|
|
|
digest = Digest::SHA256.new
|
|
f = File.open(file_path)
|
|
|
|
while (chunk = f.read(32_000_000))
|
|
digest.update(chunk)
|
|
end
|
|
|
|
f.close
|
|
|
|
logger.info(LOG_TAG) { file.inspect } if file.checksum.nil?
|
|
|
|
if digest.hexdigest.upcase == file.checksum.upcase
|
|
accepted_files[safe_file_name] = manifest.version
|
|
logger.info(LOG_TAG) { "[#{manifest.version}] Verified file: #{file_path}" }
|
|
else
|
|
rejected_files << { file: file, manifest_version: manifest.version }
|
|
logger.info(LOG_TAG) { "[#{manifest.version}] File failed Verification: #{file_path}" }
|
|
end
|
|
end
|
|
end
|
|
|
|
logger.info(LOG_TAG) { "#{rejected_files.count} missing or corrupt files" }
|
|
|
|
selected_packages = []
|
|
selected_packages_hash = {}
|
|
|
|
rejected_files.each do |hash|
|
|
next if selected_packages_hash["#{hash[:file].package}_#{hash[:manifest_version]}"]
|
|
|
|
package = packages.find { |pkg| pkg.name == hash[:file].package && pkg.version == hash[:manifest_version] }
|
|
|
|
if package
|
|
selected_packages_hash["#{hash[:file].package}_#{hash[:manifest_version]}"] = true
|
|
selected_packages << package
|
|
else
|
|
raise "missing package: #{hash[:file].package}:#{hash[:manifest_version]} in fetched packages list!"
|
|
end
|
|
end
|
|
|
|
# Removed packages that don't need to be fetched or processed
|
|
packages.delete_if { |package| !selected_packages.find { |pkg| pkg == package } } if folder_exists
|
|
|
|
packages
|
|
end
|
|
|
|
def fetch_packages(packages)
|
|
hashes = packages.map do |pkg|
|
|
{
|
|
category: pkg.category,
|
|
subcategory: pkg.subcategory,
|
|
name: "#{pkg.name}.zip",
|
|
version: pkg.version
|
|
}
|
|
end
|
|
|
|
package_details = Api.package_details(hashes)
|
|
|
|
if package_details
|
|
@packages = [package_details].flatten
|
|
@packages.each do |rich|
|
|
package = packages.find do |pkg|
|
|
pkg.category == rich.category &&
|
|
pkg.subcategory == rich.subcategory &&
|
|
"#{pkg.name}.zip" == rich.name &&
|
|
pkg.version == rich.version
|
|
end
|
|
|
|
package.instance_variable_set(:"@name", rich.name)
|
|
package.instance_variable_set(:"@size", rich.size)
|
|
package.instance_variable_set(:"@checksum", rich.checksum)
|
|
package.instance_variable_set(:"@checksum_chunk_size", rich.checksum_chunk_size)
|
|
package.instance_variable_set(:"@checksum_chunks", rich.checksum_chunks)
|
|
end
|
|
|
|
@packages_to_download = []
|
|
|
|
@status.label = "Downloading #{@application.name}..."
|
|
@status.value = "Verifying local packages..."
|
|
@status.progress = 0.0
|
|
|
|
package_details.each do |pkg|
|
|
@status.operations[:"#{pkg.checksum}"] = Status::Operation.new(
|
|
label: pkg.name,
|
|
value: "Pending...",
|
|
progress: 0.0
|
|
)
|
|
end
|
|
|
|
@status.step = :prefetch_verifying_packages
|
|
|
|
package_details.each_with_index.each do |pkg, i|
|
|
operation = @status.operations[:"#{pkg.checksum}"]
|
|
|
|
if verify_package(pkg)
|
|
operation.value = "Verified"
|
|
operation.progress = 1.0
|
|
else
|
|
@packages_to_download << pkg
|
|
|
|
operation.value = "#{W3DHub.format_size(pkg.custom_partially_valid_at_bytes)} / #{W3DHub.format_size(pkg.size)}"
|
|
operation.progress = pkg.custom_partially_valid_at_bytes.to_f / pkg.size
|
|
end
|
|
|
|
@status.progress = i.to_f / package_details.count
|
|
|
|
update_interface_task_status
|
|
end
|
|
|
|
@status.operations.delete_if { |key, o| o.progress >= 1.0 }
|
|
|
|
@status.step = :fetch_packages
|
|
|
|
@total_bytes_to_download = @packages_to_download.sum { |pkg| pkg.size - pkg.custom_partially_valid_at_bytes }
|
|
@bytes_downloaded = 0
|
|
|
|
pool = Pool.new(workers: Store.settings[:parallel_downloads])
|
|
|
|
@packages_to_download.each do |pkg|
|
|
pool.add_job Pool::Job.new( proc {
|
|
package_bytes_downloaded = pkg.custom_partially_valid_at_bytes
|
|
|
|
package_fetch(pkg) do |chunk, remaining_bytes, total_bytes|
|
|
@bytes_downloaded += chunk.to_s.length
|
|
package_bytes_downloaded += chunk.to_s.length
|
|
|
|
@status.value = "#{W3DHub.format_size(@bytes_downloaded)} / #{W3DHub.format_size(@total_bytes_to_download)}"
|
|
@status.progress = @bytes_downloaded.to_f / @total_bytes_to_download
|
|
|
|
operation = @status.operations[:"#{pkg.checksum}"]
|
|
operation.value = "#{W3DHub.format_size(package_bytes_downloaded)} / #{W3DHub.format_size(pkg.size)}"
|
|
operation.progress = package_bytes_downloaded.to_f / pkg.size # total_bytes
|
|
|
|
update_interface_task_status
|
|
end
|
|
})
|
|
end
|
|
|
|
pool.manage_pool
|
|
else
|
|
fail!("Failed to fetch package details")
|
|
end
|
|
end
|
|
|
|
def verify_packages(packages)
|
|
end
|
|
|
|
def unpack_packages(packages)
|
|
path = Cache.install_path(@application, @channel)
|
|
logger.info(LOG_TAG) { "Unpacking packages in '#{path}'..." }
|
|
Cache.create_directories(path, true)
|
|
|
|
@status.operations.clear
|
|
@status.label = "Installing #{@application.name}..."
|
|
@status.value = "Unpacking..."
|
|
@status.progress = 0.0
|
|
|
|
packages.each do |pkg|
|
|
# FIXME: can't add a new key into hash during iteration (RuntimeError)
|
|
@status.operations[:"#{pkg.checksum}"] = Status::Operation.new(
|
|
label: pkg.name,
|
|
value: "Pending...",
|
|
progress: 0.0
|
|
)
|
|
end
|
|
|
|
@status.step = :unpacking
|
|
|
|
i = -1
|
|
packages.each do |package|
|
|
i += 1
|
|
|
|
status = if package.custom_is_patch
|
|
@status.operations[:"#{package.checksum}"].value = "Patching..."
|
|
@status.operations[:"#{package.checksum}"].progress = Float::INFINITY
|
|
@status.progress = i.to_f / packages.count
|
|
update_interface_task_status
|
|
|
|
apply_patch(package, path)
|
|
else
|
|
@status.operations[:"#{package.checksum}"].value = "Unpacking..."
|
|
@status.operations[:"#{package.checksum}"].progress = Float::INFINITY
|
|
@status.progress = i.to_f / packages.count
|
|
update_interface_task_status
|
|
|
|
unpack_package(package, path)
|
|
end
|
|
|
|
repair_windows_case_insensitive(package, path)
|
|
|
|
if status
|
|
@status.operations[:"#{package.checksum}"].value = package.custom_is_patch ? "Patched" : "Unpacked"
|
|
@status.operations[:"#{package.checksum}"].progress = 1.0
|
|
|
|
update_interface_task_status
|
|
else
|
|
logger.info(LOG_TAG) { "COMMAND FAILED!" }
|
|
fail!("Failed to unpack #{package.name}")
|
|
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
def create_wine_prefix
|
|
if W3DHub.unix? && @wine_prefix
|
|
# TODO: create a wine prefix if configured
|
|
@status.operations.clear
|
|
@status.label = "Installing #{@application.name}..."
|
|
@status.value = "Creating wine prefix..."
|
|
@status.progress = 0.0
|
|
|
|
@status.step = :create_wine_prefix
|
|
end
|
|
end
|
|
|
|
def install_dependencies(packages)
|
|
# TODO: install dependencies
|
|
@status.operations.clear
|
|
@status.label = "Installing #{@application.name}..."
|
|
@status.value = "Installing dependencies..."
|
|
@status.progress = 0.0
|
|
|
|
@status.step = :install_dependencies
|
|
end
|
|
|
|
def mark_application_installed
|
|
Store.application_manager.installed!(self)
|
|
|
|
@status.operations.clear
|
|
@status.label = "Installed #{@application.name}"
|
|
@status.value = ""
|
|
@status.progress = 1.0
|
|
|
|
@status.step = :mark_application_installed
|
|
|
|
logger.info(LOG_TAG) { "#{@app_id} has been installed." }
|
|
end
|
|
|
|
#############
|
|
# Functions #
|
|
#############
|
|
|
|
def fetch_manifest(category, subcategory, name, version, &block)
|
|
# Check for and integrity of local manifest
|
|
|
|
package = nil
|
|
array = Api.package_details([{ category: category, subcategory: subcategory, name: name, version: version }])
|
|
if array.is_a?(Array)
|
|
package = array.first
|
|
else
|
|
fail!("Failed to fetch manifest package details!")
|
|
return
|
|
end
|
|
|
|
if File.exist?(Cache.package_path(category, subcategory, name, version))
|
|
verified = verify_package(package)
|
|
|
|
# download manifest if not valid
|
|
package_fetch(package) unless verified
|
|
true if verified
|
|
else
|
|
# download manifest if not cached
|
|
package_fetch(package)
|
|
end
|
|
end
|
|
|
|
def package_fetch(package, &block)
|
|
logger.info(LOG_TAG) { "Downloading: #{package.category}:#{package.subcategory}:#{package.name}-#{package.version}" }
|
|
|
|
Api.package(package) do |chunk, remaining_bytes, total_bytes|
|
|
block&.call(chunk, remaining_bytes, total_bytes)
|
|
end
|
|
end
|
|
|
|
def verify_package(package, &block)
|
|
logger.info(LOG_TAG) { "Verifying: #{package.category}:#{package.subcategory}:#{package.name}-#{package.version}" }
|
|
|
|
digest = Digest::SHA256.new
|
|
path = Cache.package_path(package.category, package.subcategory, package.name, package.version)
|
|
|
|
return false unless File.exist?(path)
|
|
|
|
operation = @status.operations[:"#{package.checksum}"]
|
|
operation&.value = "Verifying..."
|
|
|
|
file_size = File.size(path)
|
|
logger.info(LOG_TAG) { " File size: #{file_size}" }
|
|
chunk_size = package.checksum_chunk_size
|
|
chunks = package.checksum_chunks.size
|
|
|
|
File.open(path) do |f|
|
|
i = -1
|
|
package.checksum_chunks.each do |chunk_start, checksum|
|
|
i += 1
|
|
operation&.progress = i.to_f / chunks
|
|
update_interface_task_status
|
|
|
|
chunk_start = Integer(chunk_start.to_s)
|
|
|
|
read_length = chunk_size
|
|
read_length = file_size - chunk_start if chunk_start + chunk_size > file_size
|
|
|
|
break if (file_size - chunk_start).negative?
|
|
|
|
f.seek(chunk_start)
|
|
|
|
chunk = f.read(read_length)
|
|
digest.update(chunk)
|
|
|
|
if Digest::SHA256.new.hexdigest(chunk).upcase == checksum.upcase
|
|
valid_at = chunk_start + read_length
|
|
# logger.debug(LOG_TAG) { " Passed chunk: #{chunk_start}" } # Only enable when deep diving to find a bug (VERBOSE)
|
|
# package.partially_valid_at_bytes = valid_at
|
|
package.partially_valid_at_bytes = chunk_start
|
|
else
|
|
logger.info(LOG_TAG) { " FAILED chunk: #{chunk_start}" }
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
digest.hexdigest.upcase == package.checksum.upcase
|
|
end
|
|
|
|
def load_manifest(category, subcategory, name, version)
|
|
Manifest.new(category, subcategory, name, version)
|
|
end
|
|
|
|
def unpack_package(package, path)
|
|
logger.info(LOG_TAG) { " #{package.name}:#{package.version}" }
|
|
package_path = Cache.package_path(package.category, package.subcategory, package.name, package.version)
|
|
|
|
logger.info(LOG_TAG) { " Running #{W3DHub.tar_command} command: #{W3DHub.tar_command} -xf \"#{package_path}\" -C \"#{path}\"" }
|
|
return system("#{W3DHub.tar_command} -xf \"#{package_path}\" -C \"#{path}\"")
|
|
end
|
|
|
|
def apply_patch(package, path)
|
|
logger.info(LOG_TAG) { " #{package.name}:#{package.version}" }
|
|
package_path = Cache.package_path(package.category, package.subcategory, package.name, package.version)
|
|
temp_path = "#{Store.settings[:package_cache_dir]}/temp"
|
|
manifest_file = package.custom_is_patch
|
|
|
|
Cache.create_directories(temp_path, true)
|
|
|
|
logger.info(LOG_TAG) { " Running #{W3DHub.tar_command} command: #{W3DHub.tar_command} -xf \"#{package_path}\" -C \"#{temp_path}\"" }
|
|
system("#{W3DHub.tar_command} -xf \"#{package_path}\" -C \"#{temp_path}\"")
|
|
|
|
logger.info(LOG_TAG) { " Loading #{temp_path}/#{manifest_file.name}.patch..." }
|
|
patch_mix = W3DHub::Mixer::Reader.new(file_path: "#{temp_path}/#{manifest_file.name}.patch", ignore_crc_mismatches: false)
|
|
patch_info = JSON.parse(patch_mix.package.files.find { |f| f.name == ".w3dhub.patch" || f.name == ".bhppatch" }.data, symbolize_names: true)
|
|
|
|
repaired_path = "#{path}/#{manifest_file.name}"
|
|
# Fix borked data -> Data 'cause Windows don't care about capitalization
|
|
repaired_path = "#{path}/#{manifest_file.name.sub('data', 'Data')}" unless File.exist?(repaired_path) && path
|
|
|
|
logger.info(LOG_TAG) { " Loading #{repaired_path}..." }
|
|
target_mix = W3DHub::Mixer::Reader.new(file_path: repaired_path, ignore_crc_mismatches: false)
|
|
|
|
logger.info(LOG_TAG) { " Removing files..." } if patch_info[:removedFiles].size.positive?
|
|
patch_info[:removedFiles].each do |file|
|
|
logger.debug(LOG_TAG) { " #{file}" }
|
|
target_mix.package.files.delete_if { |f| f.name == file }
|
|
end
|
|
|
|
logger.info(LOG_TAG) { " Adding/Updating files..." } if patch_info[:updatedFiles].size.positive?
|
|
patch_info[:updatedFiles].each do |file|
|
|
logger.debug(LOG_TAG) { " #{file}" }
|
|
|
|
patch = patch_mix.package.files.find { |f| f.name == file }
|
|
target = target_mix.package.files.find { |f| f.name == file }
|
|
|
|
if target
|
|
target_mix.package.files[target_mix.package.files.index(target)] = patch
|
|
else
|
|
target_mix.package.files << patch
|
|
end
|
|
end
|
|
|
|
logger.info(LOG_TAG) { " Writing updated #{repaired_path}..." } if patch_info[:updatedFiles].size.positive?
|
|
W3DHub::Mixer::Writer.new(file_path: repaired_path, package: target_mix.package, memory_buffer: true)
|
|
|
|
FileUtils.remove_dir(temp_path)
|
|
|
|
true
|
|
end
|
|
|
|
def repair_windows_case_insensitive(package, path)
|
|
# Windows is just confused
|
|
return true if W3DHub.windows?
|
|
|
|
# Force data/ to Data/
|
|
return true unless File.exist?("#{path}/data") && File.directory?("#{path}/data")
|
|
|
|
logger.info(LOG_TAG) { " Moving #{path}/data/ to #{path}/Data/" }
|
|
|
|
FileUtils.mv(Dir.glob("#{path}/data/**"), "#{path}/Data", force: true)
|
|
FileUtils.remove_dir("#{path}/data", force: true)
|
|
|
|
true
|
|
end
|
|
end
|
|
end
|
|
end
|