diff --git a/lib/granblue/downloaders/base_downloader.rb b/lib/granblue/downloaders/base_downloader.rb index 09aaac8..a740adc 100644 --- a/lib/granblue/downloaders/base_downloader.rb +++ b/lib/granblue/downloaders/base_downloader.rb @@ -2,9 +2,39 @@ module Granblue module Downloaders + # Abstract base class for downloading game asset images in various sizes. + # Handles local and S3 storage, with support for test mode and verbose logging. + # + # @abstract Subclass must implement {#object_type}, {#base_url}, and {#directory_for_size} + # + # @example Downloading assets for a specific ID + # class MyDownloader < BaseDownloader + # def object_type; "weapon"; end + # def base_url; "http://example.com/assets"; end + # def directory_for_size(size) + # case size + # when "main" then "large" + # when "grid" then "medium" + # when "square" then "small" + # end + # end + # end + # + # downloader = MyDownloader.new("1234", storage: :both) + # downloader.download + # + # @note Supports three image sizes: main, grid, and square + # @note Can store images locally, in S3, or both class BaseDownloader + # @return [Array] Available image size variants SIZES = %w[main grid square].freeze + # Initialize a new downloader instance + # @param id [String] ID of the object to download images for + # @param test_mode [Boolean] When true, only logs actions without downloading + # @param verbose [Boolean] When true, enables detailed logging + # @param storage [Symbol] Storage mode (:local, :s3, or :both) + # @return [void] def initialize(id, test_mode: false, verbose: false, storage: :both) @id = id @base_url = base_url @@ -15,6 +45,8 @@ module Granblue ensure_directories_exist unless @test_mode end + # Download images for all sizes + # @return [void] def download log_info "-> #{@id}" return if @test_mode @@ -28,6 +60,12 @@ module Granblue private + # Process download for a specific size variant + # @param url [String] URL to download from + # @param size [String] Size variant being processed + # @param path [String] Local path for download + # @param last [Boolean] Whether this is the last size being processed + # @return [void] def process_download(url, size, path, last: false) filename = File.basename(url) s3_key = build_s3_key(size, filename) @@ -54,11 +92,19 @@ module Granblue log_info "\t404 returned\t#{url}" end + # Download file to local storage + # @param url [String] Source URL + # @param download_uri [String] Local destination path + # @return [void] def download_to_local(url, download_uri) download = URI.parse(url).open IO.copy_stream(download, download_uri) end + # Stream file directly to S3 + # @param url [String] Source URL + # @param s3_key [String] S3 object key + # @return [void] def stream_to_s3(url, s3_key) return if @aws_service.file_exists?(s3_key) @@ -67,6 +113,11 @@ module Granblue end end + # Download file to both local storage and S3 + # @param url [String] Source URL + # @param download_uri [String] Local destination path + # @param s3_key [String] S3 object key + # @return [void] def download_to_both(url, download_uri, s3_key) download = URI.parse(url).open @@ -82,17 +133,23 @@ module Granblue end end + # Check if file should be downloaded based on storage mode + # @param local_path [String] Local file path + # @param s3_key [String] S3 object key + # @return [Boolean] true if file should be downloaded def should_download?(local_path, s3_key) - case @storage - when :local + if @storage == :local !File.exist?(local_path) - when :s3 + elsif @storage == :s3 !@aws_service.file_exists?(s3_key) - when :both + else + # :both !File.exist?(local_path) || !@aws_service.file_exists?(s3_key) end end + # Ensure local directories exist for each size + # @return [void] def ensure_directories_exist return unless store_locally? @@ -101,45 +158,85 @@ module Granblue end end + # Check if local storage is being used + # @return [Boolean] true if storing locally def store_locally? %i[local both].include?(@storage) end + # Get local download path for a size + # @param size [String] Image size variant + # @return [String] Local directory path def download_path(size) "#{Rails.root}/download/#{object_type}-#{size}" end + # Build S3 key for an image + # @param size [String] Image size variant + # @param filename [String] Image filename + # @return [String] Complete S3 key def build_s3_key(size, filename) "#{object_type}-#{size}/#{filename}" end + # Log informational message if verbose + # @param message [String] Message def log_info(message) puts message if @verbose end + # Download elemental variant image + # @param url [String] Source URL + # @param size [String] Image size variant + # @param path [String] Destination path + # @param filename [String] Image filename + # @return [void] def download_elemental_image(url, size, path, filename) return if @test_mode filepath = "#{path}/#{filename}" - download = URI.parse(url).open + URI.open(url) do |file| + content = file.read + if content + File.open(filepath, 'wb') do |output| + output.write(content) + end + else + raise "Failed to read content from #{url}" + end + end log_info "-> #{size}:\t#{url}..." - IO.copy_stream(download, filepath) rescue OpenURI::HTTPError log_info "\t404 returned\t#{url}" + rescue StandardError => e + log_info "\tError downloading #{url}: #{e.message}" end + # Get asset type (e.g., "weapon", "character") + # @abstract + # @return [String] Asset type name def object_type raise NotImplementedError, 'Subclasses must define object_type' end + # Get base URL for assets + # @abstract + # @return [String] Base URL def base_url raise NotImplementedError, 'Subclasses must define base_url' end + # Get directory name for a size variant + # @abstract + # @param size [String] Image size variant + # @return [String] Directory name def directory_for_size(size) raise NotImplementedError, 'Subclasses must define directory_for_size' end + # Build complete URL for a size variant + # @param size [String] Image size variant + # @return [String] Complete download URL def build_url(size) directory = directory_for_size(size) "#{@base_url}/#{directory}/#{@id}.jpg" diff --git a/lib/granblue/downloaders/character_downloader.rb b/lib/granblue/downloaders/character_downloader.rb index 825b1d8..5c45c3c 100644 --- a/lib/granblue/downloaders/character_downloader.rb +++ b/lib/granblue/downloaders/character_downloader.rb @@ -2,7 +2,23 @@ module Granblue module Downloaders + # Downloads character image assets from the game server in different sizes and variants. + # Handles character-specific variants like base art, uncap art, and transcendence art. + # + # @example Download images for a specific character + # downloader = CharacterDownloader.new("3040001000", storage: :both) + # downloader.download + # + # @note Character images come in multiple variants (_01, _02, etc.) based on uncap status + # @note Supports FLB (5★) and ULB (6★) art variants when available class CharacterDownloader < BaseDownloader + # Downloads images for all variants of a character based on their uncap status. + # Overrides {BaseDownloader#download} to handle character-specific variants. + # + # @return [void] + # @note Skips download if character is not found in database + # @note Downloads FLB/ULB variants only if character has those uncaps + # @see #download_variants def download character = Character.find_by(granblue_id: @id) return unless character @@ -12,9 +28,13 @@ module Granblue private + # Downloads all variants of a character's images + # @param character [Character] Character model instance to download images for + # @return [void] + # @note Only downloads variants that should exist based on character uncap status def download_variants(character) # All characters have 01 and 02 variants - variants = ["#{@id}_01", "#{@id}_02"] + variants = %W[#{@id}_01 #{@id}_02] # Add FLB variant if available variants << "#{@id}_03" if character.flb @@ -29,6 +49,9 @@ module Granblue end end + # Downloads a specific variant's images in all sizes + # @param variant_id [String] Character variant ID (e.g., "3040001000_01") + # @return [void] def download_variant(variant_id) log_info "-> #{variant_id}" if @verbose return if @test_mode @@ -40,19 +63,31 @@ module Granblue end end + # Builds URL for a specific variant and size + # @param variant_id [String] Character variant ID + # @param size [String] Image size variant ("main", "grid", or "square") + # @return [String] Complete URL for downloading the image def build_variant_url(variant_id, size) directory = directory_for_size(size) "#{@base_url}/#{directory}/#{variant_id}.jpg" end + # Gets object type for file paths and storage keys + # @return [String] Returns "character" def object_type 'character' end + # Gets base URL for character assets + # @return [String] Base URL for character images def base_url 'http://gbf.game-a.mbga.jp/assets/img/sp/assets/npc' end + # Gets directory name for a size variant + # @param size [String] Image size variant + # @return [String] Directory name in game asset URL structure + # @note Maps "main" -> "f", "grid" -> "m", "square" -> "s" def directory_for_size(size) case size.to_s when 'main' then 'f' diff --git a/lib/granblue/downloaders/download_manager.rb b/lib/granblue/downloaders/download_manager.rb index db2cfe8..f3edde2 100644 --- a/lib/granblue/downloaders/download_manager.rb +++ b/lib/granblue/downloaders/download_manager.rb @@ -2,8 +2,41 @@ module Granblue module Downloaders + # Manages downloading of game assets by coordinating different downloader types. + # Provides a single interface for downloading any type of game asset. + # + # @example Download character images + # DownloadManager.download_for_object('character', '3040001000', storage: :s3) + # + # @example Download weapon images in test mode + # DownloadManager.download_for_object('weapon', '1040001000', test_mode: true, verbose: true) + # + # @note Automatically selects the appropriate downloader based on object type + # @note Handles configuration of downloader options consistently across types class DownloadManager class << self + # Downloads assets for a specific game object using the appropriate downloader + # + # @param type [String] Type of game object ('character', 'weapon', or 'summon') + # @param granblue_id [String] Game ID of the object to download assets for + # @param test_mode [Boolean] When true, simulates downloads without actually downloading + # @param verbose [Boolean] When true, enables detailed logging + # @param storage [Symbol] Storage mode to use (:local, :s3, or :both) + # @return [void] + # + # @example Download character images to S3 + # DownloadManager.download_for_object('character', '3040001000', storage: :s3) + # + # @example Test weapon downloads with verbose logging + # DownloadManager.download_for_object('weapon', '1040001000', + # test_mode: true, + # verbose: true + # ) + # + # @note Logs warning if object type is unknown + # @see CharacterDownloader + # @see WeaponDownloader + # @see SummonDownloader def download_for_object(type, granblue_id, test_mode: false, verbose: false, storage: :both) downloader_options = { test_mode: test_mode, diff --git a/lib/granblue/downloaders/elemental_weapon_downloader.rb b/lib/granblue/downloaders/elemental_weapon_downloader.rb index 2525c3d..8e3f2b0 100644 --- a/lib/granblue/downloaders/elemental_weapon_downloader.rb +++ b/lib/granblue/downloaders/elemental_weapon_downloader.rb @@ -4,13 +4,31 @@ require_relative 'weapon_downloader' module Granblue module Downloaders + # Specialized downloader for handling elemental weapon variants. + # Some weapons have different art for each element, requiring multiple downloads. + # + # @example Download all elemental variants + # downloader = ElementalWeaponDownloader.new(1040001000) + # downloader.download + # + # @note Handles weapons that have variants for all six elements + # @note Uses specific suffix mappings for element art variants class ElementalWeaponDownloader < WeaponDownloader + # Element variant suffix mapping + # @return [Array] Ordered list of suffixes for element variants SUFFIXES = [2, 3, 4, 1, 6, 5].freeze + # Initialize downloader with base weapon ID + # @param id_base [Integer] Base ID for the elemental weapon series + # @return [void] def initialize(id_base) @id_base = id_base.to_i end + # Downloads all elemental variants of the weapon + # @return [void] + # @note Downloads variants for all six elements + # @note Uses progress reporter to show download status def download (1..6).each do |i| id = @id_base + (i - 1) * 100 diff --git a/lib/granblue/downloaders/summon_downloader.rb b/lib/granblue/downloaders/summon_downloader.rb index 20fcfc3..0ac6493 100644 --- a/lib/granblue/downloaders/summon_downloader.rb +++ b/lib/granblue/downloaders/summon_downloader.rb @@ -2,7 +2,23 @@ module Granblue module Downloaders + # Downloads summon image assets from the game server in different sizes and variants. + # Handles summon-specific variants like base art, ULB art, and transcendence art. + # + # @example Download images for a specific summon + # downloader = SummonDownloader.new("2040001000", storage: :both) + # downloader.download + # + # @note Summon images come in multiple variants based on uncap status + # @note Supports ULB (5★) and transcendence variants when available class SummonDownloader < BaseDownloader + # Downloads images for all variants of a summon based on their uncap status. + # Overrides {BaseDownloader#download} to handle summon-specific variants. + # + # @return [void] + # @note Skips download if summon is not found in database + # @note Downloads ULB and transcendence variants only if summon has those uncaps + # @see #download_variants def download summon = Summon.find_by(granblue_id: @id) return unless summon @@ -12,6 +28,11 @@ module Granblue private + # Downloads all variants of a summon's images + # @param summon [Summon] Summon model instance to download images for + # @return [void] + # @note Only downloads variants that should exist based on summon uncap status + # @note Handles special transcendence art variants for 6★ summons def download_variants(summon) # All summons have base variant variants = [@id] @@ -31,6 +52,10 @@ module Granblue end end + # Downloads a specific variant's images in all sizes + # @param variant_id [String] Summon variant ID (e.g., "2040001000_02") + # @return [void] + # @note Downloads all size variants (main/grid/square) for the given variant def download_variant(variant_id) log_info "-> #{variant_id}" if @verbose return if @test_mode @@ -42,19 +67,31 @@ module Granblue end end + # Builds URL for a specific variant and size + # @param variant_id [String] Summon variant ID + # @param size [String] Image size variant ("main", "grid", or "square") + # @return [String] Complete URL for downloading the image def build_variant_url(variant_id, size) directory = directory_for_size(size) "#{@base_url}/#{directory}/#{variant_id}.jpg" end + # Gets object type for file paths and storage keys + # @return [String] Returns "summon" def object_type 'summon' end + # Gets base URL for summon assets + # @return [String] Base URL for summon images def base_url 'http://gbf.game-a.mbga.jp/assets/img/sp/assets/summon' end + # Gets directory name for a size variant + # @param size [String] Image size variant + # @return [String] Directory name in game asset URL structure + # @note Maps "main" -> "party_main", "grid" -> "party_sub", "square" -> "s" def directory_for_size(size) case size.to_s when 'main' then 'party_main' diff --git a/lib/granblue/downloaders/weapon_downloader.rb b/lib/granblue/downloaders/weapon_downloader.rb index 66aeae2..d380a32 100644 --- a/lib/granblue/downloaders/weapon_downloader.rb +++ b/lib/granblue/downloaders/weapon_downloader.rb @@ -2,7 +2,24 @@ module Granblue module Downloaders + # Downloads weapon image assets from the game server in different sizes and variants. + # Handles weapon-specific variants like base art, transcendence art, and elemental variants. + # + # @example Download images for a specific weapon + # downloader = WeaponDownloader.new("1040001000", storage: :both) + # downloader.download + # + # @note Weapon images come in multiple variants based on uncap and element status + # @note Supports transcendence variants and element-specific variants + # @see ElementalWeaponDownloader for handling multi-element weapons class WeaponDownloader < BaseDownloader + # Downloads images for all variants of a weapon based on their uncap status. + # Overrides {BaseDownloader#download} to handle weapon-specific variants. + # + # @return [void] + # @note Skips download if weapon is not found in database + # @note Downloads transcendence variants only if weapon has those uncaps + # @see #download_variants def download weapon = Weapon.find_by(granblue_id: @id) return unless weapon @@ -12,6 +29,11 @@ module Granblue private + # Downloads all variants of a weapon's images + # @param weapon [Weapon] Weapon model instance to download images for + # @return [void] + # @note Only downloads variants that should exist based on weapon uncap status + # @note Handles special transcendence art variants for transcendable weapons def download_variants(weapon) # All weapons have base variant variants = [@id] @@ -28,6 +50,10 @@ module Granblue end end + # Downloads a specific variant's images in all sizes + # @param variant_id [String] Weapon variant ID (e.g., "1040001000_02") + # @return [void] + # @note Downloads all size variants (main/grid/square) for the given variant def download_variant(variant_id) log_info "-> #{variant_id}" if @verbose return if @test_mode @@ -39,19 +65,31 @@ module Granblue end end + # Builds URL for a specific variant and size + # @param variant_id [String] Weapon variant ID + # @param size [String] Image size variant ("main", "grid", or "square") + # @return [String] Complete URL for downloading the image def build_variant_url(variant_id, size) directory = directory_for_size(size) "#{@base_url}/#{directory}/#{variant_id}.jpg" end + # Gets object type for file paths and storage keys + # @return [String] Returns "weapon" def object_type 'weapon' end + # Gets base URL for weapon assets + # @return [String] Base URL for weapon images def base_url 'http://gbf.game-a.mbga.jp/assets/img/sp/assets/weapon' end + # Gets directory name for a size variant + # @param size [String] Image size variant + # @return [String] Directory name in game asset URL structure + # @note Maps "main" -> "ls", "grid" -> "m", "square" -> "s" def directory_for_size(size) case size.to_s when 'main' then 'ls' diff --git a/sig/granblue/downloaders/base_downloader.rbs b/sig/granblue/downloaders/base_downloader.rbs new file mode 100644 index 0000000..9174c1b --- /dev/null +++ b/sig/granblue/downloaders/base_downloader.rbs @@ -0,0 +1,53 @@ +module Granblue + module Downloaders + class BaseDownloader + SIZES: Array[String] + + # Define allowed storage types + type storage = :local | :s3 | :both + + @id: String + @base_url: String + @test_mode: bool + @verbose: bool + @storage: storage + @aws_service: AwsService + + def initialize: (String id, ?test_mode: bool, ?verbose: bool, ?storage: storage) -> void + + def download: -> void + + private + + def process_download: (String url, String size, String path, ?last: bool) -> void + + def download_to_local: (String url, String download_uri) -> void + + def stream_to_s3: (String url, String s3_key) -> void + + def download_to_both: (String url, String download_uri, String s3_key) -> void + + def should_download?: (String local_path, String s3_key) -> bool + + def ensure_directories_exist: -> void + + def store_locally?: -> bool + + def download_path: (String size) -> String + + def build_s3_key: (String size, String filename) -> String + + def log_info: (String message) -> void + + def download_elemental_image: (String url, String size, String path, String filename) -> void + + def object_type: -> String + + def base_url: -> String + + def directory_for_size: (String size) -> String + + def build_url: (String size) -> String + end + end +end diff --git a/sig/granblue/downloaders/character_downloader.rbs b/sig/granblue/downloaders/character_downloader.rbs new file mode 100644 index 0000000..b6dc2b7 --- /dev/null +++ b/sig/granblue/downloaders/character_downloader.rbs @@ -0,0 +1,28 @@ +module Granblue + module Downloaders + class CharacterDownloader < BaseDownloader + private + + def download_variants: (Character character) -> void + + def download_variant: (String variant_id) -> void + + def build_variant_url: (String variant_id, String size) -> String + + def object_type: -> String + + def base_url: -> String + + def directory_for_size: (String size) -> String + + private + + @id: String + @base_url: String + @test_mode: bool + @verbose: bool + @storage: Symbol + @aws_service: AwsService + end + end +end diff --git a/sig/granblue/downloaders/download_manager.rbs b/sig/granblue/downloaders/download_manager.rbs new file mode 100644 index 0000000..52d92c2 --- /dev/null +++ b/sig/granblue/downloaders/download_manager.rbs @@ -0,0 +1,15 @@ +module Granblue + module Downloaders + class DownloadManager + def self.download_for_object: ( + String type, + String granblue_id, + ?test_mode: bool, + ?verbose: bool, + ?storage: Symbol + ) -> void + + private + end + end +end diff --git a/sig/granblue/downloaders/summon_downloader.rbs b/sig/granblue/downloaders/summon_downloader.rbs new file mode 100644 index 0000000..7cac4eb --- /dev/null +++ b/sig/granblue/downloaders/summon_downloader.rbs @@ -0,0 +1,30 @@ +module Granblue + module Downloaders + class SummonDownloader < BaseDownloader + def download: -> void + + private + + def download_variants: (Summon summon) -> void + + def download_variant: (String variant_id) -> void + + def build_variant_url: (String variant_id, String size) -> String + + def object_type: -> String + + def base_url: -> String + + def directory_for_size: (String size) -> String + + private + + @id: String + @base_url: String + @test_mode: bool + @verbose: bool + @storage: Symbol + @aws_service: AwsService + end + end +end diff --git a/sig/granblue/downloaders/weapon_downloader.rbs b/sig/granblue/downloaders/weapon_downloader.rbs new file mode 100644 index 0000000..878deeb --- /dev/null +++ b/sig/granblue/downloaders/weapon_downloader.rbs @@ -0,0 +1,48 @@ +module Granblue + module Downloaders + class WeaponDownloader < BaseDownloader + def download: -> void + + private + + def download_variants: (Weapon weapon) -> void + + def download_variant: (String variant_id) -> void + + def build_variant_url: (String variant_id, String size) -> String + + def object_type: -> String + + def base_url: -> String + + def directory_for_size: (String size) -> String + + def build_url_for_id: (String id, String size) -> String + + # Track progress of elemental weapon downloads + def progress_reporter: (count: Integer, total: Integer, result: String, ?bar_len: Integer) -> void + + private + + @id: String + @base_url: String + @test_mode: bool + @verbose: bool + @storage: Symbol + @aws_service: AwsService + end + + # Special downloader for handling elemental weapon variants + class ElementalWeaponDownloader < WeaponDownloader + SUFFIXES: Array[Integer] + + def initialize: (Integer id_base) -> void + + def download: -> void + + private + + @id_base: Integer + end + end +end diff --git a/sig/granblue/transformers/summon_transformer.rbs b/sig/granblue/transformers/summon_transformer.rbs index 3c0e1bf..b6199ea 100644 --- a/sig/granblue/transformers/summon_transformer.rbs +++ b/sig/granblue/transformers/summon_transformer.rbs @@ -1,15 +1,12 @@ module Granblue module Transformers class SummonTransformer < BaseTransformer - # Level thresholds for transcendence calculations TRANSCENDENCE_LEVELS: Array[Integer] - # Quick summon ID for the current transformation @quick_summon_id: String? def initialize: (untyped data, ?String? quick_summon_id, ?Hash[Symbol, untyped] options) -> void - # Implements abstract method from BaseTransformer def transform: -> Array[Hash[Symbol, untyped]] private