diff --git a/.solargraph.yml b/.solargraph.yml deleted file mode 100644 index c48b418f..00000000 --- a/.solargraph.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -include: - - "app/**/*.rb" - - "config/**/*.rb" -exclude: - - "spec/**/*" - - "test/**/*" - - "vendor/**/*" - - ".bundle/**/*" -require: - - actioncable - - actionmailer - - actionpack - - actionview - - activemodel - - activerecord - - activesupport -plugins: - - solargraph-rails -domains: [] -reporters: - - require_not_found -require_paths: [] -max_files: 5000 diff --git a/Gemfile b/Gemfile index 929c9148..cf82fc06 100644 --- a/Gemfile +++ b/Gemfile @@ -11,11 +11,11 @@ gem 'mysql2', '~> 0.5.5' # For reading the .env file, which you can use in development to more easily # set environment variables for secret data. -gem 'dotenv-rails', '~> 2.8', '>= 2.8.1' +gem 'dotenv', '~> 3.2' # For the asset pipeline: templates, CSS, JS, etc. gem 'sprockets', '~> 4.2' -gem 'haml', '~> 6.1', '>= 6.1.1' +gem 'haml', '~> 7.2' gem 'sass-rails', '~> 6.0' gem 'terser', '~> 1.1', '>= 1.1.17' gem 'jsbundling-rails', '~> 1.3' @@ -25,7 +25,7 @@ gem 'turbo-rails', '~> 2.0' gem 'devise', '~> 4.9', '>= 4.9.2' gem 'devise-encryptable', '~> 0.2.0' gem 'omniauth', '~> 2.1' -gem 'omniauth-rails_csrf_protection', '~> 1.0' +gem 'omniauth-rails_csrf_protection', '~> 2.0', '>= 2.0.1' gem "omniauth_openid_connect", "~> 0.7.1" # For pagination UI. @@ -40,7 +40,7 @@ gem 'nokogiri', '~> 1.15', '>= 1.15.3' # For safely rendering users' Markdown + HTML on item list pages. gem 'rdiscount', '~> 2.2', '>= 2.2.7.1' -gem 'sanitize', '~> 6.0', '>= 6.0.2' +gem 'sanitize', '~> 7.0' # For working with Neopets APIs. # unstable version of RocketAMF interprets info registry as a hash instead of an array @@ -61,6 +61,9 @@ gem "async", "~> 2.17", require: false gem "async-http", "~> 0.89.0", require: false gem "thread-local", "~> 1.1", require: false +# For image processing (outfit PNG rendering). +gem "ruby-vips", "~> 2.2" + # For debugging. group :development do gem 'debug', '~> 1.9.2' @@ -71,7 +74,7 @@ end gem 'bootsnap', '~> 1.16', require: false # For investigating performance issues. -gem "rack-mini-profiler", "~> 3.1" +gem 'rack-mini-profiler', '~> 4.0', '>= 4.0.1' gem "memory_profiler", "~> 1.0" gem "stackprof", "~> 0.2.25" @@ -82,16 +85,6 @@ gem "sentry-rails", "~> 5.12" # For tasks that use shell commands. gem "shell", "~> 0.8.1" -# For workspace autocomplete. -group :development do - gem "solargraph", "~> 0.50.0" - gem "solargraph-rails", "~> 1.1" -end - # For automated tests. -group :development, :test do - gem "rspec-rails", "~> 7.0" -end -group :test do - gem "webmock", "~> 3.24" -end +gem 'rspec-rails', '~> 8.0', '>= 8.0.2', group: [:development, :test] +gem "webmock", "~> 3.24", group: [:test] diff --git a/Gemfile.lock b/Gemfile.lock index b9aab7ba..e31c4707 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,31 +6,31 @@ PATH GEM remote: https://rubygems.org/ specs: - action_text-trix (2.1.15) + action_text-trix (2.1.16) railties - actioncable (8.1.1) - actionpack (= 8.1.1) - activesupport (= 8.1.1) + actioncable (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.1) - actionpack (= 8.1.1) - activejob (= 8.1.1) - activerecord (= 8.1.1) - activestorage (= 8.1.1) - activesupport (= 8.1.1) + actionmailbox (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) - actionmailer (8.1.1) - actionpack (= 8.1.1) - actionview (= 8.1.1) - activejob (= 8.1.1) - activesupport (= 8.1.1) + actionmailer (8.1.2) + actionpack (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.1) - actionview (= 8.1.1) - activesupport (= 8.1.1) + actionpack (8.1.2) + actionview (= 8.1.2) + activesupport (= 8.1.2) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -38,36 +38,36 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.1) + actiontext (8.1.2) action_text-trix (~> 2.1.15) - actionpack (= 8.1.1) - activerecord (= 8.1.1) - activestorage (= 8.1.1) - activesupport (= 8.1.1) + actionpack (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.1) - activesupport (= 8.1.1) + actionview (8.1.2) + activesupport (= 8.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.1.1) - activesupport (= 8.1.1) + activejob (8.1.2) + activesupport (= 8.1.2) globalid (>= 0.3.6) - activemodel (8.1.1) - activesupport (= 8.1.1) - activerecord (8.1.1) - activemodel (= 8.1.1) - activesupport (= 8.1.1) + activemodel (8.1.2) + activesupport (= 8.1.2) + activerecord (8.1.2) + activemodel (= 8.1.2) + activesupport (= 8.1.2) timeout (>= 0.4.0) - activestorage (8.1.1) - actionpack (= 8.1.1) - activejob (= 8.1.1) - activerecord (= 8.1.1) - activesupport (= 8.1.1) + activestorage (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activesupport (= 8.1.2) marcel (~> 1.0) - activesupport (8.1.1) + activesupport (8.1.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -83,14 +83,13 @@ GEM addressable (2.8.8) public_suffix (>= 2.0.2, < 8.0) aes_key_wrap (1.1.0) - ast (2.4.3) - async (2.35.0) + async (2.35.3) console (~> 1.29) fiber-annotation io-event (~> 1.11) metrics (~> 0.12) traces (~> 0.18) - async-container (0.27.7) + async-container (0.29.0) async (~> 2.22) async-http (0.89.0) async (>= 2.10.2) @@ -106,19 +105,17 @@ GEM async-http (~> 0.56) async-pool (0.11.1) async (>= 2.0) - async-service (0.16.0) + async-service (0.17.0) async - async-container (~> 0.16) + async-container (~> 0.28) string-format (~> 0.2) attr_required (1.0.2) - backport (1.2.0) base64 (0.3.0) - bcrypt (3.1.20) - benchmark (0.5.0) + bcrypt (3.1.21) bigdecimal (4.0.1) bindata (2.5.1) bindex (0.8.1) - bootsnap (1.20.1) + bootsnap (1.21.1) msgpack (~> 1.2) builder (3.3.0) childprocess (5.1.0) @@ -146,10 +143,7 @@ GEM devise-encryptable (0.2.0) devise (>= 2.1.0) diff-lcs (1.6.2) - dotenv (2.8.1) - dotenv-rails (2.8.1) - dotenv (= 2.8.1) - railties (>= 3.2) + dotenv (3.2.0) drb (2.2.3) e2mmap (0.1.0) email_validator (2.2.4) @@ -174,21 +168,20 @@ GEM faraday-net_http (>= 2.0, < 3.5) json logger - faraday-follow_redirects (0.4.0) + faraday-follow_redirects (0.5.0) faraday (>= 1, < 3) faraday-net_http (3.4.2) net-http (~> 0.5) - ffi (1.17.2) - ffi (1.17.2-aarch64-linux-gnu) - ffi (1.17.2-arm64-darwin) - ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.3-aarch64-linux-gnu) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86_64-linux-gnu) fiber-annotation (0.2.0) fiber-local (1.1.0) fiber-storage fiber-storage (1.0.1) globalid (1.3.0) activesupport (>= 6.1) - haml (6.4.0) + haml (7.2.0) temple (>= 0.8.2) thor tilt @@ -206,7 +199,6 @@ GEM pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - jaro_winkler (1.6.1) jsbundling-rails (1.3.1) railties (>= 6.0.0) json (2.18.0) @@ -217,19 +209,13 @@ GEM bindata faraday (~> 2.0) faraday-follow_redirects - kramdown (2.5.1) - rexml (>= 3.3.9) - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - language_server-protocol (3.17.0.5) launchy (3.1.1) addressable (~> 2.8) childprocess (~> 5.0) logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - lint_roller (1.1.0) - localhost (1.6.0) + localhost (1.7.0) logger (1.7.0) loofah (2.25.0) crass (~> 1.0.2) @@ -245,7 +231,6 @@ GEM memory_profiler (1.1.0) metrics (0.15.0) mini_mime (1.1.5) - mini_portile2 (2.8.9) minitest (6.0.1) prism (~> 1.5) msgpack (1.8.0) @@ -263,21 +248,18 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.18.10) - mini_portile2 (~> 2.8.2) + nokogiri (1.19.0-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.10-aarch64-linux-gnu) + nokogiri (1.19.0-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.10-arm64-darwin) - racc (~> 1.4) - nokogiri (1.18.10-x86_64-linux-gnu) + nokogiri (1.19.0-x86_64-linux-gnu) racc (~> 1.4) omniauth (2.1.4) hashie (>= 3.4.6) logger rack (>= 2.2.3) rack-protection - omniauth-rails_csrf_protection (1.0.2) + omniauth-rails_csrf_protection (2.0.1) actionpack (>= 4.2) omniauth (~> 2.0) omniauth_openid_connect (0.7.1) @@ -298,38 +280,34 @@ GEM webfinger (~> 2.0) openssl (3.3.2) orm_adapter (0.5.0) - parallel (1.27.0) - parser (3.3.10.0) - ast (~> 2.4.1) - racc pp (0.6.3) prettyprint prettyprint (0.2.0) - prism (1.7.0) + prism (1.8.0) process-metrics (0.8.0) console (~> 1.8) json (~> 2) samovar (~> 2.1) protocol-hpack (1.5.1) - protocol-http (0.56.1) - protocol-http1 (0.35.2) - protocol-http (~> 0.22) - protocol-http2 (0.23.0) + protocol-http (0.58.0) + protocol-http1 (0.36.0) + protocol-http (~> 0.58) + protocol-http2 (0.24.0) protocol-hpack (~> 1.4) protocol-http (~> 0.47) - protocol-rack (0.19.0) + protocol-rack (0.21.0) io-stream (>= 0.10) - protocol-http (~> 0.43) + protocol-http (~> 0.58) rack (>= 1.0) psych (5.3.1) date stringio - public_suffix (7.0.0) + public_suffix (7.0.2) racc (1.8.1) rack (3.2.4) rack-attack (6.8.0) rack (>= 1.0, < 4) - rack-mini-profiler (3.3.1) + rack-mini-profiler (4.0.1) rack (>= 1.2.0) rack-oauth2 (2.3.0) activesupport @@ -349,20 +327,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.1) - actioncable (= 8.1.1) - actionmailbox (= 8.1.1) - actionmailer (= 8.1.1) - actionpack (= 8.1.1) - actiontext (= 8.1.1) - actionview (= 8.1.1) - activejob (= 8.1.1) - activemodel (= 8.1.1) - activerecord (= 8.1.1) - activestorage (= 8.1.1) - activesupport (= 8.1.1) + rails (8.1.2) + actioncable (= 8.1.2) + actionmailbox (= 8.1.2) + actionmailer (= 8.1.2) + actionpack (= 8.1.2) + actiontext (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activemodel (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) bundler (>= 1.15.0) - railties (= 8.1.1) + railties (= 8.1.2) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -373,31 +351,26 @@ GEM rails-i18n (8.1.0) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.1.1) - actionpack (= 8.1.1) - activesupport (= 8.1.1) + railties (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) tsort (>= 0.2) zeitwerk (~> 2.6) - rainbow (3.1.1) rake (13.3.1) - rbs (2.8.4) rdiscount (2.2.7.3) - rdoc (7.0.3) + rdoc (7.1.0) erb psych (>= 4.0.0) tsort - regexp_parser (2.11.3) reline (0.6.3) io-console (~> 0.5) responders (3.2.0) actionpack (>= 7.0) railties (>= 7.0) - reverse_markdown (2.1.1) - nokogiri rexml (3.4.4) rspec-core (3.13.6) rspec-support (~> 3.13.0) @@ -407,36 +380,24 @@ GEM rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (7.1.1) - actionpack (>= 7.0) - activesupport (>= 7.0) - railties (>= 7.0) + rspec-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.6) - rubocop (1.82.1) - json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.48.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.48.0) - parser (>= 3.3.7.2) - prism (~> 1.4) - ruby-progressbar (1.13.0) + ruby-vips (2.3.0) + ffi (~> 1.12) + logger samovar (2.4.1) console (~> 1.0) mapping (~> 1.0) - sanitize (6.1.3) + sanitize (7.0.0) crass (~> 1.0.2) - nokogiri (>= 1.12.0) + nokogiri (>= 1.16.8) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -457,25 +418,6 @@ GEM shell (0.8.1) e2mmap sync - solargraph (0.50.0) - backport (~> 1.2) - benchmark - bundler (~> 2.0) - diff-lcs (~> 1.4) - e2mmap - jaro_winkler (~> 1.5) - kramdown (~> 2.3) - kramdown-parser-gfm (~> 1.1) - parser (~> 3.0) - rbs (~> 2.0) - reverse_markdown (~> 2.0) - rubocop (~> 1.38) - thor (~> 1.0) - tilt (~> 2.0) - yard (~> 0.9, >= 0.9.24) - solargraph-rails (1.2.4) - activesupport - solargraph (>= 0.48.0, <= 0.57) sprockets (4.2.2) concurrent-ruby (~> 1.0) logger @@ -496,20 +438,17 @@ GEM temple (0.10.4) terser (1.2.6) execjs (>= 0.3.0, < 3) - thor (1.4.0) + thor (1.5.0) thread-local (1.1.0) - tilt (2.6.1) + tilt (2.7.0) timeout (0.6.0) traces (0.18.2) tsort (0.2.0) - turbo-rails (2.0.20) + turbo-rails (2.0.21) actionpack (>= 7.1.0) railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.2.0) uri (1.1.1) useragent (0.16.11) validate_url (1.0.15) @@ -535,13 +474,11 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) will_paginate (4.0.1) - yard (0.9.38) zeitwerk (2.7.4) PLATFORMS aarch64-linux arm64-darwin - ruby x86_64-linux DEPENDENCIES @@ -553,9 +490,9 @@ DEPENDENCIES debug (~> 1.9.2) devise (~> 4.9, >= 4.9.2) devise-encryptable (~> 0.2.0) - dotenv-rails (~> 2.8, >= 2.8.1) + dotenv (~> 3.2) falcon (~> 0.48.0) - haml (~> 6.1, >= 6.1.1) + haml (~> 7.2) http_accept_language (~> 2.1, >= 2.1.1) jsbundling-rails (~> 1.3) letter_opener (~> 1.8, >= 1.8.1) @@ -563,21 +500,20 @@ DEPENDENCIES mysql2 (~> 0.5.5) nokogiri (~> 1.15, >= 1.15.3) omniauth (~> 2.1) - omniauth-rails_csrf_protection (~> 1.0) + omniauth-rails_csrf_protection (~> 2.0, >= 2.0.1) omniauth_openid_connect (~> 0.7.1) rack-attack (~> 6.7) - rack-mini-profiler (~> 3.1) + rack-mini-profiler (~> 4.0, >= 4.0.1) rails (~> 8.0, >= 8.0.1) rails-i18n (~> 8.0, >= 8.0.1) rdiscount (~> 2.2, >= 2.2.7.1) - rspec-rails (~> 7.0) - sanitize (~> 6.0, >= 6.0.2) + rspec-rails (~> 8.0, >= 8.0.2) + ruby-vips (~> 2.2) + sanitize (~> 7.0) sass-rails (~> 6.0) sentry-rails (~> 5.12) sentry-ruby (~> 5.12) shell (~> 0.8.1) - solargraph (~> 0.50.0) - solargraph-rails (~> 1.1) sprockets (~> 4.2) stackprof (~> 0.2.25) terser (~> 1.1, >= 1.1.17) diff --git a/app/assets/javascripts/swf_assets/show.js b/app/assets/javascripts/swf_assets/show.js index 66aa24ac..ee5446d5 100644 --- a/app/assets/javascripts/swf_assets/show.js +++ b/app/assets/javascripts/swf_assets/show.js @@ -150,13 +150,13 @@ function updateStage() { function updateCanvasDimensions() { // Set the canvas's internal dimensions to be higher, if the device has high - // DPI. Scale the movie clip to match, too. + // DPI. Scale the stage to match, too. const internalWidth = canvas.offsetWidth * window.devicePixelRatio; const internalHeight = canvas.offsetHeight * window.devicePixelRatio; canvas.width = internalWidth; canvas.height = internalHeight; - movieClip.scaleX = internalWidth / library.properties.width; - movieClip.scaleY = internalHeight / library.properties.height; + stage.scaleX = internalWidth / library.properties.width; + stage.scaleY = internalHeight / library.properties.height; } window.addEventListener("resize", () => { @@ -176,23 +176,28 @@ window.addEventListener("resize", () => { //////////////////////////////////////////////////// async function startMovie() { + // Install the MotionGuidePlugin, which is needed for motion path animations. + createjs.MotionGuidePlugin.install(); + // Load the movie's library (from the JS file already run), and use it to // build a movie clip. library = await getLibrary(); movieClip = buildMovieClip(library); - updateCanvasDimensions(); - if (canvas.getContext("2d") == null) { console.warn(`Out of memory, can't use canvas for ${libraryUrl}.`); // TODO: "Too many animations!" return; } - stage = new window.createjs.Stage(canvas); + stage = new library.Stage(canvas); stage.addChild(movieClip); + updateCanvasDimensions(); updateStage(); + // Signal to the library that the composition is ready. + AdobeAn.compositionLoaded(library.properties.id); + loadingStatus = "loaded"; canvas.setAttribute("data-status", "loaded"); diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1a3d973e..17e7664d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,11 +12,9 @@ class ApplicationController < ActionController::Base before_action :save_return_to_path, if: ->(c) { c.controller_name == 'sessions' && c.action_name == 'new' } - # Enable profiling tools if logged in as admin. + # Enable profiling tools in development or when logged in as an admin. before_action do - if current_user && current_user.admin? - Rack::MiniProfiler.authorize_request - end + Rack::MiniProfiler.authorize_request if Rails.env.development? || current_user&.admin? end class AccessDenied < StandardError; end diff --git a/app/controllers/item_trades_controller.rb b/app/controllers/item_trades_controller.rb index 8a31cda4..75ba7172 100644 --- a/app/controllers/item_trades_controller.rb +++ b/app/controllers/item_trades_controller.rb @@ -3,9 +3,12 @@ class ItemTradesController < ApplicationController @item = Item.find params[:item_id] @type = type_from_params - @item_trades = @item.closet_hangers.trading.includes(:user, :list). - user_is_active.order('users.last_trade_activity_at DESC'). - to_trades(current_user, request.remote_ip) + @item_trades = @item.visible_trades( + scope: ClosetHanger.includes(:user, :list). + order('users.last_trade_activity_at DESC'), + user: current_user, + remote_ip: request.remote_ip + ) @trades = @item_trades[@type] if user_signed_in? diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb index 18fbb19e..54a82672 100644 --- a/app/controllers/items_controller.rb +++ b/app/controllers/items_controller.rb @@ -80,8 +80,10 @@ class ItemsController < ApplicationController respond_to do |format| format.html do - @trades = @item.closet_hangers.trading.user_is_active. - to_trades(current_user, request.remote_ip) + @trades = @item.visible_trades( + user: current_user, + remote_ip: request.remote_ip + ) @contributors_with_counts = @item.contributors_with_counts @@ -107,6 +109,15 @@ class ItemsController < ApplicationController includes(:species).merge(Species.alphabetical) end + format.json do + render json: @item.as_json( + include_trade_counts: true, + include_nc_trade_value: true, + current_user: current_user, + remote_ip: request.remote_ip + ) + end + format.gif do expires_in 1.month redirect_to @item.thumbnail_url, allow_other_host: true diff --git a/app/controllers/outfits_controller.rb b/app/controllers/outfits_controller.rb index 73be29fd..0da8e394 100644 --- a/app/controllers/outfits_controller.rb +++ b/app/controllers/outfits_controller.rb @@ -13,7 +13,26 @@ class OutfitsController < ApplicationController end def edit - render "outfits/edit", layout: false + respond_to do |format| + format.html { render "outfits/edit", layout: false } + format.png do + @outfit = build_outfit_from_wardrobe_params + if @outfit.valid? + renderer = OutfitImageRenderer.new(@outfit) + png_data = renderer.render + + if png_data + send_data png_data, type: "image/png", disposition: "inline", + filename: "outfit.png" + expires_in 1.day, public: true + else + head :not_found + end + else + head :bad_request + end + end + end end def index @@ -118,6 +137,40 @@ class OutfitsController < ApplicationController biology: [:species_id, :color_id, :pose, :pet_state_id]) end + def build_outfit_from_wardrobe_params + # Load items first + worn_item_ids = params[:objects] ? Array(params[:objects]).map(&:to_i) : [] + closeted_item_ids = params[:closet] ? Array(params[:closet]).map(&:to_i) : [] + + worn_items = Item.where(id: worn_item_ids) + closeted_items = Item.where(id: closeted_item_ids) + + # Build outfit with biology and items + outfit = Outfit.new( + worn_items: worn_items, + closeted_items: closeted_items, + ) + + # Set biology from species, color, and pose params + if params[:species] && params[:color] && params[:pose] + outfit.biology = { + species_id: params[:species], + color_id: params[:color], + pose: params[:pose] + } + elsif params[:state] + # Alternative: use pet_state_id directly + outfit.biology = { pet_state_id: params[:state] } + end + + # Set alt style if provided + if params[:style] + outfit.alt_style_id = params[:style].to_i + end + + outfit + end + def find_authorized_outfit raise ActiveRecord::RecordNotFound unless user_signed_in? @outfit = current_user.outfits.find(params[:id]) diff --git a/app/helpers/items_helper.rb b/app/helpers/items_helper.rb index 28e2fc99..e0536c86 100644 --- a/app/helpers/items_helper.rb +++ b/app/helpers/items_helper.rb @@ -141,6 +141,13 @@ module ItemsHelper def auction_genie_url_for(item) AUCTION_GENIE_URL_TEMPLATE.expand(auctiongenie: item.name).to_s end + + LEBRON_URL_TEMPLATE = Addressable::Template.new( + "https://stylisher.club/search/{name}" + ) + def lebron_url_for(item) + LEBRON_URL_TEMPLATE.expand(name: item.name).to_s + end def format_contribution_count(count) " (×#{count})".html_safe if count > 1 diff --git a/app/javascript/wardrobe-2020/components/OutfitMovieLayer.js b/app/javascript/wardrobe-2020/components/OutfitMovieLayer.js index 0afd76c4..47ddf81a 100644 --- a/app/javascript/wardrobe-2020/components/OutfitMovieLayer.js +++ b/app/javascript/wardrobe-2020/components/OutfitMovieLayer.js @@ -390,6 +390,10 @@ export function loadMovieLibrary(librarySrc, { preferArchive = false } = {}) { ); } delete window.AdobeAn.compositions[compositionId]; + + // Install the MotionGuidePlugin, which is needed for motion path animations. + window.createjs.MotionGuidePlugin.install(); + const library = composition.getLibrary(); // One more loading step as part of loading this library is loading the diff --git a/app/models/item.rb b/app/models/item.rb index e1b1c5db..a8fb9afe 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -34,7 +34,7 @@ class Item < ApplicationRecord attr_writer :current_body_id, :owned, :wanted NCRarities = [0, 500] - PAINTBRUSH_SET_DESCRIPTION = 'This item is part of a deluxe paint brush set!' + PAINTBRUSH_SET_DESCRIPTION = 'This item is part of a deluxe paint brush set' scope :newest, -> { order(arel_table[:created_at].desc) if arel_table[:created_at] @@ -162,7 +162,7 @@ class Item < ApplicationRecord end def pb? - I18n.with_locale(:en) { self.description == PAINTBRUSH_SET_DESCRIPTION } + I18n.with_locale(:en) { self.description.include?(PAINTBRUSH_SET_DESCRIPTION) } end def np? @@ -444,11 +444,34 @@ class Item < ApplicationRecord created_at || Time.new(2010) end + # Returns the visible trades for this item, filtered by user visibility. + # Accepts an optional scope to add additional query constraints (e.g., includes, order). + def visible_trades(scope: nil, user: nil, remote_ip: nil) + base = closet_hangers.trading.user_is_active + base = base.merge(scope) if scope + base.to_trades(user, remote_ip) + end + def as_json(options={}) - super({ + result = super({ only: [:id, :name, :description, :thumbnail_url, :rarity_index], methods: [:zones_restrict], }.merge(options)) + + if options[:include_trade_counts] + trades = visible_trades( + user: options[:current_user], + remote_ip: options[:remote_ip] + ) + result['num_trades_offering'] = trades[:offering].size + result['num_trades_seeking'] = trades[:seeking].size + end + + if options[:include_nc_trade_value] + result['nc_trade_value'] = nc_trade_value + end + + result end def compatible_body_ids(use_cached: true) diff --git a/app/models/outfit.rb b/app/models/outfit.rb index 21138789..74a5c596 100644 --- a/app/models/outfit.rb +++ b/app/models/outfit.rb @@ -172,52 +172,67 @@ class Outfit < ApplicationRecord def visible_layers return [] if pet_state.nil? - # TODO: This method doesn't currently handle alt styles! If the outfit has - # an alt_style, we should use its layers instead of pet_state layers, and - # filter items to only those with body_id=0. This isn't needed yet because - # this method is only used on item pages, which don't support alt styles. - # See useOutfitAppearance.js for the complete logic including alt styles. - item_appearances = item_appearances(swf_asset_includes: [:zone]) + # Step 1: Choose biology layers - use alt style if present, otherwise pet state + if alt_style + biology_layers = alt_style.swf_assets.includes(:zone).to_a + body = alt_style + using_alt_style = true + else + biology_layers = pet_state.swf_assets.includes(:zone).to_a + body = pet_type + using_alt_style = false + end - pet_layers = pet_state.swf_assets.includes(:zone).to_a + # Step 2: Load item appearances for the appropriate body + item_appearances = Item.appearances_for( + worn_items, + body, + swf_asset_includes: [:zone] + ).values item_layers = item_appearances.map(&:swf_assets).flatten - pet_restricted_zone_ids = pet_layers.map(&:restricted_zone_ids). + # For alt styles, only body_id=0 items are compatible + if using_alt_style + item_layers.reject! { |sa| sa.body_id != 0 } + end + + # Step 3: Apply restriction rules + biology_restricted_zone_ids = biology_layers.map(&:restricted_zone_ids). flatten.to_set item_restricted_zone_ids = item_appearances. map(&:restricted_zone_ids).flatten.to_set - # When an item restricts a zone, it hides pet layers of the same zone. + # Rule 3a: When an item restricts a zone, it hides biology layers of the same zone. # We use this to e.g. make a hat hide a hair ruff. # # NOTE: Items' restricted layers also affect what items you can wear at # the same time. We don't enforce anything about that here, and # instead assume that the input by this point is valid! - pet_layers.reject! { |sa| item_restricted_zone_ids.include?(sa.zone_id) } + biology_layers.reject! { |sa| item_restricted_zone_ids.include?(sa.zone_id) } - # When a pet appearance restricts a zone, or when the pet is Unconverted, - # it makes body-specific items incompatible. We use this to disallow UCs - # from wearing certain body-specific Biology Effects, Statics, etc, while - # still allowing non-body-specific items in those zones! (I think this - # happens for some Invisible pet stuff, too?) + # Rule 3b: When a biology appearance restricts a zone, or when the pet is + # Unconverted, it makes body-specific items incompatible. We use this to + # disallow UCs from wearing certain body-specific Biology Effects, Statics, + # etc, while still allowing non-body-specific items in those zones! (I think + # this happens for some Invisible pet stuff, too?) # # TODO: We shouldn't be *hiding* these zones, like we do with items; we # should be doing this way earlier, to prevent the item from even # showing up even in search results! # - # NOTE: This can result in both pet layers and items occupying the same + # NOTE: This can result in both biology layers and items occupying the same # zone, like Static, so long as the item isn't body-specific! That's # correct, and the item layer should be on top! (Here, we implement # it by placing item layers second in the list, and rely on JS sort # stability, and *then* rely on the UI to respect that ordering when # rendering them by depth. Not great! 😅) # - # NOTE: We used to also include the pet appearance's *occupied* zones in + # NOTE: We used to also include the biology appearance's *occupied* zones in # this condition, not just the restricted zones, as a sensible # defensive default, even though we weren't aware of any relevant # items. But now we know that actually the "Bruce Brucey B Mouth" # occupies the real Mouth zone, and still should be visible and - # above pet layers! So, we now only check *restricted* zones. + # above biology layers! So, we now only check *restricted* zones. # # NOTE: UCs used to implement their restrictions by listing specific # zones, but it seems that the logic has changed to just be about @@ -234,18 +249,20 @@ class Outfit < ApplicationRecord item_layers.reject! { |sa| sa.body_specific? } else item_layers.reject! { |sa| sa.body_specific? && - pet_restricted_zone_ids.include?(sa.zone_id) } + biology_restricted_zone_ids.include?(sa.zone_id) } end - # A pet appearance can also restrict its own zones. The Wraith Uni is an - # interesting example: it has a horn, but its zone restrictions hide it! - pet_layers.reject! { |sa| pet_restricted_zone_ids.include?(sa.zone_id) } + # Rule 3c: A biology appearance can also restrict its own zones. The Wraith + # Uni is an interesting example: it has a horn, but its zone restrictions + # hide it! + biology_layers.reject! { |sa| biology_restricted_zone_ids.include?(sa.zone_id) } - (pet_layers + item_layers).sort_by(&:depth) + # Step 4: Sort by depth and return + (biology_layers + item_layers).sort_by(&:depth) end def wardrobe_params - { + params = { name: name, color: color_id, species: species_id, @@ -254,6 +271,8 @@ class Outfit < ApplicationRecord objects: worn_item_ids, closet: closeted_item_ids, } + params[:style] = alt_style_id if alt_style_id.present? + params end def ensure_unique_name diff --git a/app/models/pet/auto_modeling.rb b/app/models/pet/auto_modeling.rb new file mode 100644 index 00000000..a2d2d5de --- /dev/null +++ b/app/models/pet/auto_modeling.rb @@ -0,0 +1,63 @@ +# Pet::AutoModeling provides utilities for automatically modeling items on pet +# bodies using the NC Mall preview API. This allows us to fetch appearance data +# for items without needing a real pet of that type. +# +# The workflow: +# 1. Generate a combined "SCI" (Species/Color Image hash) using NC Mall's +# getPetData endpoint, which combines a pet type with items. +# 2. Fetch the viewer data for that combined SCI using the CustomPets API. +# 3. Process the viewer data to create SwfAsset records. +module Pet::AutoModeling + extend self + + # Model an item on a specific body ID. This fetches the appearance data from + # Neopets and creates/updates the SwfAsset records. + # + # @param item [Item] The item to model + # @param body_id [Integer] The body ID to model on + # @return [Symbol] Result status: + # - :modeled - Successfully created SwfAsset records + # - :not_compatible - Item is explicitly not compatible with this body + # @raise [NoPetTypeForBody] If no PetType exists for this body_id + # @raise [Neopets::NCMall::ResponseNotOK] On HTTP errors (transient for 5xx) + # @raise [Neopets::NCMall::UnexpectedResponseFormat] On invalid response + # @raise [Neopets::CustomPets::DownloadError] On AMF protocol errors + def model_item_on_body(item, body_id) + # Find a pet type with this body ID to use as a base + pet_type = PetType.find_by(body_id: body_id) + raise NoPetTypeForBody.new(body_id) if pet_type.nil? + + # Fetch the viewer data for this item on this pet type + new_image_hash = Neopets::NCMall.fetch_pet_data_sci(pet_type.image_hash, [item.id]) + viewer_data = Neopets::CustomPets.fetch_viewer_data("@#{new_image_hash}") + + # If the item wasn't in the response, it's not compatible. + object_info = viewer_data[:object_info_registry]&.to_h&.[](item.id.to_s) + return :not_compatible if object_info.nil? + + # Process the modeling data using the existing infrastructure + snapshot = Pet::ModelingSnapshot.new(viewer_data) + + # Save the pet type (may update image hash, etc.) + snapshot.pet_type.save! + + # Get the items from the snapshot and process them + modeled_items = snapshot.items + modeled_item = modeled_items.find { |i| i.id == item.id } + + if modeled_item + modeled_item.save! + modeled_item.handle_assets! + end + + :modeled + end + + class NoPetTypeForBody < StandardError + attr_reader :body_id + def initialize(body_id) + @body_id = body_id + super("No PetType found for body_id=#{body_id}") + end + end +end diff --git a/app/services/neopets/nc_mall.rb b/app/services/neopets/nc_mall.rb index bc740fe0..0e284b08 100644 --- a/app/services/neopets/nc_mall.rb +++ b/app/services/neopets/nc_mall.rb @@ -120,6 +120,44 @@ module Neopets::NCMall end end + # Generate a new image hash for a pet wearing specific items. Takes a base + # pet sci (species/color image hash) and optional item IDs, and returns a + # response containing the combined image hash in the :newsci field. + # Use the returned hash with Neopets::CustomPets.fetch_viewer_data("@#{newsci}") + # to get the full appearance data. + PET_DATA_URL = "https://ncmall.neopets.com/mall/ajax/petview/getPetData.php" + def self.fetch_pet_data_sci(pet_sci, item_ids = []) + Sync do + params = {"selPetsci" => pet_sci} + item_ids.each { |id| params["itemsList[]"] = id.to_s } + + DTIRequests.post( + PET_DATA_URL, + [["Content-Type", "application/x-www-form-urlencoded"]], + params.to_query, + ) do |response| + if response.status != 200 + raise ResponseNotOK.new(response.status), + "expected status 200 but got #{response.status} (#{PET_DATA_URL})" + end + + begin + data = JSON.parse(response.read) + rescue JSON::ParserError + raise UnexpectedResponseFormat, + "failed to parse pet data response as JSON" + end + + unless data["newsci"].is_a?(String) && data["newsci"].present? + raise UnexpectedResponseFormat, + "missing or invalid field newsci in pet data response" + end + + data["newsci"] + end + end + end + private # Map load_type from menu JSON to the v2 API type parameter. diff --git a/app/views/items/_item_header.html.haml b/app/views/items/_item_header.html.haml index 61906de6..857fc94f 100644 --- a/app/views/items/_item_header.html.haml +++ b/app/views/items/_item_header.html.haml @@ -15,10 +15,13 @@ = link_to t('items.show.resources.jn_items'), jn_items_url_for(item) = link_to t('items.show.resources.impress_2020'), impress_2020_url_for(item) - if item.nc_trade_value - = link_to 'https://www.neopets.com/~lebron', + = link_to lebron_url_for(item), title: nc_trade_value_updated_at_text(item.nc_trade_value) do - = t 'items.show.resources.lebron', + = t 'items.show.resources.lebron_value', value: nc_trade_value_estimate_text(item.nc_trade_value) + - elsif item.nc? + = link_to lebron_url_for(item) do + = t 'items.show.resources.lebron' - unless item.nc? = link_to t('items.show.resources.shop_wizard'), shop_wizard_url_for(item) = link_to t('items.show.resources.trading_post'), trading_post_url_for(item) diff --git a/bin/solargraph b/bin/solargraph deleted file mode 100755 index b02c02ad..00000000 --- a/bin/solargraph +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# -# This file was generated by Bundler. -# -# The application 'solargraph' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) - -bundle_binstub = File.expand_path("bundle", __dir__) - -if File.file?(bundle_binstub) - if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") - load(bundle_binstub) - else - abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. -Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") - end -end - -require "rubygems" -require "bundler/setup" - -load Gem.bin_path("solargraph", "solargraph") diff --git a/config/locales/en.yml b/config/locales/en.yml index e3104434..a96038df 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -310,7 +310,8 @@ en: resources: jn_items: JN Items impress_2020: DTI 2020 - lebron: "Lebron: %{value}" + lebron: Lebron + lebron_value: "Lebron: %{value}" shop_wizard: Shop Wizard trading_post: Trades auction_genie: Auctions diff --git a/deploy/setup.yml b/deploy/setup.yml index d6b59878..43745812 100644 --- a/deploy/setup.yml +++ b/deploy/setup.yml @@ -191,6 +191,7 @@ name: - libmysqlclient-dev - libyaml-dev + - libvips-dev - name: Create the app folder file: diff --git a/docs/customization-architecture.md b/docs/customization-architecture.md index b143144a..bccf84a8 100644 --- a/docs/customization-architecture.md +++ b/docs/customization-architecture.md @@ -249,6 +249,28 @@ This crowdsourced approach is why DTI is "self-sustaining" - users passively con See `app/models/pet/modeling_snapshot.rb` for the full implementation. +### Potential Upgrade: Auto-Modeling + +We are currently not making full use of a recently-discovered Neopets feature: **we no longer need a real pet to model +the item**. There's an NC Mall feature that supports previews of *any* item, not just those sold in the Mall. + +See the `pets:load` task for implementation details. + +We still need users to show us new items in the first place, to learn their item IDs and what body types they might +fit. But once we have that, we can proactively attempt to model the pet on all relevant body types. + +Let's pursue this in two steps: + +1. [x] Create a backfill Rake task to attempt to load any models we suspect we need, based on the same logic as the + `-is:modeled` item search filter. + - Consider having this task auto-update the `modeling_status_hint` field on the item, if we've demonstrated that + the item is almost certainly completely modeled, despite our heuristic indicating it is not. This will keep the + `is:modeled` filter clean and approximately empty. + - As part of this, let's refactor the logic out of `pets:load`, to more simply construct an "image hash" ("sci") + from a pet type + items combination. +2. [ ] Set this as a cron job to run very frequently, to quickly load in new items. + - If we're able to reliably keep `is:modeled` basically empty, this could even be safe to run every, say, 2–5min. + ### Cached Fields To avoid expensive queries, several models cache computed data: diff --git a/lib/outfit_image_renderer.rb b/lib/outfit_image_renderer.rb new file mode 100644 index 00000000..520a6b28 --- /dev/null +++ b/lib/outfit_image_renderer.rb @@ -0,0 +1,82 @@ +require "vips" + +class OutfitImageRenderer + CANVAS_SIZE = 600 + + def initialize(outfit) + @outfit = outfit + end + + def render + layers = @outfit.visible_layers + + # Filter out layers without image URLs + layers_with_images = layers.select(&:image_url?) + + return nil if layers_with_images.empty? + + # Fetch all layer images in parallel + image_data_by_layer = fetch_layer_images(layers_with_images) + + # Create transparent canvas in sRGB colorspace + canvas = Vips::Image.black(CANVAS_SIZE, CANVAS_SIZE, bands: 4) + canvas = canvas.new_from_image([0, 0, 0, 0]) + canvas = canvas.copy(interpretation: :srgb) + + # Composite each layer onto the canvas + layers_with_images.each do |layer| + image_data = image_data_by_layer[layer] + next unless image_data + + begin + layer_image = Vips::Image.new_from_buffer(image_data, "") + + # Resize the layer to fit the canvas size + # All layer images are square, but may not be CANVAS_SIZE x CANVAS_SIZE + # We need to resize them to exactly CANVAS_SIZE x CANVAS_SIZE + if layer_image.width != CANVAS_SIZE || layer_image.height != CANVAS_SIZE + layer_image = layer_image.resize( + CANVAS_SIZE.to_f / layer_image.width, + vscale: CANVAS_SIZE.to_f / layer_image.height + ) + end + + # Composite this layer onto the canvas at (0, 0) + # No offset needed since the layer is now exactly canvas-sized + canvas = canvas.composite([layer_image], :over) + rescue Vips::Error => e + # Log and skip layers that fail to load/composite + Rails.logger.warn "Failed to composite layer #{layer.id} (#{layer.image_url}): #{e.message}" + next + end + end + + # Return PNG data + canvas.write_to_buffer(".png") + end + + private + + def fetch_layer_images(layers) + image_data_by_layer = {} + + DTIRequests.load_many(max_at_once: 10) do |semaphore| + layers.each do |layer| + semaphore.async do + begin + response = DTIRequests.get(layer.image_url) + if response.success? + image_data_by_layer[layer] = response.read + else + Rails.logger.warn "Failed to fetch image for layer #{layer.id} (#{layer.image_url}): HTTP #{response.status}" + end + rescue => e + Rails.logger.warn "Error fetching image for layer #{layer.id} (#{layer.image_url}): #{e.message}" + end + end + end + + image_data_by_layer + end + end +end diff --git a/lib/rocketamf_extensions/remote_gateway/request.rb b/lib/rocketamf_extensions/remote_gateway/request.rb index 81a7e484..66db5688 100644 --- a/lib/rocketamf_extensions/remote_gateway/request.rb +++ b/lib/rocketamf_extensions/remote_gateway/request.rb @@ -30,24 +30,17 @@ module RocketAMFExtensions raise RocketAMF::AMFError.new(first_message_data) end - # HACK: It seems to me that these messages come back with Windows-1250 - # (or similar) encoding on the strings? I'm basing this on the - # Patchwork Staff item, whose description arrives as: + # HACK: Older items in Neopets' database have Windows-1250 encoding, + # while newer items use proper UTF-8. We detect which encoding was used + # by checking if the string is valid UTF-8, and only re-encode if needed. # - # "That staff is cute, but dont use it as a walking stick \x96 I " + - # "dont think it will hold you up!" + # Example of Windows-1250 item: Patchwork Staff (57311), whose + # description contains byte 0x96 (en-dash in Windows-1250). # - # And the `\x96` is meant to represent an endash, which it doesn't in - # UTF-8 or in most extended ASCII encodings, but *does* in Windows's - # specific extended ASCII. - # - # Idk if this is something to do with the AMFPHP spec or how the AMFPHP - # server code they use serializes strings (I couldn't find any - # reference to it?), or just their internal database encoding being - # passed along as-is, or what? But this seems to be the most correct - # interpretation I know how to do, so, let's do it! + # Example of UTF-8 item: Carnival Party Décor (80042), whose name + # contains proper UTF-8 bytes [195, 169] for the é character. result.messages[0].data.body.tap do |body| - reencode_strings! body, "Windows-1250", "UTF-8" + reencode_strings_if_needed! body, "Windows-1250", "UTF-8" end end @@ -92,13 +85,17 @@ module RocketAMFExtensions end end - def reencode_strings!(target, from, to) + def reencode_strings_if_needed!(target, from, to) if target.is_a? String - target.force_encoding(from).encode!(to) + # Only re-encode if the string is not valid UTF-8 + # (indicating it's in the old Windows-1250 encoding) + unless target.valid_encoding? + target.force_encoding(from).encode!(to) + end elsif target.is_a? Array - target.each { |x| reencode_strings!(x, from, to) } + target.each { |x| reencode_strings_if_needed!(x, from, to) } elsif target.is_a? Hash - target.values.each { |x| reencode_strings!(x, from, to) } + target.values.each { |x| reencode_strings_if_needed!(x, from, to) } end end end diff --git a/lib/tasks/items.rake b/lib/tasks/items.rake index 6a100489..c815e402 100644 --- a/lib/tasks/items.rake +++ b/lib/tasks/items.rake @@ -9,4 +9,90 @@ namespace :items do end end end + + desc "Auto-model items on missing body types using NC Mall preview API" + task :auto_model, [:limit] => :environment do |task, args| + limit = (args[:limit] || 100).to_i + dry_run = ENV["DRY_RUN"] == "1" + auto_hint = ENV["AUTO_HINT"] != "0" + + puts "Auto-modeling up to #{limit} items#{dry_run ? ' (DRY RUN)' : ''}..." + puts "Auto-hint: #{auto_hint ? 'enabled' : 'disabled'}" + puts + + # Find items that need modeling, newest first + items = Item.is_not_modeled.order(created_at: :desc).limit(limit) + puts "Found #{items.count} items to process" + puts + + items.each_with_index do |item, index| + puts "[#{index + 1}/#{items.count}] Item ##{item.id}: #{item.name}" + + missing_body_ids = item.predicted_missing_body_ids + if missing_body_ids.empty? + puts " ⚠️ No missing body IDs (item may already be fully modeled)" + puts + next + end + + puts " Missing #{missing_body_ids.size} body IDs: #{missing_body_ids.join(', ')}" + + # Track results for this item + results = {modeled: 0, not_compatible: 0, not_found: 0} + had_transient_error = false + + missing_body_ids.each do |body_id| + if dry_run + puts " Body #{body_id}: [DRY RUN] would attempt modeling" + next + end + + begin + result = Pet::AutoModeling.model_item_on_body(item, body_id) + results[result] += 1 + + case result + when :modeled + puts " Body #{body_id}: ✅ Modeled successfully" + when :not_compatible + puts " Body #{body_id}: ❌ Not compatible (heuristic over-predicted)" + end + rescue Pet::AutoModeling::NoPetTypeForBody => e + puts " Body #{body_id}: ⚠️ #{e.message}" + rescue Neopets::NCMall::ResponseNotOK => e + if e.status >= 500 + puts " Body #{body_id}: ⚠️ Server error (#{e.status}), will retry later" + had_transient_error = true + else + puts " Body #{body_id}: ❌ HTTP error (#{e.status})" + Sentry.capture_exception(e) + end + rescue Neopets::NCMall::UnexpectedResponseFormat => e + puts " Body #{body_id}: ❌ Unexpected response format: #{e.message}" + Sentry.capture_exception(e) + rescue Neopets::CustomPets::DownloadError => e + puts " Body #{body_id}: ⚠️ AMF error: #{e.message}" + had_transient_error = true + end + end + + unless dry_run + # Set hint if we've addressed all bodies without transient errors. + # That way, if the item is not compatible with some bodies, we'll stop + # trying to auto-model it. + if auto_hint && !had_transient_error + item.update!(modeling_status_hint: "done") + puts " 📋 Set modeling_status_hint = 'done'" + end + end + + puts " Summary: #{results[:modeled]} modeled, #{results[:not_compatible]} not compatible, #{results[:not_found]} not found" + puts + + # Be nice to Neopets API + sleep 0.5 unless dry_run || index == items.count - 1 + end + + puts "Done!" + end end diff --git a/lib/tasks/pets.rake b/lib/tasks/pets.rake index f3c51731..294e3ffd 100644 --- a/lib/tasks/pets.rake +++ b/lib/tasks/pets.rake @@ -1,7 +1,30 @@ namespace :pets do - desc "Load a pet's viewer data" - task :load, [:name] => [:environment] do |task, args| - viewer_data = Neopets::CustomPets.fetch_viewer_data(args[:name]) + desc "Load a pet's viewer data (by name or by color/species/items)" + task :load, [:first] => [:environment] do |task, args| + # Collect all arguments (first + extras) + all_args = [args[:first]] + args.extras + + # If only one argument, treat it as a pet name + if all_args.length == 1 + viewer_data = Neopets::CustomPets.fetch_viewer_data(all_args[0]) + else + # Multiple arguments: color, species, and optional item IDs + color_name = all_args[0] + species_name = all_args[1] + item_ids = all_args[2..] + + # Look up the PetType to use for the preview + pet_type = PetType.matching_name(color_name, species_name).first! + + # Convert it to an image hash for direct lookup + new_image_hash = Neopets::NCMall.fetch_pet_data_sci(pet_type.image_hash, item_ids) + pet_name = '@' + new_image_hash + $stderr.puts "Loading pet #{pet_name}" + + # Load the image hash as if it were a pet + viewer_data = Neopets::CustomPets.fetch_viewer_data(pet_name) + end + puts JSON.pretty_generate(viewer_data) end diff --git a/spec/fixtures/outfit_images/Blue Acara With Cape.png b/spec/fixtures/outfit_images/Blue Acara With Cape.png new file mode 100644 index 00000000..56fc6341 Binary files /dev/null and b/spec/fixtures/outfit_images/Blue Acara With Cape.png differ diff --git a/spec/fixtures/outfit_images/Blue Acara With Hat.png b/spec/fixtures/outfit_images/Blue Acara With Hat.png new file mode 100644 index 00000000..c70c7c02 Binary files /dev/null and b/spec/fixtures/outfit_images/Blue Acara With Hat.png differ diff --git a/spec/fixtures/outfit_images/Blue Acara.png b/spec/fixtures/outfit_images/Blue Acara.png new file mode 100644 index 00000000..5ed00824 Binary files /dev/null and b/spec/fixtures/outfit_images/Blue Acara.png differ diff --git a/spec/fixtures/outfit_images/Cape.png b/spec/fixtures/outfit_images/Cape.png new file mode 100644 index 00000000..7bdaf8ae Binary files /dev/null and b/spec/fixtures/outfit_images/Cape.png differ diff --git a/spec/fixtures/outfit_images/Hat.png b/spec/fixtures/outfit_images/Hat.png new file mode 100644 index 00000000..d18a6bc6 Binary files /dev/null and b/spec/fixtures/outfit_images/Hat.png differ diff --git a/spec/fixtures/zones.yml b/spec/fixtures/zones.yml index a2ec78f5..02ed76e2 100644 --- a/spec/fixtures/zones.yml +++ b/spec/fixtures/zones.yml @@ -28,9 +28,9 @@ hindbiology: type_id: 1 label: Hind Biology plain_label: hindbiology -markings: - id: 31 - depth: 35 +markings1: + id: 6 + depth: 8 type_id: 2 label: Markings plain_label: markings @@ -88,6 +88,12 @@ body: type_id: 1 label: Body plain_label: body +markings2: + id: 16 + depth: 19 + type_id: 2 + label: Markings + plain_label: markings bodydisease: id: 17 depth: 20 @@ -172,6 +178,12 @@ head: type_id: 1 label: Head plain_label: head +markings3: + id: 31 + depth: 35 + type_id: 2 + label: Markings + plain_label: markings headdisease: id: 32 depth: 36 @@ -196,9 +208,9 @@ glasses: type_id: 2 label: Glasses plain_label: glasses -earrings: - id: 41 - depth: 45 +earrings1: + id: 36 + depth: 39 type_id: 2 label: Earrings plain_label: earrings @@ -220,15 +232,21 @@ headdrippings: type_id: 1 label: Head Drippings plain_label: headdrippings -hat: - id: 50 - depth: 16 +hat1: + id: 40 + depth: 44 type_id: 2 label: Hat plain_label: hat -righthanditem: - id: 49 - depth: 5 +earrings2: + id: 41 + depth: 45 + type_id: 2 + label: Earrings + plain_label: earrings +righthanditem1: + id: 42 + depth: 46 type_id: 2 label: Right-hand Item plain_label: righthanditem @@ -268,6 +286,18 @@ backgrounditem: type_id: 3 label: Background Item plain_label: backgrounditem +righthanditem2: + id: 49 + depth: 5 + type_id: 2 + label: Right-hand Item + plain_label: righthanditem +hat2: + id: 50 + depth: 16 + type_id: 2 + label: Hat + plain_label: hat belt: id: 51 depth: 27 diff --git a/spec/lib/outfit_image_renderer_spec.rb b/spec/lib/outfit_image_renderer_spec.rb new file mode 100644 index 00000000..c386a1c1 --- /dev/null +++ b/spec/lib/outfit_image_renderer_spec.rb @@ -0,0 +1,240 @@ +require 'webmock/rspec' +require_relative '../rails_helper' + +RSpec.describe OutfitImageRenderer do + fixtures :zones, :colors, :species + + # Helper to load a fixture image + def load_fixture_image(filename) + path = Rails.root.join('spec', 'fixtures', 'outfit_images', filename) + File.read(path) + end + + # Helper to create a pet state with specific swf_assets + def build_pet_state(pet_type, pose: "HAPPY_FEM", swf_assets: []) + pet_state = PetState.create!( + pet_type: pet_type, + pose: pose, + swf_asset_ids: swf_assets.empty? ? [0] : swf_assets.map(&:id) + ) + pet_state.swf_assets = swf_assets + pet_state + end + + # Helper to create a SwfAsset for biology (pet layers) + def build_biology_asset(zone, body_id:) + @remote_id = (@remote_id || 0) + 1 + SwfAsset.create!( + type: "biology", + remote_id: @remote_id, + url: "https://images.neopets.example/biology_#{@remote_id}.swf", + zone: zone, + body_id: body_id, + zones_restrict: "", + has_image: true + ) + end + + # Helper to create a SwfAsset for items (object layers) + def build_item_asset(zone, body_id:) + @remote_id = (@remote_id || 0) + 1 + SwfAsset.create!( + type: "object", + remote_id: @remote_id, + url: "https://images.neopets.example/object_#{@remote_id}.swf", + zone: zone, + body_id: body_id, + zones_restrict: "", + has_image: true + ) + end + + # Helper to create an item with specific swf_assets + def build_item(name, swf_assets: []) + item = Item.create!( + name: name, + description: "Test item", + thumbnail_url: "https://images.neopets.example/thumbnail.png", + rarity: "Common", + price: 100, + zones_restrict: "", + species_support_ids: "" + ) + swf_assets.each do |asset| + ParentSwfAssetRelationship.create!( + parent: item, + swf_asset: asset + ) + end + item + end + + before do + PetType.destroy_all + @pet_type = PetType.create!( + species: species(:acara), + color: colors(:blue), + body_id: 1, + created_at: Time.new(2005) + ) + end + + describe "#render" do + context "with a simple outfit" do + it "composites biology and item layers into a single PNG" do + # Load fixture images + acara_png = load_fixture_image('Blue Acara.png') + hat_png = load_fixture_image('Hat.png') + expected_composite_png = load_fixture_image('Blue Acara With Hat.png') + + # Create biology and item assets + biology_asset = build_biology_asset(zones(:head), body_id: 1) + item_asset = build_item_asset(zones(:hat1), body_id: 1) + + # Stub HTTP requests for the actual image URLs that will be generated + stub_request(:get, biology_asset.image_url). + to_return(body: acara_png, status: 200) + stub_request(:get, item_asset.image_url). + to_return(body: hat_png, status: 200) + + # Build outfit + pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset]) + item = build_item("Test Hat", swf_assets: [item_asset]) + outfit = Outfit.new( + pet_state: pet_state, + worn_items: [item] + ) + + # Render + renderer = OutfitImageRenderer.new(outfit) + result = renderer.render + + # Verify we got PNG data back + expect(result).not_to be_nil + expect(result).to be_a(String) + expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b) # PNG magic bytes + + # Verify the result is a valid 600x600 PNG + result_image = Vips::Image.new_from_buffer(result, "") + expect(result_image.width).to eq(600) + expect(result_image.height).to eq(600) + + # Verify the composite matches the expected image pixel-perfectly + expected_image = Vips::Image.new_from_buffer(expected_composite_png, "") + + # Calculate the absolute difference between images + diff = (result_image - expected_image).abs + max_diff = diff.max + + # Allow a small tolerance for minor encoding/compositing differences + # The expected image was generated with a different method, so we expect + # very close but not necessarily pixel-perfect matches + tolerance = 2 + if max_diff > tolerance + debug_path = Rails.root.join('tmp', 'test_render_result.png') + result_image.write_to_file(debug_path.to_s) + fail "Images should match within tolerance of #{tolerance}, but found max difference of #{max_diff}. Actual output saved to #{debug_path}" + end + end + end + + context "when a layer image fails to load" do + it "skips the failed layer and continues" do + hat_png = load_fixture_image('Hat.png') + + biology_asset = build_biology_asset(zones(:head), body_id: 1) + item_asset = build_item_asset(zones(:hat1), body_id: 1) + + # Stub one successful request and one failure + stub_request(:get, biology_asset.image_url). + to_return(status: 404) + stub_request(:get, item_asset.image_url). + to_return(body: hat_png, status: 200) + + pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset]) + item = build_item("Test Hat", swf_assets: [item_asset]) + outfit = Outfit.new( + pet_state: pet_state, + worn_items: [item] + ) + + renderer = OutfitImageRenderer.new(outfit) + result = renderer.render + + # Should still render successfully with just the one layer + expect(result).not_to be_nil + expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b) + end + end + + context "when no layers have images" do + it "returns nil" do + # Create an asset but stub image_url to return nil + biology_asset = build_biology_asset(zones(:head), body_id: 1) + allow_any_instance_of(SwfAsset).to receive(:image_url?).and_return(false) + + pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset]) + outfit = Outfit.new(pet_state: pet_state) + + renderer = OutfitImageRenderer.new(outfit) + result = renderer.render + + expect(result).to be_nil + end + end + + it "resizes all layers to 600x600 before compositing" do + # Load a 1200x1200 item layer (real-world case from Neopets) + item_1200_png = load_fixture_image('Cape.png') + acara_600_png = load_fixture_image('Blue Acara.png') + expected_composite_png = load_fixture_image('Blue Acara With Cape.png') + + # Create assets + biology_asset = build_biology_asset(zones(:head), body_id: 1) + item_asset = build_item_asset(zones(:hat1), body_id: 1) + + # Stub HTTP requests + stub_request(:get, biology_asset.image_url). + to_return(body: acara_600_png, status: 200) + stub_request(:get, item_asset.image_url). + to_return(body: item_1200_png, status: 200) + + # Build outfit + pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset]) + item = build_item("Test Item", swf_assets: [item_asset]) + outfit = Outfit.new( + pet_state: pet_state, + worn_items: [item] + ) + + # Render + renderer = OutfitImageRenderer.new(outfit) + result = renderer.render + + # Verify we got valid PNG data + expect(result).not_to be_nil + expect(result).to be_a(String) + expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b) + + # Verify the result is exactly 600x600 + result_image = Vips::Image.new_from_buffer(result, "") + expect(result_image.width).to eq(600) + expect(result_image.height).to eq(600) + + # Verify the composite matches the expected image pixel-perfectly + expected_image = Vips::Image.new_from_buffer(expected_composite_png, "") + + # Calculate the absolute difference between images + diff = (result_image - expected_image).abs + max_diff = diff.max + + # Allow a small tolerance for minor encoding/compositing differences + tolerance = 2 + if max_diff > tolerance + debug_path = Rails.root.join('tmp', 'test_render_1200_result.png') + result_image.write_to_file(debug_path.to_s) + fail "Images should match within tolerance of #{tolerance}, but found max difference of #{max_diff}. Actual output saved to #{debug_path}" + end + end + end +end diff --git a/spec/models/outfit_spec.rb b/spec/models/outfit_spec.rb index b1a4758f..93d7eb4e 100644 --- a/spec/models/outfit_spec.rb +++ b/spec/models/outfit_spec.rb @@ -1,7 +1,7 @@ require_relative '../rails_helper' RSpec.describe Outfit do - fixtures :colors, :species, :zones + fixtures :zones, :colors, :species let(:blue) { colors(:blue) } let(:acara) { species(:acara) } @@ -54,7 +54,7 @@ RSpec.describe Outfit do describe "Item::Appearance#compatible_with?" do it "returns true for items in different zones with no restrictions" do - hat = create_item("Hat", zones(:hat)) + hat = create_item("Hat", zones(:hat1)) shirt = create_item("Shirt", zones(:shirtdress)) appearances = Item.appearances_for([hat, shirt], @pet_type) @@ -66,8 +66,8 @@ RSpec.describe Outfit do end it "returns false for items in the same zone" do - hat1 = create_item("Hat 1", zones(:hat)) - hat2 = create_item("Hat 2", zones(:hat)) + hat1 = create_item("Hat 1", zones(:hat1)) + hat2 = create_item("Hat 2", zones(:hat1)) appearances = Item.appearances_for([hat1, hat2], @pet_type) hat1_appearance = appearances[hat1.id] @@ -89,7 +89,7 @@ RSpec.describe Outfit do zones_restrict_array[28] = "1" # Set bit for zone 29 zones_restrict = zones_restrict_array.join - restricting_hat = create_item("Restricting Hat", zones(:hat), zones_restrict: zones_restrict) + restricting_hat = create_item("Restricting Hat", zones(:hat1), zones_restrict: zones_restrict) # Create an item in the ruff zone ruff_item = create_item("Ruff Item", zones(:ruff)) @@ -104,7 +104,7 @@ RSpec.describe Outfit do it "returns true for empty appearances" do # Create items that don't fit the current pet (wrong body_id) - hat = create_item("Hat", zones(:hat), body_id: 999) + hat = create_item("Hat", zones(:hat1), body_id: 999) shirt = create_item("Shirt", zones(:shirtdress), body_id: 999) appearances = Item.appearances_for([hat, shirt], @pet_type) @@ -122,7 +122,7 @@ RSpec.describe Outfit do describe "#without_item" do it "returns a new outfit without the given item" do - hat = create_item("Hat", zones(:hat)) + hat = create_item("Hat", zones(:hat1)) outfit_with_hat = @outfit.with_item(hat) new_outfit = outfit_with_hat.without_item(hat) @@ -132,7 +132,7 @@ RSpec.describe Outfit do end it "returns a new outfit instance (immutable)" do - hat = create_item("Hat", zones(:hat)) + hat = create_item("Hat", zones(:hat1)) outfit_with_hat = @outfit.with_item(hat) new_outfit = outfit_with_hat.without_item(hat) @@ -142,7 +142,7 @@ RSpec.describe Outfit do end it "does nothing if the item is not worn" do - hat = create_item("Hat", zones(:hat)) + hat = create_item("Hat", zones(:hat1)) new_outfit = @outfit.without_item(hat) @@ -152,7 +152,7 @@ RSpec.describe Outfit do describe "#with_item" do it "adds an item when there are no conflicts" do - hat = create_item("Hat", zones(:hat)) + hat = create_item("Hat", zones(:hat1)) new_outfit = @outfit.with_item(hat) @@ -160,7 +160,7 @@ RSpec.describe Outfit do end it "returns a new outfit instance (immutable)" do - hat = create_item("Hat", zones(:hat)) + hat = create_item("Hat", zones(:hat1)) new_outfit = @outfit.with_item(hat) @@ -170,7 +170,7 @@ RSpec.describe Outfit do end it "is idempotent (adding same item twice has no effect)" do - hat = create_item("Hat", zones(:hat)) + hat = create_item("Hat", zones(:hat1)) outfit1 = @outfit.with_item(hat) outfit2 = outfit1.with_item(hat) @@ -182,7 +182,7 @@ RSpec.describe Outfit do it "does not add items that don't fit this pet" do # Create item with wrong body_id - hat = create_item("Hat", zones(:hat), body_id: 999) + hat = create_item("Hat", zones(:hat1), body_id: 999) new_outfit = @outfit.with_item(hat) @@ -191,8 +191,8 @@ RSpec.describe Outfit do context "with conflicting items" do it "moves conflicting item to closet when items occupy the same zone" do - hat1 = create_item("Hat 1", zones(:hat)) - hat2 = create_item("Hat 2", zones(:hat)) + hat1 = create_item("Hat 1", zones(:hat1)) + hat2 = create_item("Hat 2", zones(:hat1)) outfit_with_hat1 = @outfit.with_item(hat1) outfit_with_hat2 = outfit_with_hat1.with_item(hat2) @@ -211,7 +211,7 @@ RSpec.describe Outfit do zones_restrict_array = Array.new(52, "0") zones_restrict_array[28] = "1" zones_restrict = zones_restrict_array.join - restricting_hat = create_item("Restricting Hat", zones(:hat), zones_restrict: zones_restrict) + restricting_hat = create_item("Restricting Hat", zones(:hat1), zones_restrict: zones_restrict) # First wear ruff item, then wear restricting hat outfit_with_ruff = @outfit.with_item(ruff_item) @@ -223,7 +223,7 @@ RSpec.describe Outfit do end it "keeps compatible items when adding new item" do - hat = create_item("Hat", zones(:hat)) + hat = create_item("Hat", zones(:hat1)) shirt = create_item("Shirt", zones(:shirtdress)) pants = create_item("Pants", zones(:trousers)) @@ -235,9 +235,9 @@ RSpec.describe Outfit do end it "can move multiple conflicting items to closet" do - hat1 = create_item("Hat 1", zones(:hat)) - hat2 = create_item("Hat 2", zones(:hat)) - hat3 = create_item("Hat 3", zones(:hat)) + hat1 = create_item("Hat 1", zones(:hat1)) + hat2 = create_item("Hat 2", zones(:hat1)) + hat3 = create_item("Hat 3", zones(:hat1)) # Wear hat1 and hat2 by manually building the outfit # (normally you can't, but we're testing the conflict resolution) @@ -253,8 +253,8 @@ RSpec.describe Outfit do end it "does not duplicate items in closet if already closeted" do - hat1 = create_item("Hat 1", zones(:hat)) - hat2 = create_item("Hat 2", zones(:hat)) + hat1 = create_item("Hat 1", zones(:hat1)) + hat2 = create_item("Hat 2", zones(:hat1)) # Wear hat1 outfit1 = @outfit.with_item(hat1) @@ -278,11 +278,552 @@ RSpec.describe Outfit do it "works with outfit that has no pet_state" do # This shouldn't happen in practice, but let's be defensive outfit_no_pet = Outfit.new - hat = create_item("Hat", zones(:hat)) + hat = create_item("Hat", zones(:hat1)) # Should not crash, but also won't add the item expect { outfit_no_pet.with_item(hat) }.not_to raise_error end end end + + # Helper to create a pet state with specific swf_assets + def build_pet_state(pet_type, pose: "HAPPY_FEM", swf_assets: []) + pet_state = PetState.create!( + pet_type: pet_type, + pose: pose, + swf_asset_ids: swf_assets.empty? ? [0] : swf_assets.map(&:id) + ) + pet_state.swf_assets = swf_assets + pet_state + end + + # Helper to create a SwfAsset for biology (pet layers) + def build_biology_asset(zone, body_id:, zones_restrict: "") + @remote_id = (@remote_id || 0) + 1 + SwfAsset.create!( + type: "biology", + remote_id: @remote_id, + url: "https://images.neopets.example/biology_#{@remote_id}.swf", + zone: zone, + body_id: body_id, + zones_restrict: zones_restrict + ) + end + + # Helper to create a SwfAsset for items (object layers) + def build_item_asset(zone, body_id:, zones_restrict: "") + @remote_id = (@remote_id || 0) + 1 + SwfAsset.create!( + type: "object", + remote_id: @remote_id, + url: "https://images.neopets.example/object_#{@remote_id}.swf", + zone: zone, + body_id: body_id, + zones_restrict: zones_restrict + ) + end + + # Helper to create an item with specific swf_assets + def build_item(name, swf_assets: []) + item = Item.create!( + name: name, + description: "Test item", + thumbnail_url: "https://images.neopets.example/thumbnail.png", + rarity: "Common", + price: 100, + zones_restrict: "", + species_support_ids: "" + ) + swf_assets.each do |asset| + ParentSwfAssetRelationship.create!( + parent: item, + swf_asset: asset + ) + end + item + end + + describe "#visible_layers" do + before do + # Clean up any existing pet types to avoid conflicts + PetType.destroy_all + + # Create a basic pet type for testing + @pet_type = PetType.create!( + species: species(:acara), + color: colors(:blue), + body_id: 1, + created_at: Time.new(2005) + ) + end + + context "basic layer composition" do + it "returns pet layers when no items are worn" do + # Create biology assets for the pet + head = build_biology_asset(zones(:head), body_id: 1) + body = build_biology_asset(zones(:body), body_id: 1) + + pet_state = build_pet_state(@pet_type, swf_assets: [head, body]) + outfit = Outfit.new(pet_state: pet_state) + + layers = outfit.visible_layers + + expect(layers).to contain_exactly(head, body) + end + + it "returns pet layers and item layers when items are worn" do + # Create pet layers + head = build_biology_asset(zones(:head), body_id: 1) + body = build_biology_asset(zones(:body), body_id: 1) + pet_state = build_pet_state(@pet_type, swf_assets: [head, body]) + + # Create item layers + hat_asset = build_item_asset(zones(:hat1), body_id: 1) + hat = build_item("Test Hat", swf_assets: [hat_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [hat] + + layers = outfit.visible_layers + + expect(layers).to contain_exactly(head, body, hat_asset) + end + + it "includes body_id=0 items that fit all pets" do + # Create pet layers + head = build_biology_asset(zones(:head), body_id: 1) + pet_state = build_pet_state(@pet_type, swf_assets: [head]) + + # Create a background item (body_id=0, fits all) + bg_asset = build_item_asset(zones(:background), body_id: 0) + background = build_item("Test Background", swf_assets: [bg_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [background] + + layers = outfit.visible_layers + + expect(layers).to contain_exactly(head, bg_asset) + end + end + + context "items restricting pet layers (Rule 3a)" do + it "hides pet layers in zones that items restrict" do + # Create pet layers including hair + head = build_biology_asset(zones(:head), body_id: 1) + hair = build_biology_asset(zones(:hairfront), body_id: 1) + pet_state = build_pet_state(@pet_type, swf_assets: [head, hair]) + + # Create a hat that restricts the hair zone + # zones_restrict is a bitfield where position 37 (Hair Front zone id) is "1" + zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1 + hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict) + hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [hat] + + layers = outfit.visible_layers + + # Hair should be hidden, but head and hat should be visible + expect(layers).to contain_exactly(head, hat_asset) + expect(layers).not_to include(hair) + end + + it "hides multiple pet layers when item restricts multiple zones" do + # Create pet layers + head = build_biology_asset(zones(:head), body_id: 1) + hair_front = build_biology_asset(zones(:hairfront), body_id: 1) + head_transient = build_biology_asset(zones(:headtransientbiology), body_id: 1) + pet_state = build_pet_state(@pet_type, swf_assets: [head, hair_front, head_transient]) + + # Create an item that restricts both Hair Front (37) and Head Transient Biology (38) + zones_restrict = "0" * 36 + "11" + "0" * 20 # bits 37 and 38 = 1 + hood_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict) + hood = build_item("Agent Hood", swf_assets: [hood_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [hood] + + layers = outfit.visible_layers + + # Both hair_front and head_transient should be hidden + expect(layers).to contain_exactly(head, hood_asset) + expect(layers).not_to include(hair_front, head_transient) + end + end + + context "pets restricting body-specific item layers (Rule 3b)" do + it "hides body-specific items in zones the pet restricts" do + # Create a pet with a layer that restricts the Static zone (46) + head = build_biology_asset(zones(:head), body_id: 1) + zones_restrict = "0" * 45 + "1" + "0" * 10 # bit 46 = 1 + restricting_layer = build_biology_asset(zones(:body), body_id: 1, zones_restrict: zones_restrict) + pet_state = build_pet_state(@pet_type, swf_assets: [head, restricting_layer]) + + # Create a body-specific Static item + static_asset = build_item_asset(zones(:static), body_id: 1) + static_item = build_item("Body-specific Static", swf_assets: [static_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [static_item] + + layers = outfit.visible_layers + + # The body-specific static item should be hidden + expect(layers).to contain_exactly(head, restricting_layer) + expect(layers).not_to include(static_asset) + end + + it "allows body_id=0 items even in zones the pet restricts" do + # Create a pet with a layer that restricts the Background Item zone (48) + # Background Item is type_id 3 (universal zone), so body_id=0 items should always work + head = build_biology_asset(zones(:head), body_id: 1) + zones_restrict = "0" * 47 + "1" + "0" * 10 # bit 48 = 1 + restricting_layer = build_biology_asset(zones(:body), body_id: 1, zones_restrict: zones_restrict) + pet_state = build_pet_state(@pet_type, swf_assets: [head, restricting_layer]) + + # Create a body_id=0 Background Item (fits all bodies, universal zone) + bg_item_asset = build_item_asset(zones(:backgrounditem), body_id: 0) + bg_item = build_item("Universal Background Item", swf_assets: [bg_item_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [bg_item] + + layers = outfit.visible_layers + + # The body_id=0 item should be visible even though the zone is restricted + expect(layers).to contain_exactly(head, restricting_layer, bg_item_asset) + end + end + + context "UNCONVERTED pets (Rule 3b special case)" do + it "rejects all body-specific items" do + # Create an UNCONVERTED pet + head = build_biology_asset(zones(:head), body_id: 1) + body = build_biology_asset(zones(:body), body_id: 1) + pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head, body]) + + # Create both body-specific and body_id=0 items + body_specific_asset = build_item_asset(zones(:hat1), body_id: 1) + body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset]) + + universal_asset = build_item_asset(zones(:background), body_id: 0) + universal_item = build_item("Universal Background", swf_assets: [universal_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [body_specific_item, universal_item] + + layers = outfit.visible_layers + + # Only body_id=0 items should be visible + expect(layers).to contain_exactly(head, body, universal_asset) + expect(layers).not_to include(body_specific_asset) + end + + it "rejects body-specific items regardless of zone restrictions" do + # Create an UNCONVERTED pet with no zone restrictions + head = build_biology_asset(zones(:head), body_id: 1) + pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head]) + + # Create a body-specific item in a zone the pet doesn't restrict + hat_asset = build_item_asset(zones(:hat1), body_id: 1) + hat = build_item("Body-specific Hat", swf_assets: [hat_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [hat] + + layers = outfit.visible_layers + + # The body-specific item should still be hidden + expect(layers).to contain_exactly(head) + expect(layers).not_to include(hat_asset) + end + end + + context "pets restricting their own layers (Rule 3c)" do + it "hides pet layers in zones the pet itself restricts" do + # Create a pet with a horn asset and a layer that restricts the horn's zone + # (Simulating the Wraith Uni case) + body = build_biology_asset(zones(:body), body_id: 1) + + # Create a horn in the Head Transient Biology zone (38) + horn = build_biology_asset(zones(:headtransientbiology), body_id: 1) + + # Create a layer that restricts zone 38 + zones_restrict = "0" * 37 + "1" + "0" * 20 # bit 38 = 1 + restricting_layer = build_biology_asset(zones(:head), body_id: 1, zones_restrict: zones_restrict) + + pet_state = build_pet_state(@pet_type, swf_assets: [body, horn, restricting_layer]) + + outfit = Outfit.new(pet_state: pet_state) + + layers = outfit.visible_layers + + # The horn should be hidden by the pet's own restrictions + expect(layers).to contain_exactly(body, restricting_layer) + expect(layers).not_to include(horn) + end + + it "applies self-restrictions in combination with item restrictions" do + # Create a pet with multiple layers, some restricted by itself + body = build_biology_asset(zones(:body), body_id: 1) + hair = build_biology_asset(zones(:hairfront), body_id: 1) + + # Pet restricts its own Head zone (30) + zones_restrict = "0" * 29 + "1" + "0" * 25 # bit 30 = 1 + head = build_biology_asset(zones(:head), body_id: 1) + restricting_layer = build_biology_asset(zones(:eyes), body_id: 1, zones_restrict: zones_restrict) + + pet_state = build_pet_state(@pet_type, swf_assets: [body, hair, head, restricting_layer]) + + # Add an item that restricts Hair Front (37) + item_zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1 + hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: item_zones_restrict) + hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [hat] + + layers = outfit.visible_layers + + # Hair should be hidden by item, Head should be hidden by pet's own restrictions + expect(layers).to contain_exactly(body, restricting_layer, hat_asset) + expect(layers).not_to include(hair, head) + end + end + + context "depth sorting and layer ordering" do + it "sorts layers by zone depth" do + # Create layers in various zones with different depths + background = build_biology_asset(zones(:background), body_id: 1) # depth 3 + body_layer = build_biology_asset(zones(:body), body_id: 1) # depth 18 + head_layer = build_biology_asset(zones(:head), body_id: 1) # depth 34 + + pet_state = build_pet_state(@pet_type, swf_assets: [head_layer, background, body_layer]) + + outfit = Outfit.new(pet_state: pet_state) + + layers = outfit.visible_layers + + # Should be sorted by depth: background (3) < body (18) < head (34) + expect(layers[0]).to eq(background) + expect(layers[1]).to eq(body_layer) + expect(layers[2]).to eq(head_layer) + end + + it "places item layers after pet layers at the same depth" do + # Create a pet layer and item layer in zones with the same depth + # Static zone has depth 48 + pet_static = build_biology_asset(zones(:static), body_id: 1) + pet_state = build_pet_state(@pet_type, swf_assets: [pet_static]) + + item_static = build_item_asset(zones(:static), body_id: 0) + static_item = build_item("Static Item", swf_assets: [item_static]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [static_item] + + layers = outfit.visible_layers + + # Both should be present, with item layer last (on top) + expect(layers).to eq([pet_static, item_static]) + end + + it "sorts complex outfits correctly by depth" do + # Create a complex outfit with multiple pet and item layers + background = build_biology_asset(zones(:background), body_id: 1) # depth 3 + body_layer = build_biology_asset(zones(:body), body_id: 1) # depth 18 + head_layer = build_biology_asset(zones(:head), body_id: 1) # depth 34 + + pet_state = build_pet_state(@pet_type, swf_assets: [head_layer, background, body_layer]) + + # Add items at various depths + bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4 + hat_asset = build_item_asset(zones(:hat1), body_id: 1) # depth 44 + shirt_asset = build_item_asset(zones(:shirtdress), body_id: 1) # depth 26 + + bg = build_item("Background Item", swf_assets: [bg_item]) + hat = build_item("Hat", swf_assets: [hat_asset]) + shirt = build_item("Shirt", swf_assets: [shirt_asset]) + + outfit = Outfit.new(pet_state: pet_state) + outfit.worn_items = [hat, bg, shirt] + + layers = outfit.visible_layers + + # Expected order by depth: + # background (3), bg_item (4), body_layer (18), shirt_asset (26), + # head_layer (34), hat_asset (44) + expect(layers.map(&:depth)).to eq([3, 4, 18, 26, 34, 44]) + expect(layers).to eq([background, bg_item, body_layer, shirt_asset, head_layer, hat_asset]) + end + end + + context "alt styles (alternative pet appearances)" do + before do + # Create an alt style with its own body_id distinct from regular pets + @alt_style = AltStyle.create!( + species: species(:acara), + color: colors(:blue), + body_id: 999, # Distinct from the regular pet's body_id (1) + series_name: "Nostalgic", + thumbnail_url: "https://images.neopets.example/alt_style.png" + ) + end + + it "uses alt style layers instead of pet state layers" do + # Create regular pet layers + regular_head = build_biology_asset(zones(:head), body_id: 1) + regular_body = build_biology_asset(zones(:body), body_id: 1) + pet_state = build_pet_state(@pet_type, swf_assets: [regular_head, regular_body]) + + # Create alt style layers (with the alt style's body_id) + alt_head = build_biology_asset(zones(:head), body_id: 999) + alt_body = build_biology_asset(zones(:body), body_id: 999) + @alt_style.swf_assets = [alt_head, alt_body] + + # Create outfit with alt_style + outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) + + layers = outfit.visible_layers + + # Should use alt style layers, not pet state layers + expect(layers).to contain_exactly(alt_head, alt_body) + expect(layers).not_to include(regular_head, regular_body) + end + + it "only includes body_id=0 items with alt styles" do + # Create alt style layers + alt_head = build_biology_asset(zones(:head), body_id: 999) + @alt_style.swf_assets = [alt_head] + pet_state = build_pet_state(@pet_type) + + # Create a body-specific item for the alt style's body_id + body_specific_asset = build_item_asset(zones(:hat1), body_id: 999) + body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset]) + + # Create a universal item (body_id=0) + universal_asset = build_item_asset(zones(:background), body_id: 0) + universal_item = build_item("Universal Background", swf_assets: [universal_asset]) + + outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) + outfit.worn_items = [body_specific_item, universal_item] + + layers = outfit.visible_layers + + # Only the universal item should appear + expect(layers).to contain_exactly(alt_head, universal_asset) + expect(layers).not_to include(body_specific_asset) + end + + it "does not include items from the regular pet's body_id" do + # Create alt style layers + alt_body = build_biology_asset(zones(:body), body_id: 999) + @alt_style.swf_assets = [alt_body] + pet_state = build_pet_state(@pet_type) + + # Create an item that fits the regular pet's body_id (1) + regular_item_asset = build_item_asset(zones(:hat1), body_id: 1) + regular_item = build_item("Regular Pet Hat", swf_assets: [regular_item_asset]) + + outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) + outfit.worn_items = [regular_item] + + layers = outfit.visible_layers + + # The regular pet item should not appear on the alt style + expect(layers).to contain_exactly(alt_body) + expect(layers).not_to include(regular_item_asset) + end + + it "applies item restriction rules with alt styles" do + # Create alt style layers including hair + alt_head = build_biology_asset(zones(:head), body_id: 999) + alt_hair = build_biology_asset(zones(:hairfront), body_id: 999) + @alt_style.swf_assets = [alt_head, alt_hair] + pet_state = build_pet_state(@pet_type) + + # Create a universal hat that restricts the hair zone + zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 (Hair Front) = 1 + hat_asset = build_item_asset(zones(:hat1), body_id: 0, zones_restrict: zones_restrict) + hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset]) + + outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) + outfit.worn_items = [hat] + + layers = outfit.visible_layers + + # Hair should be hidden by the hat's zone restrictions + expect(layers).to contain_exactly(alt_head, hat_asset) + expect(layers).not_to include(alt_hair) + end + + it "applies pet restriction rules with alt styles" do + # Create alt style with a layer that restricts a zone + alt_head = build_biology_asset(zones(:head), body_id: 999) + zones_restrict = "0" * 47 + "1" + "0" * 10 # bit 48 (Background Item) = 1 + restricting_layer = build_biology_asset(zones(:body), body_id: 999, zones_restrict: zones_restrict) + @alt_style.swf_assets = [alt_head, restricting_layer] + pet_state = build_pet_state(@pet_type) + + # Create a universal Background Item + bg_item_asset = build_item_asset(zones(:backgrounditem), body_id: 0) + bg_item = build_item("Universal Background Item", swf_assets: [bg_item_asset]) + + outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) + outfit.worn_items = [bg_item] + + layers = outfit.visible_layers + + # body_id=0 items should still appear even in restricted zones + # (because they're not body-specific) + expect(layers).to contain_exactly(alt_head, restricting_layer, bg_item_asset) + end + + it "applies self-restriction rules with alt styles" do + # Create alt style that restricts its own horn layer + alt_body = build_biology_asset(zones(:body), body_id: 999) + alt_horn = build_biology_asset(zones(:headtransientbiology), body_id: 999) + zones_restrict = "0" * 37 + "1" + "0" * 20 # bit 38 (Head Transient Biology) = 1 + restricting_layer = build_biology_asset(zones(:head), body_id: 999, zones_restrict: zones_restrict) + @alt_style.swf_assets = [alt_body, alt_horn, restricting_layer] + pet_state = build_pet_state(@pet_type) + + outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) + + layers = outfit.visible_layers + + # The horn should be hidden by the alt style's own restrictions + expect(layers).to contain_exactly(alt_body, restricting_layer) + expect(layers).not_to include(alt_horn) + end + + it "sorts alt style and item layers by depth correctly" do + # Create alt style layers at various depths + alt_background = build_biology_asset(zones(:background), body_id: 999) # depth 3 + alt_body = build_biology_asset(zones(:body), body_id: 999) # depth 18 + alt_head = build_biology_asset(zones(:head), body_id: 999) # depth 34 + @alt_style.swf_assets = [alt_head, alt_background, alt_body] + pet_state = build_pet_state(@pet_type) + + # Add universal items at various depths + bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4 + trinket = build_item_asset(zones(:righthanditem1), body_id: 0) # depth 46 + + bg = build_item("Background Item", swf_assets: [bg_item]) + trinket_item = build_item("Trinket", swf_assets: [trinket]) + + outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) + outfit.worn_items = [trinket_item, bg] + + layers = outfit.visible_layers + + # Expected order by depth: + # alt_background (3), bg_item (4), alt_body (18), alt_head (34), trinket (46) + expect(layers.map(&:depth)).to eq([3, 4, 18, 34, 46]) + expect(layers).to eq([alt_background, bg_item, alt_body, alt_head, trinket]) + end + end + end end diff --git a/spec/models/pet/auto_modeling_spec.rb b/spec/models/pet/auto_modeling_spec.rb new file mode 100644 index 00000000..de349c2e --- /dev/null +++ b/spec/models/pet/auto_modeling_spec.rb @@ -0,0 +1,92 @@ +require_relative '../../rails_helper' +require_relative '../../support/mocks/custom_pets' +require_relative '../../support/mocks/nc_mall' + +RSpec.describe Pet::AutoModeling, type: :model do + fixtures :colors, :species, :zones + + # Set up a Purple Chia pet type (body_id 212) for testing + let!(:pet_type) do + PetType.create!( + species_id: Species.find_by_name!("chia").id, + color_id: Color.find_by_name!("purple").id, + body_id: 212, + image_hash: "purpchia" + ) + end + + # A known compatible item for testing (exists in mock data) + let(:compatible_item) do + Item.create!( + id: 71706, + name: "On the Roof Background", + description: "Who is that on the roof?! Could it be...?", + thumbnail_url: "https://images.neopets.com/items/gif_roof_onthe_fg.gif", + rarity: "Special", + rarity_index: 101, + price: 0, + zones_restrict: "0000000000000000000000000000000000000000000000000000" + ) + end + + describe ".model_item_on_body" do + context "when item is compatible with the body" do + let(:item) { compatible_item } + + it "returns :modeled" do + result = Pet::AutoModeling.model_item_on_body(item, 212) + expect(result).to eq :modeled + end + + it "creates SwfAsset records for the item" do + expect { + Pet::AutoModeling.model_item_on_body(item, 212) + }.to change { SwfAsset.where(type: "object").count }.by(1) + end + + it "associates the SwfAsset with the item" do + Pet::AutoModeling.model_item_on_body(item, 212) + item.reload + + asset = item.swf_assets.find_by(remote_id: 410722) + expect(asset).to be_present + expect(asset.body_id).to eq 0 # This item fits all bodies + expect(asset.zone_id).to eq 3 + end + end + + context "when item is not in the response" do + let(:item) do + # Create an item that won't be in our mock response + Item.create!( + id: 99999, + name: "Nonexistent Item", + description: "This item doesn't exist in the mock", + thumbnail_url: "https://example.com/item.gif", + rarity: "Special", + rarity_index: 101, + price: 0, + zones_restrict: "0000000000000000000000000000000000000000000000000000" + ) + end + + it "returns :not_compatible" do + result = Pet::AutoModeling.model_item_on_body(item, 212) + expect(result).to eq :not_compatible + end + end + + context "when no PetType exists for the body_id" do + let(:item) { compatible_item } + + it "raises NoPetTypeForBody" do + expect { + Pet::AutoModeling.model_item_on_body(item, 99999) + }.to raise_error(Pet::AutoModeling::NoPetTypeForBody) do |error| + expect(error.body_id).to eq 99999 + expect(error.message).to include "99999" + end + end + end + end +end diff --git a/spec/support/mocks/custom_pets.rb b/spec/support/mocks/custom_pets.rb index 33bb3e70..5760a3f2 100644 --- a/spec/support/mocks/custom_pets.rb +++ b/spec/support/mocks/custom_pets.rb @@ -3,9 +3,14 @@ module Neopets::CustomPets DATA_DIR = Pathname.new(__dir__) / "custom_pets" def self.fetch_viewer_data(pet_name, ...) - File.open(DATA_DIR / "#{pet_name}.json") do |file| - HashWithIndifferentAccess.new JSON.load(file) + # NOTE: Windows doesn't support `@` in filenames, so we use a `scis` directory instead. + path = if pet_name.start_with?('@') + DATA_DIR / "scis" / "#{pet_name[1..]}.json" + else + DATA_DIR / "#{pet_name}.json" end + + File.open(path) { |f| HashWithIndifferentAccess.new JSON.load(f) } end def self.fetch_metadata(...) diff --git a/spec/support/mocks/custom_pets/scis/purpchia-39552.json b/spec/support/mocks/custom_pets/scis/purpchia-39552.json new file mode 100644 index 00000000..87524fec --- /dev/null +++ b/spec/support/mocks/custom_pets/scis/purpchia-39552.json @@ -0,0 +1,69 @@ +{ + "custom_pet": { + "name": "@mock:m:thyass:39552", + "owner": "", + "slot": 1.0, + "scale": 0.5, + "muted": true, + "body_id": 212.0, + "species_id": 6.0, + "color_id": 61.0, + "alt_style": false, + "alt_color": 61.0, + "style_closet_id": null, + "biology_by_zone": { + "37": { + "part_id": 10083.0, + "zone_id": 37.0, + "asset_url": "https://images.neopets.com/cp/bio/swf/000/000/010/10083_8a1111a13f.swf", + "manifest": "https://images.neopets.com/cp/bio/data/000/000/010/10083_8a1111a13f/manifest.json", + "zones_restrict": "0000000000000000000000000000000000000000000000000000" + }, + "15": { + "part_id": 11613.0, + "zone_id": 15.0, + "asset_url": "https://images.neopets.com/cp/bio/swf/000/000/011/11613_f7d8d377ab.swf", + "manifest": "https://images.neopets.com/cp/bio/data/000/000/011/11613_f7d8d377ab/manifest.json", + "zones_restrict": "0000000000000000000000000000000000000000000000000000" + }, + "34": { + "part_id": 14187.0, + "zone_id": 34.0, + "asset_url": "https://images.neopets.com/cp/bio/swf/000/000/014/14187_0e65c2082f.swf", + "manifest": "https://images.neopets.com/cp/bio/data/000/000/014/14187_0e65c2082f/manifest.json", + "zones_restrict": "0000000000000000000000000000000000000000000000000000" + }, + "33": { + "part_id": 14189.0, + "zone_id": 33.0, + "asset_url": "https://images.neopets.com/cp/bio/swf/000/000/014/14189_102e4991e9.swf", + "manifest": "https://images.neopets.com/cp/bio/data/000/000/014/14189_102e4991e9/manifest.json", + "zones_restrict": "0000000000000000000000000000000000000000000000000000" + } + }, + "equipped_by_zone": {}, + "original_biology": [] + }, + "closet_items": {}, + "object_info_registry": { + "39552": { + "obj_info_id": 39552.0, + "assets_by_zone": {}, + "zones_restrict": "0000000000000000000000000000000000000000000000000000", + "is_compatible": false, + "is_paid": true, + "thumbnail_url": "https://images.neopets.com/items/mall_springyeyeglasses.gif", + "name": "Springy Eye Glasses", + "description": "Hey, keep your eyes in your head!", + "category": "Clothes", + "type": "Clothes", + "rarity": "Artifact", + "rarity_index": 500.0, + "price": 0.0, + "weight_lbs": 1.0, + "species_support": [3.0], + "converted": true + } + }, + "object_asset_registry": {} +} diff --git a/spec/support/mocks/custom_pets/scis/purpchia-71706.json b/spec/support/mocks/custom_pets/scis/purpchia-71706.json new file mode 100644 index 00000000..cec79f7e --- /dev/null +++ b/spec/support/mocks/custom_pets/scis/purpchia-71706.json @@ -0,0 +1,85 @@ +{ + "custom_pet": { + "name": "@mock:m:thyass:71706", + "owner": "", + "slot": 1.0, + "scale": 0.5, + "muted": true, + "body_id": 212.0, + "species_id": 6.0, + "color_id": 61.0, + "alt_style": false, + "alt_color": 61.0, + "style_closet_id": null, + "biology_by_zone": { + "37": { + "part_id": 10083.0, + "zone_id": 37.0, + "asset_url": "https://images.neopets.com/cp/bio/swf/000/000/010/10083_8a1111a13f.swf", + "manifest": "https://images.neopets.com/cp/bio/data/000/000/010/10083_8a1111a13f/manifest.json", + "zones_restrict": "0000000000000000000000000000000000000000000000000000" + }, + "15": { + "part_id": 11613.0, + "zone_id": 15.0, + "asset_url": "https://images.neopets.com/cp/bio/swf/000/000/011/11613_f7d8d377ab.swf", + "manifest": "https://images.neopets.com/cp/bio/data/000/000/011/11613_f7d8d377ab/manifest.json", + "zones_restrict": "0000000000000000000000000000000000000000000000000000" + }, + "34": { + "part_id": 14187.0, + "zone_id": 34.0, + "asset_url": "https://images.neopets.com/cp/bio/swf/000/000/014/14187_0e65c2082f.swf", + "manifest": "https://images.neopets.com/cp/bio/data/000/000/014/14187_0e65c2082f/manifest.json", + "zones_restrict": "0000000000000000000000000000000000000000000000000000" + }, + "33": { + "part_id": 14189.0, + "zone_id": 33.0, + "asset_url": "https://images.neopets.com/cp/bio/swf/000/000/014/14189_102e4991e9.swf", + "manifest": "https://images.neopets.com/cp/bio/data/000/000/014/14189_102e4991e9/manifest.json", + "zones_restrict": "0000000000000000000000000000000000000000000000000000" + } + }, + "equipped_by_zone": { + "3": { + "asset_id": 410722.0, + "zone_id": 3.0, + "closet_obj_id": 0.0 + } + }, + "original_biology": [] + }, + "closet_items": {}, + "object_info_registry": { + "71706": { + "obj_info_id": 71706.0, + "assets_by_zone": { + "3": 410722.0 + }, + "zones_restrict": "0000000000000000000000000000000000000000000000000000", + "is_compatible": true, + "is_paid": false, + "thumbnail_url": "https://images.neopets.com/items/gif_roof_onthe_fg.gif", + "name": "On the Roof Background", + "description": "Who is that on the roof?! Could it be...?", + "category": "Special", + "type": "Mystical Surroundings", + "rarity": "Special", + "rarity_index": 101.0, + "price": 0.0, + "weight_lbs": 1.0, + "species_support": [], + "converted": true + } + }, + "object_asset_registry": { + "410722": { + "asset_id": 410722.0, + "zone_id": 3.0, + "asset_url": "https://images.neopets.com/cp/items/swf/000/000/410/410722_3bcd2f5e11.swf", + "obj_info_id": 71706.0, + "manifest": "https://images.neopets.com/cp/items/data/000/000/410/410722_3bcd2f5e11/manifest.json?v=1706" + } + } +} diff --git a/spec/support/mocks/custom_pets/scis/purpchia-99999.json b/spec/support/mocks/custom_pets/scis/purpchia-99999.json new file mode 100644 index 00000000..15be1377 --- /dev/null +++ b/spec/support/mocks/custom_pets/scis/purpchia-99999.json @@ -0,0 +1,24 @@ +{ + "custom_pet": { + "name": "@purpchia:99999", + "body_id": 212.0, + "species_id": 6.0, + "color_id": 61.0, + "alt_style": false, + "alt_color": 61.0, + "biology_by_zone": { + "15": { + "part_id": 11613.0, + "zone_id": 15.0, + "asset_url": "https://images.neopets.com/cp/bio/swf/000/000/011/11613_f7d8d377ab.swf", + "manifest": "https://images.neopets.com/cp/bio/data/000/000/011/11613_f7d8d377ab/manifest.json", + "zones_restrict": "0000000000000000000000000000000000000000000000000000" + } + }, + "equipped_by_zone": [], + "original_biology": [] + }, + "closet_items": [], + "object_info_registry": [], + "object_asset_registry": [] +} \ No newline at end of file diff --git a/spec/support/mocks/nc_mall.rb b/spec/support/mocks/nc_mall.rb new file mode 100644 index 00000000..a4485a78 --- /dev/null +++ b/spec/support/mocks/nc_mall.rb @@ -0,0 +1,8 @@ +# We replace Neopets::NCMall.fetch_pet_data_sci with a mocked implementation. +module Neopets::NCMall + # Mock implementation that generates predictable SCI hashes for testing. + # The hash is derived from the pet_sci and item_ids to ensure consistency. + def self.fetch_pet_data_sci(pet_sci, item_ids = []) + "#{pet_sci}-#{item_ids.sort.join('-')}" + end +end diff --git a/vendor/cache/action_text-trix-2.1.15.gem b/vendor/cache/action_text-trix-2.1.15.gem deleted file mode 100644 index 5bd03361..00000000 Binary files a/vendor/cache/action_text-trix-2.1.15.gem and /dev/null differ diff --git a/vendor/cache/action_text-trix-2.1.16.gem b/vendor/cache/action_text-trix-2.1.16.gem new file mode 100644 index 00000000..15df629a Binary files /dev/null and b/vendor/cache/action_text-trix-2.1.16.gem differ diff --git a/vendor/cache/actioncable-8.1.1.gem b/vendor/cache/actioncable-8.1.1.gem deleted file mode 100644 index e26fdbf9..00000000 Binary files a/vendor/cache/actioncable-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/actioncable-8.1.2.gem b/vendor/cache/actioncable-8.1.2.gem new file mode 100644 index 00000000..bb0fc73c Binary files /dev/null and b/vendor/cache/actioncable-8.1.2.gem differ diff --git a/vendor/cache/actionmailbox-8.1.1.gem b/vendor/cache/actionmailbox-8.1.1.gem deleted file mode 100644 index f4338582..00000000 Binary files a/vendor/cache/actionmailbox-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/actionmailbox-8.1.2.gem b/vendor/cache/actionmailbox-8.1.2.gem new file mode 100644 index 00000000..b2bfc8a2 Binary files /dev/null and b/vendor/cache/actionmailbox-8.1.2.gem differ diff --git a/vendor/cache/actionmailer-8.1.1.gem b/vendor/cache/actionmailer-8.1.1.gem deleted file mode 100644 index d3cd2ef3..00000000 Binary files a/vendor/cache/actionmailer-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/actionmailer-8.1.2.gem b/vendor/cache/actionmailer-8.1.2.gem new file mode 100644 index 00000000..5fb54a42 Binary files /dev/null and b/vendor/cache/actionmailer-8.1.2.gem differ diff --git a/vendor/cache/actionpack-8.1.1.gem b/vendor/cache/actionpack-8.1.1.gem deleted file mode 100644 index 024cef1c..00000000 Binary files a/vendor/cache/actionpack-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/actionpack-8.1.2.gem b/vendor/cache/actionpack-8.1.2.gem new file mode 100644 index 00000000..cad109f9 Binary files /dev/null and b/vendor/cache/actionpack-8.1.2.gem differ diff --git a/vendor/cache/actiontext-8.1.1.gem b/vendor/cache/actiontext-8.1.1.gem deleted file mode 100644 index eda358ed..00000000 Binary files a/vendor/cache/actiontext-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/actiontext-8.1.2.gem b/vendor/cache/actiontext-8.1.2.gem new file mode 100644 index 00000000..b2382c6a Binary files /dev/null and b/vendor/cache/actiontext-8.1.2.gem differ diff --git a/vendor/cache/actionview-8.1.1.gem b/vendor/cache/actionview-8.1.1.gem deleted file mode 100644 index 94d28fc6..00000000 Binary files a/vendor/cache/actionview-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/actionview-8.1.2.gem b/vendor/cache/actionview-8.1.2.gem new file mode 100644 index 00000000..16092aa8 Binary files /dev/null and b/vendor/cache/actionview-8.1.2.gem differ diff --git a/vendor/cache/activejob-8.1.1.gem b/vendor/cache/activejob-8.1.1.gem deleted file mode 100644 index 9ed139da..00000000 Binary files a/vendor/cache/activejob-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/activejob-8.1.2.gem b/vendor/cache/activejob-8.1.2.gem new file mode 100644 index 00000000..4c1ccae1 Binary files /dev/null and b/vendor/cache/activejob-8.1.2.gem differ diff --git a/vendor/cache/activemodel-8.1.1.gem b/vendor/cache/activemodel-8.1.1.gem deleted file mode 100644 index 216afa4d..00000000 Binary files a/vendor/cache/activemodel-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/activemodel-8.1.2.gem b/vendor/cache/activemodel-8.1.2.gem new file mode 100644 index 00000000..c5c7f734 Binary files /dev/null and b/vendor/cache/activemodel-8.1.2.gem differ diff --git a/vendor/cache/activerecord-8.1.1.gem b/vendor/cache/activerecord-8.1.1.gem deleted file mode 100644 index fa7e0dbb..00000000 Binary files a/vendor/cache/activerecord-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/activerecord-8.1.2.gem b/vendor/cache/activerecord-8.1.2.gem new file mode 100644 index 00000000..a9797c55 Binary files /dev/null and b/vendor/cache/activerecord-8.1.2.gem differ diff --git a/vendor/cache/activestorage-8.1.1.gem b/vendor/cache/activestorage-8.1.1.gem deleted file mode 100644 index 9e8d1182..00000000 Binary files a/vendor/cache/activestorage-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/activestorage-8.1.2.gem b/vendor/cache/activestorage-8.1.2.gem new file mode 100644 index 00000000..e1021759 Binary files /dev/null and b/vendor/cache/activestorage-8.1.2.gem differ diff --git a/vendor/cache/activesupport-8.1.1.gem b/vendor/cache/activesupport-8.1.1.gem deleted file mode 100644 index beabc684..00000000 Binary files a/vendor/cache/activesupport-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/activesupport-8.1.2.gem b/vendor/cache/activesupport-8.1.2.gem new file mode 100644 index 00000000..d96e98f5 Binary files /dev/null and b/vendor/cache/activesupport-8.1.2.gem differ diff --git a/vendor/cache/ast-2.4.3.gem b/vendor/cache/ast-2.4.3.gem deleted file mode 100644 index 1f5e5c25..00000000 Binary files a/vendor/cache/ast-2.4.3.gem and /dev/null differ diff --git a/vendor/cache/async-2.35.0.gem b/vendor/cache/async-2.35.0.gem deleted file mode 100644 index 1c14e3e4..00000000 Binary files a/vendor/cache/async-2.35.0.gem and /dev/null differ diff --git a/vendor/cache/async-2.35.3.gem b/vendor/cache/async-2.35.3.gem new file mode 100644 index 00000000..fdd4b864 Binary files /dev/null and b/vendor/cache/async-2.35.3.gem differ diff --git a/vendor/cache/async-container-0.27.7.gem b/vendor/cache/async-container-0.27.7.gem deleted file mode 100644 index 919808cb..00000000 Binary files a/vendor/cache/async-container-0.27.7.gem and /dev/null differ diff --git a/vendor/cache/async-container-0.29.0.gem b/vendor/cache/async-container-0.29.0.gem new file mode 100644 index 00000000..d7be7c27 Binary files /dev/null and b/vendor/cache/async-container-0.29.0.gem differ diff --git a/vendor/cache/async-service-0.16.0.gem b/vendor/cache/async-service-0.16.0.gem deleted file mode 100644 index e68368b8..00000000 Binary files a/vendor/cache/async-service-0.16.0.gem and /dev/null differ diff --git a/vendor/cache/async-service-0.17.0.gem b/vendor/cache/async-service-0.17.0.gem new file mode 100644 index 00000000..236a1a9f Binary files /dev/null and b/vendor/cache/async-service-0.17.0.gem differ diff --git a/vendor/cache/backport-1.2.0.gem b/vendor/cache/backport-1.2.0.gem deleted file mode 100644 index 20e8e145..00000000 Binary files a/vendor/cache/backport-1.2.0.gem and /dev/null differ diff --git a/vendor/cache/bcrypt-3.1.20.gem b/vendor/cache/bcrypt-3.1.20.gem deleted file mode 100644 index c1242b57..00000000 Binary files a/vendor/cache/bcrypt-3.1.20.gem and /dev/null differ diff --git a/vendor/cache/bcrypt-3.1.21.gem b/vendor/cache/bcrypt-3.1.21.gem new file mode 100644 index 00000000..93699ebd Binary files /dev/null and b/vendor/cache/bcrypt-3.1.21.gem differ diff --git a/vendor/cache/benchmark-0.5.0.gem b/vendor/cache/benchmark-0.5.0.gem deleted file mode 100644 index f68092b6..00000000 Binary files a/vendor/cache/benchmark-0.5.0.gem and /dev/null differ diff --git a/vendor/cache/bootsnap-1.20.1.gem b/vendor/cache/bootsnap-1.20.1.gem deleted file mode 100644 index 99e736d1..00000000 Binary files a/vendor/cache/bootsnap-1.20.1.gem and /dev/null differ diff --git a/vendor/cache/bootsnap-1.21.1.gem b/vendor/cache/bootsnap-1.21.1.gem new file mode 100644 index 00000000..e65da8dc Binary files /dev/null and b/vendor/cache/bootsnap-1.21.1.gem differ diff --git a/vendor/cache/dotenv-2.8.1.gem b/vendor/cache/dotenv-2.8.1.gem deleted file mode 100644 index 1e952071..00000000 Binary files a/vendor/cache/dotenv-2.8.1.gem and /dev/null differ diff --git a/vendor/cache/dotenv-3.2.0.gem b/vendor/cache/dotenv-3.2.0.gem new file mode 100644 index 00000000..5178699a Binary files /dev/null and b/vendor/cache/dotenv-3.2.0.gem differ diff --git a/vendor/cache/dotenv-rails-2.8.1.gem b/vendor/cache/dotenv-rails-2.8.1.gem deleted file mode 100644 index 1aab6b48..00000000 Binary files a/vendor/cache/dotenv-rails-2.8.1.gem and /dev/null differ diff --git a/vendor/cache/faraday-follow_redirects-0.4.0.gem b/vendor/cache/faraday-follow_redirects-0.4.0.gem deleted file mode 100644 index b8f38924..00000000 Binary files a/vendor/cache/faraday-follow_redirects-0.4.0.gem and /dev/null differ diff --git a/vendor/cache/faraday-follow_redirects-0.5.0.gem b/vendor/cache/faraday-follow_redirects-0.5.0.gem new file mode 100644 index 00000000..f09d2bd3 Binary files /dev/null and b/vendor/cache/faraday-follow_redirects-0.5.0.gem differ diff --git a/vendor/cache/ffi-1.17.2-aarch64-linux-gnu.gem b/vendor/cache/ffi-1.17.2-aarch64-linux-gnu.gem deleted file mode 100644 index 8e391bce..00000000 Binary files a/vendor/cache/ffi-1.17.2-aarch64-linux-gnu.gem and /dev/null differ diff --git a/vendor/cache/ffi-1.17.2-arm64-darwin.gem b/vendor/cache/ffi-1.17.2-arm64-darwin.gem deleted file mode 100644 index 2219753b..00000000 Binary files a/vendor/cache/ffi-1.17.2-arm64-darwin.gem and /dev/null differ diff --git a/vendor/cache/ffi-1.17.2-x86_64-linux-gnu.gem b/vendor/cache/ffi-1.17.2-x86_64-linux-gnu.gem deleted file mode 100644 index 3704931f..00000000 Binary files a/vendor/cache/ffi-1.17.2-x86_64-linux-gnu.gem and /dev/null differ diff --git a/vendor/cache/ffi-1.17.2.gem b/vendor/cache/ffi-1.17.2.gem deleted file mode 100644 index 4aacd855..00000000 Binary files a/vendor/cache/ffi-1.17.2.gem and /dev/null differ diff --git a/vendor/cache/ffi-1.17.3-aarch64-linux-gnu.gem b/vendor/cache/ffi-1.17.3-aarch64-linux-gnu.gem new file mode 100644 index 00000000..7d006cb4 Binary files /dev/null and b/vendor/cache/ffi-1.17.3-aarch64-linux-gnu.gem differ diff --git a/vendor/cache/ffi-1.17.3-arm64-darwin.gem b/vendor/cache/ffi-1.17.3-arm64-darwin.gem new file mode 100644 index 00000000..a596cad6 Binary files /dev/null and b/vendor/cache/ffi-1.17.3-arm64-darwin.gem differ diff --git a/vendor/cache/ffi-1.17.3-x86_64-linux-gnu.gem b/vendor/cache/ffi-1.17.3-x86_64-linux-gnu.gem new file mode 100644 index 00000000..2b85bc29 Binary files /dev/null and b/vendor/cache/ffi-1.17.3-x86_64-linux-gnu.gem differ diff --git a/vendor/cache/haml-6.4.0.gem b/vendor/cache/haml-6.4.0.gem deleted file mode 100644 index fec05c07..00000000 Binary files a/vendor/cache/haml-6.4.0.gem and /dev/null differ diff --git a/vendor/cache/haml-7.2.0.gem b/vendor/cache/haml-7.2.0.gem new file mode 100644 index 00000000..53fa7a62 Binary files /dev/null and b/vendor/cache/haml-7.2.0.gem differ diff --git a/vendor/cache/jaro_winkler-1.6.1.gem b/vendor/cache/jaro_winkler-1.6.1.gem deleted file mode 100644 index f413c9be..00000000 Binary files a/vendor/cache/jaro_winkler-1.6.1.gem and /dev/null differ diff --git a/vendor/cache/kramdown-2.5.1.gem b/vendor/cache/kramdown-2.5.1.gem deleted file mode 100644 index 263791ae..00000000 Binary files a/vendor/cache/kramdown-2.5.1.gem and /dev/null differ diff --git a/vendor/cache/kramdown-parser-gfm-1.1.0.gem b/vendor/cache/kramdown-parser-gfm-1.1.0.gem deleted file mode 100644 index f087be5c..00000000 Binary files a/vendor/cache/kramdown-parser-gfm-1.1.0.gem and /dev/null differ diff --git a/vendor/cache/language_server-protocol-3.17.0.5.gem b/vendor/cache/language_server-protocol-3.17.0.5.gem deleted file mode 100644 index 40a28d80..00000000 Binary files a/vendor/cache/language_server-protocol-3.17.0.5.gem and /dev/null differ diff --git a/vendor/cache/lint_roller-1.1.0.gem b/vendor/cache/lint_roller-1.1.0.gem deleted file mode 100644 index 0f874b6d..00000000 Binary files a/vendor/cache/lint_roller-1.1.0.gem and /dev/null differ diff --git a/vendor/cache/localhost-1.6.0.gem b/vendor/cache/localhost-1.6.0.gem deleted file mode 100644 index 76a41523..00000000 Binary files a/vendor/cache/localhost-1.6.0.gem and /dev/null differ diff --git a/vendor/cache/localhost-1.7.0.gem b/vendor/cache/localhost-1.7.0.gem new file mode 100644 index 00000000..fafbc576 Binary files /dev/null and b/vendor/cache/localhost-1.7.0.gem differ diff --git a/vendor/cache/mini_portile2-2.8.9.gem b/vendor/cache/mini_portile2-2.8.9.gem deleted file mode 100644 index f90f71bf..00000000 Binary files a/vendor/cache/mini_portile2-2.8.9.gem and /dev/null differ diff --git a/vendor/cache/nokogiri-1.18.10-arm64-darwin.gem b/vendor/cache/nokogiri-1.18.10-arm64-darwin.gem deleted file mode 100644 index 029fa10d..00000000 Binary files a/vendor/cache/nokogiri-1.18.10-arm64-darwin.gem and /dev/null differ diff --git a/vendor/cache/nokogiri-1.18.10.gem b/vendor/cache/nokogiri-1.18.10.gem deleted file mode 100644 index ff8ff0d9..00000000 Binary files a/vendor/cache/nokogiri-1.18.10.gem and /dev/null differ diff --git a/vendor/cache/nokogiri-1.19.0-aarch64-linux-gnu.gem b/vendor/cache/nokogiri-1.19.0-aarch64-linux-gnu.gem new file mode 100644 index 00000000..e6446efb Binary files /dev/null and b/vendor/cache/nokogiri-1.19.0-aarch64-linux-gnu.gem differ diff --git a/vendor/cache/nokogiri-1.19.0-arm64-darwin.gem b/vendor/cache/nokogiri-1.19.0-arm64-darwin.gem new file mode 100644 index 00000000..1d115670 Binary files /dev/null and b/vendor/cache/nokogiri-1.19.0-arm64-darwin.gem differ diff --git a/vendor/cache/nokogiri-1.19.0-x86_64-linux-gnu.gem b/vendor/cache/nokogiri-1.19.0-x86_64-linux-gnu.gem new file mode 100644 index 00000000..29f506e6 Binary files /dev/null and b/vendor/cache/nokogiri-1.19.0-x86_64-linux-gnu.gem differ diff --git a/vendor/cache/omniauth-rails_csrf_protection-1.0.2.gem b/vendor/cache/omniauth-rails_csrf_protection-1.0.2.gem deleted file mode 100644 index 09b79f22..00000000 Binary files a/vendor/cache/omniauth-rails_csrf_protection-1.0.2.gem and /dev/null differ diff --git a/vendor/cache/omniauth-rails_csrf_protection-2.0.1.gem b/vendor/cache/omniauth-rails_csrf_protection-2.0.1.gem new file mode 100644 index 00000000..2217f9e0 Binary files /dev/null and b/vendor/cache/omniauth-rails_csrf_protection-2.0.1.gem differ diff --git a/vendor/cache/parallel-1.27.0.gem b/vendor/cache/parallel-1.27.0.gem deleted file mode 100644 index 1b86f818..00000000 Binary files a/vendor/cache/parallel-1.27.0.gem and /dev/null differ diff --git a/vendor/cache/parser-3.3.10.0.gem b/vendor/cache/parser-3.3.10.0.gem deleted file mode 100644 index b2573c3b..00000000 Binary files a/vendor/cache/parser-3.3.10.0.gem and /dev/null differ diff --git a/vendor/cache/prism-1.7.0.gem b/vendor/cache/prism-1.7.0.gem deleted file mode 100644 index 55afb6fe..00000000 Binary files a/vendor/cache/prism-1.7.0.gem and /dev/null differ diff --git a/vendor/cache/prism-1.8.0.gem b/vendor/cache/prism-1.8.0.gem new file mode 100644 index 00000000..6ef2eead Binary files /dev/null and b/vendor/cache/prism-1.8.0.gem differ diff --git a/vendor/cache/protocol-http-0.56.1.gem b/vendor/cache/protocol-http-0.56.1.gem deleted file mode 100644 index 87e96857..00000000 Binary files a/vendor/cache/protocol-http-0.56.1.gem and /dev/null differ diff --git a/vendor/cache/protocol-http-0.58.0.gem b/vendor/cache/protocol-http-0.58.0.gem new file mode 100644 index 00000000..1ed2ab7b Binary files /dev/null and b/vendor/cache/protocol-http-0.58.0.gem differ diff --git a/vendor/cache/protocol-http1-0.35.2.gem b/vendor/cache/protocol-http1-0.35.2.gem deleted file mode 100644 index a0859dda..00000000 Binary files a/vendor/cache/protocol-http1-0.35.2.gem and /dev/null differ diff --git a/vendor/cache/protocol-http1-0.36.0.gem b/vendor/cache/protocol-http1-0.36.0.gem new file mode 100644 index 00000000..43e1169d Binary files /dev/null and b/vendor/cache/protocol-http1-0.36.0.gem differ diff --git a/vendor/cache/protocol-http2-0.23.0.gem b/vendor/cache/protocol-http2-0.23.0.gem deleted file mode 100644 index 70e56add..00000000 Binary files a/vendor/cache/protocol-http2-0.23.0.gem and /dev/null differ diff --git a/vendor/cache/protocol-http2-0.24.0.gem b/vendor/cache/protocol-http2-0.24.0.gem new file mode 100644 index 00000000..f45bdcec Binary files /dev/null and b/vendor/cache/protocol-http2-0.24.0.gem differ diff --git a/vendor/cache/protocol-rack-0.19.0.gem b/vendor/cache/protocol-rack-0.19.0.gem deleted file mode 100644 index 15c0c0f8..00000000 Binary files a/vendor/cache/protocol-rack-0.19.0.gem and /dev/null differ diff --git a/vendor/cache/protocol-rack-0.21.0.gem b/vendor/cache/protocol-rack-0.21.0.gem new file mode 100644 index 00000000..ac067f13 Binary files /dev/null and b/vendor/cache/protocol-rack-0.21.0.gem differ diff --git a/vendor/cache/public_suffix-7.0.0.gem b/vendor/cache/public_suffix-7.0.0.gem deleted file mode 100644 index f733f902..00000000 Binary files a/vendor/cache/public_suffix-7.0.0.gem and /dev/null differ diff --git a/vendor/cache/public_suffix-7.0.2.gem b/vendor/cache/public_suffix-7.0.2.gem new file mode 100644 index 00000000..ceaccc07 Binary files /dev/null and b/vendor/cache/public_suffix-7.0.2.gem differ diff --git a/vendor/cache/rack-mini-profiler-3.3.1.gem b/vendor/cache/rack-mini-profiler-3.3.1.gem deleted file mode 100644 index a830b7ac..00000000 Binary files a/vendor/cache/rack-mini-profiler-3.3.1.gem and /dev/null differ diff --git a/vendor/cache/rack-mini-profiler-4.0.1.gem b/vendor/cache/rack-mini-profiler-4.0.1.gem new file mode 100644 index 00000000..2f190065 Binary files /dev/null and b/vendor/cache/rack-mini-profiler-4.0.1.gem differ diff --git a/vendor/cache/rails-8.1.1.gem b/vendor/cache/rails-8.1.2.gem similarity index 67% rename from vendor/cache/rails-8.1.1.gem rename to vendor/cache/rails-8.1.2.gem index 28dd3228..d33a1291 100644 Binary files a/vendor/cache/rails-8.1.1.gem and b/vendor/cache/rails-8.1.2.gem differ diff --git a/vendor/cache/railties-8.1.1.gem b/vendor/cache/railties-8.1.1.gem deleted file mode 100644 index 89c41cd9..00000000 Binary files a/vendor/cache/railties-8.1.1.gem and /dev/null differ diff --git a/vendor/cache/railties-8.1.2.gem b/vendor/cache/railties-8.1.2.gem new file mode 100644 index 00000000..68c59302 Binary files /dev/null and b/vendor/cache/railties-8.1.2.gem differ diff --git a/vendor/cache/rainbow-3.1.1.gem b/vendor/cache/rainbow-3.1.1.gem deleted file mode 100644 index 863181a2..00000000 Binary files a/vendor/cache/rainbow-3.1.1.gem and /dev/null differ diff --git a/vendor/cache/rbs-2.8.4.gem b/vendor/cache/rbs-2.8.4.gem deleted file mode 100644 index b5728708..00000000 Binary files a/vendor/cache/rbs-2.8.4.gem and /dev/null differ diff --git a/vendor/cache/rdoc-7.0.3.gem b/vendor/cache/rdoc-7.0.3.gem deleted file mode 100644 index 6527161c..00000000 Binary files a/vendor/cache/rdoc-7.0.3.gem and /dev/null differ diff --git a/vendor/cache/rdoc-7.1.0.gem b/vendor/cache/rdoc-7.1.0.gem new file mode 100644 index 00000000..75652312 Binary files /dev/null and b/vendor/cache/rdoc-7.1.0.gem differ diff --git a/vendor/cache/regexp_parser-2.11.3.gem b/vendor/cache/regexp_parser-2.11.3.gem deleted file mode 100644 index 60eb7aa7..00000000 Binary files a/vendor/cache/regexp_parser-2.11.3.gem and /dev/null differ diff --git a/vendor/cache/reverse_markdown-2.1.1.gem b/vendor/cache/reverse_markdown-2.1.1.gem deleted file mode 100644 index 688f4850..00000000 Binary files a/vendor/cache/reverse_markdown-2.1.1.gem and /dev/null differ diff --git a/vendor/cache/rspec-rails-7.1.1.gem b/vendor/cache/rspec-rails-7.1.1.gem deleted file mode 100644 index 807b133f..00000000 Binary files a/vendor/cache/rspec-rails-7.1.1.gem and /dev/null differ diff --git a/vendor/cache/rspec-rails-8.0.2.gem b/vendor/cache/rspec-rails-8.0.2.gem new file mode 100644 index 00000000..5c3ca66a Binary files /dev/null and b/vendor/cache/rspec-rails-8.0.2.gem differ diff --git a/vendor/cache/rubocop-1.82.1.gem b/vendor/cache/rubocop-1.82.1.gem deleted file mode 100644 index 5cce65e9..00000000 Binary files a/vendor/cache/rubocop-1.82.1.gem and /dev/null differ diff --git a/vendor/cache/rubocop-ast-1.48.0.gem b/vendor/cache/rubocop-ast-1.48.0.gem deleted file mode 100644 index 058aa796..00000000 Binary files a/vendor/cache/rubocop-ast-1.48.0.gem and /dev/null differ diff --git a/vendor/cache/ruby-progressbar-1.13.0.gem b/vendor/cache/ruby-progressbar-1.13.0.gem deleted file mode 100644 index c50b94b2..00000000 Binary files a/vendor/cache/ruby-progressbar-1.13.0.gem and /dev/null differ diff --git a/vendor/cache/ruby-vips-2.3.0.gem b/vendor/cache/ruby-vips-2.3.0.gem new file mode 100644 index 00000000..d93bdb41 Binary files /dev/null and b/vendor/cache/ruby-vips-2.3.0.gem differ diff --git a/vendor/cache/sanitize-6.1.3.gem b/vendor/cache/sanitize-6.1.3.gem deleted file mode 100644 index ea43ca81..00000000 Binary files a/vendor/cache/sanitize-6.1.3.gem and /dev/null differ diff --git a/vendor/cache/sanitize-7.0.0.gem b/vendor/cache/sanitize-7.0.0.gem new file mode 100644 index 00000000..a6bf6d3f Binary files /dev/null and b/vendor/cache/sanitize-7.0.0.gem differ diff --git a/vendor/cache/solargraph-0.50.0.gem b/vendor/cache/solargraph-0.50.0.gem deleted file mode 100644 index 8c8f42ee..00000000 Binary files a/vendor/cache/solargraph-0.50.0.gem and /dev/null differ diff --git a/vendor/cache/solargraph-rails-1.2.4.gem b/vendor/cache/solargraph-rails-1.2.4.gem deleted file mode 100644 index 2c00f851..00000000 Binary files a/vendor/cache/solargraph-rails-1.2.4.gem and /dev/null differ diff --git a/vendor/cache/thor-1.4.0.gem b/vendor/cache/thor-1.4.0.gem deleted file mode 100644 index f72f098c..00000000 Binary files a/vendor/cache/thor-1.4.0.gem and /dev/null differ diff --git a/vendor/cache/thor-1.5.0.gem b/vendor/cache/thor-1.5.0.gem new file mode 100644 index 00000000..43f63f6e Binary files /dev/null and b/vendor/cache/thor-1.5.0.gem differ diff --git a/vendor/cache/tilt-2.6.1.gem b/vendor/cache/tilt-2.6.1.gem deleted file mode 100644 index 1af34bfc..00000000 Binary files a/vendor/cache/tilt-2.6.1.gem and /dev/null differ diff --git a/vendor/cache/tilt-2.7.0.gem b/vendor/cache/tilt-2.7.0.gem new file mode 100644 index 00000000..eb5702ef Binary files /dev/null and b/vendor/cache/tilt-2.7.0.gem differ diff --git a/vendor/cache/turbo-rails-2.0.20.gem b/vendor/cache/turbo-rails-2.0.20.gem deleted file mode 100644 index bb227319..00000000 Binary files a/vendor/cache/turbo-rails-2.0.20.gem and /dev/null differ diff --git a/vendor/cache/turbo-rails-2.0.21.gem b/vendor/cache/turbo-rails-2.0.21.gem new file mode 100644 index 00000000..5f597428 Binary files /dev/null and b/vendor/cache/turbo-rails-2.0.21.gem differ diff --git a/vendor/cache/unicode-display_width-3.2.0.gem b/vendor/cache/unicode-display_width-3.2.0.gem deleted file mode 100644 index 37a7d7a0..00000000 Binary files a/vendor/cache/unicode-display_width-3.2.0.gem and /dev/null differ diff --git a/vendor/cache/unicode-emoji-4.2.0.gem b/vendor/cache/unicode-emoji-4.2.0.gem deleted file mode 100644 index 3ceb38a0..00000000 Binary files a/vendor/cache/unicode-emoji-4.2.0.gem and /dev/null differ diff --git a/vendor/cache/yard-0.9.38.gem b/vendor/cache/yard-0.9.38.gem deleted file mode 100644 index 67d5a7f6..00000000 Binary files a/vendor/cache/yard-0.9.38.gem and /dev/null differ diff --git a/vendor/gems/RocketAMF-1.0.0/RocketAMF.gemspec b/vendor/gems/RocketAMF-1.0.0/RocketAMF.gemspec index 2c4182f0..d285d2ff 100644 --- a/vendor/gems/RocketAMF-1.0.0/RocketAMF.gemspec +++ b/vendor/gems/RocketAMF-1.0.0/RocketAMF.gemspec @@ -14,7 +14,6 @@ Gem::Specification.new do |s| s.extensions = Dir[*["ext/**/extconf.rb"]] s.require_paths = ["lib"] - s.has_rdoc = true s.extra_rdoc_files = ['README.rdoc'] s.rdoc_options = ['--line-numbers', '--main', 'README.rdoc'] end \ No newline at end of file