Compare commits
No commits in common. "main" and "modeling-tests" have entirely different histories.
main
...
modeling-t
81 changed files with 545 additions and 2461 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,7 +5,6 @@ tmp/**/*
|
|||
.env
|
||||
.env.*
|
||||
/spec/examples.txt
|
||||
/.yardoc
|
||||
|
||||
/app/assets/builds/*
|
||||
!/app/assets/builds/.keep
|
||||
|
|
12
Gemfile
12
Gemfile
|
@ -66,10 +66,7 @@ gem "async-http", "~> 0.75.0", require: false
|
|||
gem "thread-local", "~> 1.1", require: false
|
||||
|
||||
# For debugging.
|
||||
group :development do
|
||||
gem 'debug', '~> 1.9.2'
|
||||
gem 'web-console', '~> 4.2'
|
||||
end
|
||||
gem 'web-console', '~> 4.2', group: :development
|
||||
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', '~> 1.16', require: false
|
||||
|
@ -87,13 +84,10 @@ gem "sentry-rails", "~> 5.12"
|
|||
gem "shell", "~> 0.8.1"
|
||||
|
||||
# For workspace autocomplete.
|
||||
group :development do
|
||||
gem "solargraph", "~> 0.50.0"
|
||||
gem "solargraph-rails", "~> 1.1"
|
||||
end
|
||||
gem "solargraph", "~> 0.50.0", group: :development
|
||||
gem "solargraph-rails", "~> 1.1", group: :development
|
||||
|
||||
# For automated tests.
|
||||
group :development, :test do
|
||||
gem "rspec-rails", "~> 7.0"
|
||||
gem "webmock", "~> 3.24", group: :test
|
||||
end
|
||||
|
|
13
Gemfile.lock
13
Gemfile.lock
|
@ -128,15 +128,9 @@ GEM
|
|||
fiber-annotation
|
||||
fiber-local (~> 1.1)
|
||||
json
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
csv (3.3.0)
|
||||
date (3.3.4)
|
||||
debug (1.9.2)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
devise (4.9.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
|
@ -188,7 +182,6 @@ GEM
|
|||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
hashdiff (1.1.2)
|
||||
hashie (5.0.0)
|
||||
http_accept_language (2.1.1)
|
||||
httparty (0.22.0)
|
||||
|
@ -503,10 +496,6 @@ GEM
|
|||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.24.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.8.2)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
|
@ -524,7 +513,6 @@ DEPENDENCIES
|
|||
async (~> 2.17)
|
||||
async-http (~> 0.75.0)
|
||||
bootsnap (~> 1.16)
|
||||
debug (~> 1.9.2)
|
||||
devise (~> 4.9, >= 4.9.2)
|
||||
devise-encryptable (~> 0.2.0)
|
||||
dotenv-rails (~> 2.8, >= 2.8.1)
|
||||
|
@ -561,7 +549,6 @@ DEPENDENCIES
|
|||
thread-local (~> 1.1)
|
||||
turbo-rails (~> 2.0)
|
||||
web-console (~> 4.2)
|
||||
webmock (~> 3.24)
|
||||
will_paginate (~> 4.0)
|
||||
|
||||
RUBY VERSION
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
class MagicMagnifier extends HTMLElement {
|
||||
connectedCallback() {
|
||||
setTimeout(() => this.#attachLens(), 0);
|
||||
this.addEventListener("mousemove", this.#onMouseMove);
|
||||
}
|
||||
|
||||
#attachLens() {
|
||||
const lens = document.createElement("magic-magnifier-lens");
|
||||
lens.inert = true;
|
||||
lens.useContent(this.children);
|
||||
this.appendChild(lens);
|
||||
}
|
||||
|
||||
#onMouseMove(e) {
|
||||
const lens = this.querySelector("magic-magnifier-lens");
|
||||
const rect = this.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
this.style.setProperty("--magic-magnifier-x", x + "px");
|
||||
this.style.setProperty("--magic-magnifier-y", y + "px");
|
||||
}
|
||||
}
|
||||
|
||||
class MagicMagnifierLens extends HTMLElement {
|
||||
useContent(contentNodes) {
|
||||
for (const contentNode of contentNodes) {
|
||||
this.appendChild(contentNode.cloneNode(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("magic-magnifier", MagicMagnifier);
|
||||
customElements.define("magic-magnifier-lens", MagicMagnifierLens);
|
|
@ -1,46 +0,0 @@
|
|||
class SupportOutfitViewer extends HTMLElement {
|
||||
#internals = this.attachInternals();
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener("mouseenter", this.#onMouseEnter, { capture: true });
|
||||
this.addEventListener("mouseleave", this.#onMouseLeave, { capture: true });
|
||||
this.addEventListener("click", this.#onClick);
|
||||
this.#internals.states.add("ready");
|
||||
}
|
||||
|
||||
// When a row is hovered, highlight its corresponding outfit viewer layer.
|
||||
#onMouseEnter(e) {
|
||||
if (!e.target.matches("tr")) return;
|
||||
|
||||
const id = e.target.querySelector("[data-field=id]").innerText;
|
||||
const layers = this.querySelectorAll(
|
||||
`outfit-viewer [data-asset-id="${CSS.escape(id)}"]`,
|
||||
);
|
||||
for (const layer of layers) {
|
||||
layer.setAttribute("highlighted", "");
|
||||
}
|
||||
}
|
||||
|
||||
// When a row is unhovered, unhighlight its corresponding outfit viewer layer.
|
||||
#onMouseLeave(e) {
|
||||
if (!e.target.matches("tr")) return;
|
||||
|
||||
const id = e.target.querySelector("[data-field=id]").innerText;
|
||||
const layers = this.querySelectorAll(
|
||||
`outfit-viewer [data-asset-id="${CSS.escape(id)}"]`,
|
||||
);
|
||||
for (const layer of layers) {
|
||||
layer.removeAttribute("highlighted");
|
||||
}
|
||||
}
|
||||
|
||||
// When clicking a row, redirect the click to the first link.
|
||||
#onClick(e) {
|
||||
const row = e.target.closest("tr");
|
||||
if (row == null) return;
|
||||
|
||||
row.querySelector("[data-field=links] a").click();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("support-outfit-viewer", SupportOutfitViewer);
|
|
@ -2,3 +2,54 @@
|
|||
width: 300px
|
||||
height: 300px
|
||||
margin: 0 auto
|
||||
|
||||
.alt-style-form
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1em
|
||||
align-items: flex-start
|
||||
|
||||
fieldset
|
||||
width: 100%
|
||||
display: grid
|
||||
grid-template-columns: auto 1fr
|
||||
align-items: center
|
||||
gap: 1em
|
||||
|
||||
> *:nth-child(2n)
|
||||
width: 40rch
|
||||
max-width: 100%
|
||||
box-sizing: border-box
|
||||
|
||||
input[type=url]
|
||||
font-size: .85em
|
||||
|
||||
label
|
||||
font-weight: bold
|
||||
|
||||
.thumbnail-field
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: .25em
|
||||
|
||||
img
|
||||
width: 40px
|
||||
height: 40px
|
||||
|
||||
input
|
||||
flex: 1 0 20ch
|
||||
|
||||
.field_with_errors
|
||||
display: contents
|
||||
|
||||
.actions
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1em
|
||||
|
||||
label
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: .25em
|
||||
font-size: .85em
|
||||
font-style: italic
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
magic-magnifier
|
||||
position: relative
|
||||
|
||||
// Only show the lens when we are hovering, and the magnifier's X and Y
|
||||
// coordinates are set. (This ensures the component is running, and has
|
||||
// received a mousemove event, instead of defaulting to (0, 0).)
|
||||
magic-magnifier-lens
|
||||
display: none
|
||||
|
||||
&:hover
|
||||
@container style(--magic-magnifier-x) and style(--magic-magnifier-y)
|
||||
magic-magnifier-lens
|
||||
display: block
|
||||
|
||||
magic-magnifier-lens
|
||||
width: var(--magic-magnifier-lens-width, 100px)
|
||||
height: var(--magic-magnifier-lens-height, 100px)
|
||||
overflow: hidden
|
||||
border-radius: 100%
|
||||
|
||||
background: white
|
||||
border: 2px solid black
|
||||
box-shadow: 3px 3px 3px rgba(0, 0, 0, .5)
|
||||
|
||||
position: absolute
|
||||
left: var(--magic-magnifier-x, 0px)
|
||||
top: var(--magic-magnifier-y, 0px)
|
||||
|
||||
> *
|
||||
// Translations are applied in the opposite of the order they're specified.
|
||||
// So, here's what we're doing:
|
||||
//
|
||||
// 1. Translate the content left by --magic-magnifier-x and up by
|
||||
// --magic-magnifier-y, to align the target location with the lens's
|
||||
// top-right corner.
|
||||
// 2. Zoom in by --magic-magnifier-scale.
|
||||
// 3. Translate the content right by half of --magic-magnifier-lens-width,
|
||||
// and down by half of --magic-magnifier-lens-height, to align the
|
||||
// target location with the lens's center.
|
||||
//
|
||||
// Note that it *is* possible to specify transforms relative to the center,
|
||||
// rather than the top-left corner—this is in fact the default!—but that
|
||||
// gets confusing fast with scale in play. I think this is easier to reason
|
||||
// about with the top-left corner in terms of math, and center it after the
|
||||
// fact.
|
||||
transform: translateX(calc(var(--magic-magnifier-lens-width, 100px) / 2)) translateY(calc(var(--magic-magnifier-lens-height, 100px) / 2)) scale(var(--magic-magnifier-scale, 2)) translateX(calc(-1 * var(--magic-magnifier-x, 0px))) translateY(calc(-1 * var(--magic-magnifier-y, 0px)))
|
||||
transform-origin: left top
|
|
@ -108,18 +108,3 @@ outfit-viewer
|
|||
|
||||
&:has(outfit-layer:state(loading))
|
||||
+outfit-viewer-loading
|
||||
|
||||
// If a layer has the `[highlighted]` attribute, it's brought to the front,
|
||||
// and other layers are grayed out and blurred. We use this in the support
|
||||
// outfit viewer, when you hover over a layer.
|
||||
&:has(outfit-layer[highlighted])
|
||||
outfit-layer[highlighted]
|
||||
z-index: 999
|
||||
|
||||
// Filter everything behind the bottom-most highlighted layer, using a
|
||||
// backdrop filter. This gives us the best visual consistency by applying
|
||||
// effects to the entire backdrop, instead of each layer and then
|
||||
// re-compositing them.
|
||||
backdrop-filter: grayscale(1) brightness(2) blur(1px)
|
||||
& ~ outfit-layer[highlighted]
|
||||
backdrop-filter: none
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
@import "../partials/clean/constants"
|
||||
|
||||
.support-form
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1em
|
||||
align-items: flex-start
|
||||
|
||||
.fields
|
||||
list-style-type: none
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: .75em
|
||||
width: 100%
|
||||
|
||||
> li
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: .25em
|
||||
max-width: 60ch
|
||||
|
||||
> label, > .field_with_errors label
|
||||
display: block
|
||||
font-weight: bold
|
||||
|
||||
.field_with_errors
|
||||
> label
|
||||
color: $error-color
|
||||
|
||||
input[type=text], input[type=url]
|
||||
border-color: $error-border-color
|
||||
color: $error-color
|
||||
|
||||
&[data-type=radio]
|
||||
ul
|
||||
list-style-type: none
|
||||
|
||||
&[data-type=radio-grid] // Set the `--num-columns` property to configure!
|
||||
max-width: none
|
||||
|
||||
ul
|
||||
list-style-type: none
|
||||
display: grid
|
||||
grid-template-columns: repeat(var(--num-columns, 1), 1fr)
|
||||
gap: .25em
|
||||
|
||||
li
|
||||
display: flex
|
||||
align-items: stretch // Give the bubbles equal heights!
|
||||
|
||||
label
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: .5em
|
||||
padding: .5em 1em
|
||||
border: 1px solid $soft-border-color
|
||||
border-radius: 1em
|
||||
flex: 1 1 auto
|
||||
|
||||
input
|
||||
margin: 0
|
||||
|
||||
&:has(:checked)
|
||||
background: $module-bg-color
|
||||
border-color: $module-border-color
|
||||
|
||||
input[type=text], input[type=url]
|
||||
width: 100%
|
||||
min-width: 10ch
|
||||
box-sizing: border-box
|
||||
|
||||
.thumbnail-input
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: .25em
|
||||
|
||||
img
|
||||
width: 40px
|
||||
height: 40px
|
||||
|
||||
fieldset
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: .25em
|
||||
|
||||
legend
|
||||
font-weight: bold
|
||||
|
||||
.field_with_errors
|
||||
display: contents
|
||||
|
||||
.actions
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1em
|
||||
|
||||
.go-to-next
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: .25em
|
||||
font-size: .85em
|
||||
font-style: italic
|
|
@ -1,7 +1,6 @@
|
|||
@import "clean/mixins"
|
||||
|
||||
=context-button
|
||||
+awesome-button
|
||||
+awesome-button-color(#aaaaaa)
|
||||
+opacity(0.9)
|
||||
font-size: 80%
|
||||
|
||||
|
|
|
@ -67,21 +67,14 @@
|
|||
background: #FEEBC8
|
||||
color: #7B341E
|
||||
|
||||
.support-form
|
||||
grid-area: support
|
||||
font-size: 85%
|
||||
text-align: left
|
||||
|
||||
.user-lists-info
|
||||
grid-area: lists
|
||||
font-size: 85%
|
||||
text-align: left
|
||||
|
||||
display: flex
|
||||
gap: 1em
|
||||
|
||||
a::after
|
||||
content: " ›"
|
||||
.user-lists-form-opener
|
||||
&::after
|
||||
content: " ›"
|
||||
|
||||
.user-lists-form
|
||||
background: $background-color
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
support-outfit-viewer
|
||||
margin-block: 1em
|
||||
@import "../partials/clean/constants"
|
||||
|
||||
.fields li[data-type=radio-grid]
|
||||
--num-columns: 3
|
||||
outfit-viewer
|
||||
margin: 0 auto
|
||||
|
||||
.reference-link
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: .5em
|
||||
padding-inline: .5em
|
||||
.pose-options
|
||||
list-style-type: none
|
||||
display: grid
|
||||
grid-template-columns: 1fr 1fr 1fr
|
||||
gap: .25em
|
||||
|
||||
img
|
||||
height: 2em
|
||||
width: auto
|
||||
label
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: .5em
|
||||
padding: .5em 1em
|
||||
border: 1px solid $soft-border-color
|
||||
border-radius: 1em
|
||||
|
||||
input
|
||||
margin: 0
|
||||
|
||||
&:has(:checked)
|
||||
background: $module-bg-color
|
||||
border-color: $module-border-color
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
@import "../partials/clean/constants"
|
||||
@import "../partials/context_button"
|
||||
|
||||
support-outfit-viewer
|
||||
display: flex
|
||||
gap: 2em
|
||||
flex-wrap: wrap
|
||||
justify-content: center
|
||||
|
||||
outfit-viewer
|
||||
flex: 0 0 auto
|
||||
border: 1px solid $module-border-color
|
||||
border-radius: 1em
|
||||
|
||||
.outfit-viewer-controls
|
||||
margin-block: .5em
|
||||
isolation: isolate // Avoid z-index weirdness with our buttons vs the lens
|
||||
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
gap: .5em
|
||||
|
||||
font-size: .85em
|
||||
|
||||
fieldset
|
||||
display: contents
|
||||
|
||||
legend
|
||||
font-weight: bold
|
||||
&::after
|
||||
content: ":"
|
||||
|
||||
label
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: .25em
|
||||
|
||||
input[type=radio]
|
||||
margin: 0
|
||||
|
||||
[type=submit]
|
||||
+context-button
|
||||
|
||||
> table
|
||||
flex: 0 0 auto
|
||||
border-collapse: collapse
|
||||
table-layout: fixed
|
||||
border-radius: .5em
|
||||
|
||||
th, td
|
||||
border: 1px solid $module-border-color
|
||||
font-size: .85em
|
||||
padding: .25em .5em
|
||||
text-align: left
|
||||
|
||||
> tbody
|
||||
[data-field=links]
|
||||
ul
|
||||
list-style-type: none
|
||||
display: flex
|
||||
gap: .5em
|
||||
|
||||
// Once the component is ready, add some hints about potential interactions.
|
||||
&:state(ready)
|
||||
> table
|
||||
> tbody > tr
|
||||
cursor: zoom-in
|
||||
&:hover
|
||||
background: $module-bg-color
|
||||
|
||||
magic-magnifier
|
||||
--magic-magnifier-lens-width: 100px
|
||||
--magic-magnifier-lens-height: 100px
|
||||
--magic-magnifier-scale: 2.5
|
||||
|
||||
magic-magnifier-lens
|
||||
z-index: 2 // Be above things by default, but not by much!
|
|
@ -15,7 +15,9 @@ class AltStylesController < ApplicationController
|
|||
@color = find_color
|
||||
@species = find_species
|
||||
|
||||
@alt_styles = @all_alt_styles.includes(:swf_assets)
|
||||
@alt_styles = @all_alt_styles.includes(:swf_assets).
|
||||
by_creation_date.order(:color_id, :species_id, :series_name).
|
||||
paginate(page: params[:page], per_page: 30)
|
||||
@alt_styles.where!(series_name: @series_name) if @series_name.present?
|
||||
@alt_styles.merge!(@color.alt_styles) if @color
|
||||
@alt_styles.merge!(@species.alt_styles) if @species
|
||||
|
@ -25,16 +27,9 @@ class AltStylesController < ApplicationController
|
|||
SwfAsset.preload_manifests @alt_styles.map(&:swf_assets).flatten
|
||||
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
@alt_styles = @alt_styles.
|
||||
by_creation_date.order(:color_id, :species_id, :series_name).
|
||||
paginate(page: params[:page], per_page: 30)
|
||||
render
|
||||
}
|
||||
format.html { render }
|
||||
format.json {
|
||||
@alt_styles = @alt_styles.includes(swf_assets: [:zone]).
|
||||
sort_by(&:full_name)
|
||||
render json: @alt_styles.as_json(
|
||||
render json: @alt_styles.includes(swf_assets: [:zone]).as_json(
|
||||
only: [:id, :species_id, :color_id, :body_id, :series_name,
|
||||
:adjective_name, :thumbnail_url],
|
||||
include: {
|
||||
|
|
|
@ -4,7 +4,7 @@ require 'async/container'
|
|||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery
|
||||
|
||||
helper_method :current_user, :support_staff?, :user_signed_in?
|
||||
helper_method :current_user, :user_signed_in?
|
||||
|
||||
before_action :set_locale
|
||||
|
||||
|
@ -111,12 +111,10 @@ class ApplicationController < ActionController::Base
|
|||
return_to || root_path
|
||||
end
|
||||
|
||||
def support_staff?
|
||||
current_user&.support_staff?
|
||||
end
|
||||
|
||||
def support_staff_only
|
||||
raise AccessDenied, "Support staff only" unless support_staff?
|
||||
unless current_user&.support_staff?
|
||||
raise AccessDenied, "Support staff only"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
class ItemsController < ApplicationController
|
||||
before_action :set_query
|
||||
before_action :support_staff_only, except: [:index, :show, :sources]
|
||||
rescue_from Item::Search::Error, :with => :search_error
|
||||
|
||||
def index
|
||||
|
@ -113,21 +112,6 @@ class ItemsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@item = Item.find params[:id]
|
||||
render layout: "application"
|
||||
end
|
||||
|
||||
def update
|
||||
@item = Item.find params[:id]
|
||||
if @item.update(item_params)
|
||||
flash[:notice] = "\"#{@item.name}\" successfully saved!"
|
||||
redirect_to @item
|
||||
else
|
||||
render action: "edit", layout: "application", status: :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
def sources
|
||||
# Load all the items, then group them by source.
|
||||
item_ids = params[:ids].split(",")
|
||||
|
@ -180,15 +164,6 @@ class ItemsController < ApplicationController
|
|||
|
||||
protected
|
||||
|
||||
def item_params
|
||||
params.require(:item).permit(
|
||||
:name, :thumbnail_url, :description, :modeling_status_hint,
|
||||
:is_manually_nc, :explicitly_body_specific,
|
||||
).tap do |p|
|
||||
p[:modeling_status_hint] = nil if p[:modeling_status_hint] == ""
|
||||
end
|
||||
end
|
||||
|
||||
def assign_closeted!(items)
|
||||
current_user.assign_closeted_to_items!(items) if user_signed_in?
|
||||
end
|
||||
|
|
|
@ -50,7 +50,10 @@ class OutfitsController < ApplicationController
|
|||
@colors = Color.alphabetical
|
||||
@species = Species.alphabetical
|
||||
|
||||
newest_items = Item.newest.limit(18)
|
||||
newest_items = Item.newest.
|
||||
select(:id, :name, :updated_at, :thumbnail_url, :rarity_index,
|
||||
:is_manually_nc, :cached_compatible_body_ids)
|
||||
.limit(18)
|
||||
@newest_modeled_items, @newest_unmodeled_items =
|
||||
newest_items.partition(&:predicted_fully_modeled?)
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class PetStatesController < ApplicationController
|
||||
before_action :support_staff_only
|
||||
before_action :find_pet_state
|
||||
before_action :preload_assets
|
||||
before_action :support_staff_only
|
||||
|
||||
def edit
|
||||
end
|
||||
|
@ -9,7 +8,7 @@ class PetStatesController < ApplicationController
|
|||
def update
|
||||
if @pet_state.update(pet_state_params)
|
||||
flash[:notice] = "Pet appearance \##{@pet_state.id} successfully saved!"
|
||||
redirect_to destination_after_save
|
||||
redirect_to @pet_type
|
||||
else
|
||||
render action: :edit, status: :bad_request
|
||||
end
|
||||
|
@ -18,39 +17,11 @@ class PetStatesController < ApplicationController
|
|||
protected
|
||||
|
||||
def find_pet_state
|
||||
@pet_type = PetType.find_by_param!(params[:pet_type_name])
|
||||
@pet_type = PetType.matching_name_param(params[:pet_type_name]).first!
|
||||
@pet_state = @pet_type.pet_states.find(params[:id])
|
||||
@reference_pet_type = @pet_type.reference
|
||||
end
|
||||
|
||||
def preload_assets
|
||||
SwfAsset.preload_manifests @pet_state.swf_assets
|
||||
end
|
||||
|
||||
def pet_state_params
|
||||
params.require(:pet_state).permit(:pose, :glitched)
|
||||
end
|
||||
|
||||
def destination_after_save
|
||||
if params[:next] == "unlabeled-appearance"
|
||||
next_unlabeled_appearance_path
|
||||
else
|
||||
@pet_type
|
||||
end
|
||||
end
|
||||
|
||||
def next_unlabeled_appearance_path
|
||||
unlabeled_appearance =
|
||||
PetState.next_unlabeled_appearance(after_id: params[:after])
|
||||
|
||||
if unlabeled_appearance
|
||||
edit_pet_type_pet_state_path(
|
||||
unlabeled_appearance.pet_type,
|
||||
unlabeled_appearance,
|
||||
next: "unlabeled-appearance"
|
||||
)
|
||||
else
|
||||
@pet_type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,16 +35,6 @@ class PetTypesController < ApplicationController
|
|||
if @selected_species && @selected_color && @pet_types.size == 1
|
||||
redirect_to @pet_types.first
|
||||
end
|
||||
|
||||
if support_staff?
|
||||
@counts = {
|
||||
total: PetState.count,
|
||||
glitched: PetState.glitched.count,
|
||||
needs_labeling: PetState.needs_labeling.count,
|
||||
usable: PetState.usable.count,
|
||||
}
|
||||
@unlabeled_appearance = PetState.next_unlabeled_appearance
|
||||
end
|
||||
}
|
||||
|
||||
format.json {
|
||||
|
@ -80,7 +70,9 @@ class PetTypesController < ApplicationController
|
|||
color_id: params[:color_id],
|
||||
)
|
||||
elsif params[:name]
|
||||
PetType.find_by_param!(params[:name])
|
||||
color_name, _, species_name = params[:name].rpartition("-")
|
||||
raise ActiveRecord::RecordNotFound if species_name.blank?
|
||||
PetType.matching_name(color_name, species_name).first!
|
||||
else
|
||||
raise "expected params: species_id and color_id, or name"
|
||||
end
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
class PetsController < ApplicationController
|
||||
rescue_from Neopets::CustomPets::PetNotFound, with: :pet_not_found
|
||||
rescue_from Neopets::CustomPets::DownloadError, with: :pet_download_error
|
||||
rescue_from Pet::ModelingDisabled, with: :modeling_disabled
|
||||
rescue_from Pet::UnexpectedDataFormat, with: :unexpected_data_format
|
||||
|
||||
def load
|
||||
# Uncomment this to temporarily disable modeling for most users.
|
||||
# return modeling_disabled unless user_signed_in? && current_user.admin?
|
||||
|
||||
raise Neopets::CustomPets::PetNotFound unless params[:name]
|
||||
@pet = Pet.load(params[:name])
|
||||
points = contribute(current_user, @pet)
|
||||
|
|
|
@ -127,6 +127,10 @@ module ApplicationHelper
|
|||
!@hide_home_link
|
||||
end
|
||||
|
||||
def support_staff?
|
||||
user_signed_in? && current_user.support_staff?
|
||||
end
|
||||
|
||||
def impress_2020_meta_tags
|
||||
origin = Rails.configuration.impress_2020_origin
|
||||
support_secret = Rails.application.credentials.dig(
|
||||
|
@ -213,10 +217,6 @@ module ApplicationHelper
|
|||
@hide_title_header = true
|
||||
end
|
||||
|
||||
def hide_after(last_day, &block)
|
||||
yield if Date.today <= last_day
|
||||
end
|
||||
|
||||
def use_responsive_design
|
||||
@use_responsive_design = true
|
||||
add_body_class "use-responsive-design"
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
module OutfitsHelper
|
||||
LAST_DAY_OF_ANNOUNCEMENT = Date.parse("2024-10-21")
|
||||
def show_announcement?
|
||||
Date.today <= LAST_DAY_OF_ANNOUNCEMENT
|
||||
end
|
||||
|
||||
def destination_tag(value)
|
||||
hidden_field_tag 'destination', value, :id => nil
|
||||
end
|
||||
|
@ -65,27 +70,11 @@ module OutfitsHelper
|
|||
text_field_tag 'name', nil, options
|
||||
end
|
||||
|
||||
def outfit_viewer(...)
|
||||
render partial: "outfit_viewer",
|
||||
locals: parse_outfit_viewer_options(...)
|
||||
end
|
||||
|
||||
def support_outfit_viewer(...)
|
||||
render partial: "support_outfit_viewer",
|
||||
locals: parse_outfit_viewer_options(...)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_outfit_viewer_options(
|
||||
outfit=nil, pet_state: nil, preferred_image_format: :png, **html_options
|
||||
)
|
||||
def outfit_viewer(outfit=nil, pet_state: nil, **html_options)
|
||||
outfit = Outfit.new(pet_state:) if outfit.nil? && pet_state.present?
|
||||
raise "outfit_viewer must have outfit or pet state" if outfit.nil?
|
||||
|
||||
if outfit.nil?
|
||||
raise ArgumentError, "outfit viewer must have outfit or pet state"
|
||||
end
|
||||
|
||||
{outfit:, preferred_image_format:, html_options:}
|
||||
render partial: "outfit_viewer", locals: {outfit:, html_options:}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
module SupportFormHelper
|
||||
class SupportFormBuilder < ActionView::Helpers::FormBuilder
|
||||
attr_reader :template
|
||||
delegate :capture, :check_box_tag, :concat, :content_tag,
|
||||
:hidden_field_tag, :params, :render,
|
||||
to: :template, private: true
|
||||
|
||||
def errors
|
||||
render partial: "application/support_form/errors", locals: {form: self}
|
||||
end
|
||||
|
||||
def fields(&block)
|
||||
content_tag(:ul, class: "fields", &block)
|
||||
end
|
||||
|
||||
def field(**options, &block)
|
||||
content_tag(:li, **options, &block)
|
||||
end
|
||||
|
||||
def radio_fieldset(legend, **options, &block)
|
||||
render partial: "application/support_form/radio_fieldset",
|
||||
locals: {form: self, legend:, options:, content: capture(&block)}
|
||||
end
|
||||
|
||||
def radio_field(**options, &block)
|
||||
content_tag(:li) do
|
||||
content_tag(:label, **options, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def radio_grid_fieldset(*args, &block)
|
||||
radio_fieldset(*args, "data-type": "radio-grid", &block)
|
||||
end
|
||||
|
||||
def thumbnail_input(method)
|
||||
render partial: "application/support_form/thumbnail_input",
|
||||
locals: {form: self, method:}
|
||||
end
|
||||
|
||||
def actions(&block)
|
||||
content_tag(:section, class: "actions", &block)
|
||||
end
|
||||
|
||||
def go_to_next_field(after: nil, **options, &block)
|
||||
content_tag(:label, class: "go-to-next", **options) do
|
||||
concat hidden_field_tag(:after, after) if after
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_next_check_box(value)
|
||||
check_box_tag "next", value, checked: params[:next] == value
|
||||
end
|
||||
end
|
||||
|
||||
def support_form_with(**options, &block)
|
||||
form_with(
|
||||
builder: SupportFormBuilder,
|
||||
**options,
|
||||
class: ["support-form", options[:class]],
|
||||
&block
|
||||
)
|
||||
end
|
||||
end
|
|
@ -37,7 +37,7 @@ class AltStyle < ApplicationRecord
|
|||
# `fits:<New?>-faerie-draik` intentionally will not work, and the canonical
|
||||
# filter name will be `fits:alt-style-IDNUMBER`, instead.
|
||||
def series_name
|
||||
real_series_name || AltStyle.placeholder_name
|
||||
real_series_name || "<New?>"
|
||||
end
|
||||
|
||||
def real_series_name=(new_series_name)
|
||||
|
@ -62,10 +62,11 @@ class AltStyle < ApplicationRecord
|
|||
"#{series_name} #{name}"
|
||||
end
|
||||
|
||||
EMPTY_IMAGE_URL = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
|
||||
def preview_image_url
|
||||
# Use the image URL for the first asset. Or, fall back to an empty image.
|
||||
swf_assets.first&.image_url || EMPTY_IMAGE_URL
|
||||
swf_asset = swf_assets.first
|
||||
return nil if swf_asset.nil?
|
||||
|
||||
swf_asset.image_url
|
||||
end
|
||||
|
||||
# Given a list of items, return how they look on this alt style.
|
||||
|
@ -73,6 +74,15 @@ class AltStyle < ApplicationRecord
|
|||
Item.appearances_for(items, self, ...)
|
||||
end
|
||||
|
||||
def biology=(biology)
|
||||
# TODO: This is very similar to what `PetState` does, but like… much much
|
||||
# more compact? Idk if I'm missing something, or if I was just that much
|
||||
# more clueless back when I wrote it, lol 😅
|
||||
self.swf_assets = biology.values.map do |asset_data|
|
||||
SwfAsset.from_biology_data(self.body_id, asset_data)
|
||||
end
|
||||
end
|
||||
|
||||
# At time of writing, most batches of Alt Styles thumbnails used a simple
|
||||
# pattern for the item thumbnail URL, but that's not always the case anymore.
|
||||
# For now, let's keep using this format as the default value when creating a
|
||||
|
@ -93,14 +103,6 @@ class AltStyle < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def real_thumbnail_url?
|
||||
thumbnail_url != DEFAULT_THUMBNAIL_URL
|
||||
end
|
||||
|
||||
def self.placeholder_name
|
||||
"<New?>"
|
||||
end
|
||||
|
||||
# For convenience in the console!
|
||||
def self.find_by_name(color_name, species_name)
|
||||
color = Color.find_by_name(color_name)
|
||||
|
|
|
@ -21,10 +21,6 @@ class Color < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def to_param
|
||||
name? ? human_name : id.to_s
|
||||
end
|
||||
|
||||
def example_pet_type(preferred_species: nil)
|
||||
preferred_species ||= Species.first
|
||||
pet_types.order([Arel.sql("species_id = ? DESC"), preferred_species.id],
|
||||
|
@ -40,8 +36,4 @@ class Color < ApplicationRecord
|
|||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def self.param_to_id(param)
|
||||
param.match?(/\A\d+\Z/) ? param.to_i : find_by_name!(param).id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
require "async"
|
||||
require "async/barrier"
|
||||
|
||||
class Item < ApplicationRecord
|
||||
include PrettyParam
|
||||
include Item::Dyeworks
|
||||
|
@ -20,16 +23,8 @@ class Item < ApplicationRecord
|
|||
has_many :dyeworks_variants, class_name: "Item",
|
||||
inverse_of: :dyeworks_base_item
|
||||
|
||||
# We require a name field. A number of other fields must be *specified*: they
|
||||
# can't be nil, to help ensure we aren't forgetting any fields when importing
|
||||
# items. But sometimes they happen to be blank (e.g. when TNT leaves an item
|
||||
# description empty, oops), in which case we want to accept that reality!
|
||||
validates_presence_of :name
|
||||
validates :description, :thumbnail_url, :rarity, :price, :zones_restrict,
|
||||
exclusion: {in: [nil], message: "must be specified"}
|
||||
|
||||
after_save :update_cached_fields,
|
||||
if: :modeling_status_hint_previously_changed?
|
||||
validates_presence_of :name, :description, :thumbnail_url, :rarity, :price,
|
||||
:zones_restrict
|
||||
|
||||
attr_writer :current_body_id, :owned, :wanted
|
||||
|
||||
|
@ -70,12 +65,6 @@ class Item < ApplicationRecord
|
|||
where('description NOT LIKE ?',
|
||||
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
|
||||
}
|
||||
scope :is_modeled, -> {
|
||||
where(cached_predicted_fully_modeled: true)
|
||||
}
|
||||
scope :is_not_modeled, -> {
|
||||
where(cached_predicted_fully_modeled: false)
|
||||
}
|
||||
scope :occupies, ->(zone_label) {
|
||||
Zone.matching_label(zone_label).
|
||||
map { |z| occupies_zone_id(z.id) }.reduce(none, &:or)
|
||||
|
@ -274,19 +263,8 @@ class Item < ApplicationRecord
|
|||
end
|
||||
|
||||
def update_cached_fields
|
||||
# First, clear out some cached instance variables we use for performance,
|
||||
# to ensure we recompute the latest values.
|
||||
@predicted_body_ids = nil
|
||||
@predicted_missing_body_ids = nil
|
||||
|
||||
# We also need to reload our associations, so they include any new records.
|
||||
swf_assets.reload
|
||||
|
||||
# Finally, compute and save our cached fields.
|
||||
self.cached_occupied_zone_ids = occupied_zone_ids
|
||||
self.cached_compatible_body_ids = compatible_body_ids(use_cached: false)
|
||||
self.cached_predicted_fully_modeled =
|
||||
predicted_fully_modeled?(use_cached: false)
|
||||
self.save!
|
||||
end
|
||||
|
||||
|
@ -300,16 +278,8 @@ class Item < ApplicationRecord
|
|||
write_attribute('species_support_ids', replacement)
|
||||
end
|
||||
|
||||
def modeling_hinted_done?
|
||||
modeling_status_hint == "done" || modeling_status_hint == "glitchy"
|
||||
end
|
||||
|
||||
def predicted_body_ids
|
||||
@predicted_body_ids ||= if modeling_hinted_done?
|
||||
# If we've manually set this item to no longer report as needing modeling,
|
||||
# predict that the current bodies are all of the compatible bodies.
|
||||
compatible_body_ids
|
||||
elsif compatible_body_ids.include?(0)
|
||||
@predicted_body_ids ||= if compatible_body_ids.include?(0)
|
||||
# Oh, look, it's already known to fit everybody! Sweet. We're done. (This
|
||||
# isn't folded into the case below, in case this item somehow got a
|
||||
# body-specific and non-body-specific asset. In all the cases I've seen
|
||||
|
@ -321,11 +291,6 @@ class Item < ApplicationRecord
|
|||
# This might just be a species-specific item. Let's be conservative in
|
||||
# our prediction, though we'll revise it if we see another body ID.
|
||||
compatible_body_ids
|
||||
elsif compatible_body_ids.size == 0
|
||||
# If somehow we have this item, but not any modeling data for it (weird!),
|
||||
# consider it to fit all standard pet types until shown otherwise.
|
||||
PetType.basic.released_before(released_at_estimate).
|
||||
distinct.pluck(:body_id).sort
|
||||
else
|
||||
# First, find our compatible pet types, then pair each body ID with its
|
||||
# color. (As an optimization, we omit standard colors, other than the
|
||||
|
@ -359,17 +324,10 @@ class Item < ApplicationRecord
|
|||
compatible_color_ids_by_body_id.values.
|
||||
any? { |v| v.include?("basic") && (v & modelable_color_ids).empty? }
|
||||
|
||||
# Filter to pet types that match the colors that seem compatible.
|
||||
# Get all body IDs for the colors we decided are modelable.
|
||||
predicted_pet_types =
|
||||
(basic_is_modelable ? PetType.basic : PetType.none).
|
||||
or(PetType.where(color_id: modelable_color_ids))
|
||||
|
||||
# Only include species that were released when this item was. If we don't
|
||||
# know our creation date (we don't have it for some old records), assume
|
||||
# it's pretty old.
|
||||
predicted_pet_types.merge! PetType.released_before(released_at_estimate)
|
||||
|
||||
# Get all body IDs for the pet types we decided are modelable.
|
||||
predicted_pet_types.distinct.pluck(:body_id).sort
|
||||
end
|
||||
end
|
||||
|
@ -421,8 +379,7 @@ class Item < ApplicationRecord
|
|||
body_ids_by_species_by_color
|
||||
end
|
||||
|
||||
def predicted_fully_modeled?(use_cached: true)
|
||||
return cached_predicted_fully_modeled? if use_cached
|
||||
def predicted_fully_modeled?
|
||||
predicted_missing_body_ids.empty?
|
||||
end
|
||||
|
||||
|
@ -430,12 +387,6 @@ class Item < ApplicationRecord
|
|||
compatible_body_ids.size.to_f / predicted_body_ids.size
|
||||
end
|
||||
|
||||
# We estimate the item's release time as either when we first saw it, or 2010
|
||||
# if it's so old that we don't have a record.
|
||||
def released_at_estimate
|
||||
created_at || Time.new(2010)
|
||||
end
|
||||
|
||||
def as_json(options={})
|
||||
super({
|
||||
only: [:id, :name, :description, :thumbnail_url, :rarity_index],
|
||||
|
@ -668,10 +619,21 @@ class Item < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.preload_nc_trade_values(items)
|
||||
DTIRequests.load_many(max_at_once: 10) do |task|
|
||||
# Only allow 10 trade values to be loaded at a time.
|
||||
barrier = Async::Barrier.new
|
||||
semaphore = Async::Semaphore.new(10, parent: barrier)
|
||||
|
||||
Sync do
|
||||
# Load all the trade values in concurrent async tasks. (The
|
||||
# `nc_trade_value` caches the value in the Item object.)
|
||||
items.each { |item| task.async { item.nc_trade_value } }
|
||||
items.each do |item|
|
||||
semaphore.async { item.nc_trade_value }
|
||||
end
|
||||
|
||||
# Wait until all tasks are done.
|
||||
barrier.wait
|
||||
ensure
|
||||
barrier.stop # If something goes wrong, clean up all tasks.
|
||||
end
|
||||
|
||||
items
|
||||
|
|
|
@ -132,8 +132,6 @@ class Item
|
|||
is_positive ? Filter.is_np : Filter.is_not_np
|
||||
when 'pb'
|
||||
is_positive ? Filter.is_pb : Filter.is_not_pb
|
||||
when 'modeled'
|
||||
is_positive ? Filter.is_modeled : Filter.is_not_modeled
|
||||
else
|
||||
raise_search_error "not_found.label", label: "is:#{value}"
|
||||
end
|
||||
|
@ -348,14 +346,6 @@ class Item
|
|||
self.new Item.is_not_pb, '-is:pb'
|
||||
end
|
||||
|
||||
def self.is_modeled
|
||||
self.new Item.is_modeled, 'is:modeled'
|
||||
end
|
||||
|
||||
def self.is_not_modeled
|
||||
self.new Item.is_not_modeled, '-is:modeled'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Add quotes around the value, if needed.
|
||||
|
|
|
@ -3,18 +3,71 @@ class Pet < ApplicationRecord
|
|||
|
||||
attr_reader :items, :pet_state, :alt_style
|
||||
|
||||
def load!(timeout: nil)
|
||||
raise ModelingDisabled unless Rails.configuration.modeling_enabled
|
||||
scope :with_pet_type_color_ids, ->(color_ids) {
|
||||
joins(:pet_type).where(PetType.arel_table[:id].in(color_ids))
|
||||
}
|
||||
|
||||
viewer_data_hash = Neopets::CustomPets.fetch_viewer_data(name, timeout:)
|
||||
use_modeling_snapshot(ModelingSnapshot.new(viewer_data_hash))
|
||||
def load!(timeout: nil)
|
||||
viewer_data = Neopets::CustomPets.fetch_viewer_data(name, timeout:)
|
||||
use_viewer_data(viewer_data)
|
||||
end
|
||||
|
||||
def use_modeling_snapshot(snapshot)
|
||||
self.pet_type = snapshot.pet_type
|
||||
@pet_state = snapshot.pet_state
|
||||
@alt_style = snapshot.alt_style
|
||||
@items = snapshot.items
|
||||
def use_viewer_data(viewer_data)
|
||||
pet_data = viewer_data[:custom_pet]
|
||||
|
||||
raise UnexpectedDataFormat unless pet_data[:species_id]
|
||||
raise UnexpectedDataFormat unless pet_data[:color_id]
|
||||
raise UnexpectedDataFormat unless pet_data[:body_id]
|
||||
|
||||
has_alt_style = pet_data[:alt_style].present?
|
||||
|
||||
self.pet_type = PetType.find_or_initialize_by(
|
||||
species_id: pet_data[:species_id].to_i,
|
||||
color_id: pet_data[:color_id].to_i
|
||||
)
|
||||
|
||||
begin
|
||||
new_image_hash = Neopets::CustomPets.fetch_image_hash(self.name)
|
||||
rescue => error
|
||||
Rails.logger.warn "Failed to load image hash: #{error.full_message}"
|
||||
end
|
||||
self.pet_type.image_hash = new_image_hash if new_image_hash.present?
|
||||
|
||||
# With an alt style, `body_id` in the biology data refers to the body ID of
|
||||
# the *alt* style, not the usual pet type. (We have `original_biology` for
|
||||
# *some* of the pet type's situation, but not it's body ID!)
|
||||
#
|
||||
# So, in the alt style case, don't update `body_id` - but if this is our
|
||||
# first time seeing this pet type and it doesn't *have* a `body_id` yet,
|
||||
# let's not be creating it without one. We'll need to model it without the
|
||||
# alt style first. (I don't bother with a clear error message though 😅)
|
||||
self.pet_type.body_id = pet_data[:body_id] unless has_alt_style
|
||||
if self.pet_type.body_id.nil?
|
||||
raise UnexpectedDataFormat,
|
||||
"can't process alt style on first occurrence of pet type"
|
||||
end
|
||||
|
||||
pet_state_biology = has_alt_style ? pet_data[:original_biology] :
|
||||
pet_data[:biology_by_zone]
|
||||
raise UnexpectedDataFormat if pet_state_biology.empty?
|
||||
pet_state_biology[0] = nil # remove effects if present
|
||||
@pet_state = self.pet_type.add_pet_state_from_biology! pet_state_biology
|
||||
|
||||
if has_alt_style
|
||||
raise UnexpectedDataFormat unless pet_data[:alt_color]
|
||||
raise UnexpectedDataFormat if pet_data[:biology_by_zone].empty?
|
||||
|
||||
@alt_style = AltStyle.find_or_initialize_by(id: pet_data[:alt_style].to_i)
|
||||
@alt_style.assign_attributes(
|
||||
color_id: pet_data[:alt_color].to_i,
|
||||
species_id: pet_data[:species_id].to_i,
|
||||
body_id: pet_data[:body_id].to_i,
|
||||
biology: pet_data[:biology_by_zone],
|
||||
)
|
||||
end
|
||||
|
||||
@items = Item.collection_from_pet_type_and_registries(self.pet_type,
|
||||
viewer_data[:object_info_registry], viewer_data[:object_asset_registry])
|
||||
end
|
||||
|
||||
def wardrobe_query
|
||||
|
@ -40,8 +93,11 @@ class Pet < ApplicationRecord
|
|||
|
||||
before_validation do
|
||||
pet_type.save!
|
||||
@pet_state.save! if @pet_state
|
||||
|
||||
if @pet_state
|
||||
@pet_state.save!
|
||||
@pet_state.handle_assets!
|
||||
end
|
||||
|
||||
if @items
|
||||
@items.each do |item|
|
||||
item.save! if item.changed?
|
||||
|
@ -61,5 +117,5 @@ class Pet < ApplicationRecord
|
|||
end
|
||||
|
||||
class UnexpectedDataFormat < RuntimeError;end
|
||||
class ModelingDisabled < RuntimeError;end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
# A representation of a Neopets::CustomPets viewer data response, translated
|
||||
# to DTI's database models!
|
||||
class Pet::ModelingSnapshot
|
||||
def initialize(viewer_data_hash)
|
||||
@custom_pet = viewer_data_hash[:custom_pet]
|
||||
@object_info_registry = viewer_data_hash[:object_info_registry]
|
||||
@object_asset_registry = viewer_data_hash[:object_asset_registry]
|
||||
end
|
||||
|
||||
def pet_type
|
||||
@pet_type ||= begin
|
||||
raise Pet::UnexpectedDataFormat unless @custom_pet[:species_id]
|
||||
raise Pet::UnexpectedDataFormat unless @custom_pet[:color_id]
|
||||
raise Pet::UnexpectedDataFormat unless @custom_pet[:body_id]
|
||||
|
||||
@custom_pet => {species_id:, color_id:}
|
||||
PetType.find_or_initialize_by(species_id:, color_id:).tap do |pet_type|
|
||||
# Apply the pet's body ID to the pet type, unless it's wearing an alt
|
||||
# style, in which case ignore it, because it's the *alt style*'s body ID.
|
||||
# (This can theoretically cause a problem saving a new pet type when
|
||||
# there's an alt style too!)
|
||||
pet_type.body_id = @custom_pet[:body_id] unless @custom_pet[:alt_style]
|
||||
if pet_type.body_id.nil?
|
||||
raise Pet::UnexpectedDataFormat,
|
||||
"can't process alt style on first occurrence of pet type"
|
||||
end
|
||||
|
||||
# Try using this pet for the pet type's thumbnail, but don't worry
|
||||
# if it fails.
|
||||
begin
|
||||
pet_type.consider_pet_image(@custom_pet[:name])
|
||||
rescue => error
|
||||
Rails.logger.warn "Failed to load pet image: #{error.full_message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def pet_state
|
||||
@pet_state ||= begin
|
||||
swf_asset_ids = biology_assets.map(&:remote_id)
|
||||
pet_type.pet_states.find_or_initialize_by(swf_asset_ids:).tap do |pet_state|
|
||||
pet_state.swf_assets = biology_assets
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def alt_style
|
||||
@alt_style ||= begin
|
||||
return nil unless @custom_pet[:alt_style]
|
||||
raise Pet::UnexpectedDataFormat unless @custom_pet[:alt_color]
|
||||
|
||||
id = @custom_pet[:alt_style].to_i
|
||||
AltStyle.find_or_initialize_by(id:).tap do |alt_style|
|
||||
alt_style.assign_attributes(
|
||||
color_id: @custom_pet[:alt_color].to_i,
|
||||
species_id: @custom_pet[:species_id].to_i,
|
||||
body_id: @custom_pet[:body_id].to_i,
|
||||
swf_assets: alt_style_assets,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def items
|
||||
@items ||= Item.collection_from_pet_type_and_registries(
|
||||
pet_type, @object_info_registry, @object_asset_registry
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def biology_assets
|
||||
@biology_assets ||= begin
|
||||
biology = @custom_pet[:alt_style].present? ?
|
||||
@custom_pet[:original_biology] :
|
||||
@custom_pet[:biology_by_zone]
|
||||
assets_from_biology(biology)
|
||||
end
|
||||
end
|
||||
|
||||
def item_assets_for(item_id)
|
||||
all_infos = @object_asset_registry.values
|
||||
infos = all_infos.select { |a| a[:obj_info_id].to_i == item_id.to_i }
|
||||
infos.map do |asset_data|
|
||||
remote_id = asset_data[:asset_id].to_i
|
||||
SwfAsset.find_or_initialize_by(type: "object", remote_id:).tap do |swf_asset|
|
||||
swf_asset.origin_pet_type = pet_type
|
||||
swf_asset.origin_object_data = asset_data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def alt_style_assets
|
||||
raise Pet::UnexpectedDataFormat if @custom_pet[:biology_by_zone].empty?
|
||||
assets_from_biology(@custom_pet[:biology_by_zone])
|
||||
end
|
||||
|
||||
def assets_from_biology(biology)
|
||||
raise Pet::UnexpectedDataFormat if biology.empty?
|
||||
body_id = @custom_pet[:body_id].to_i
|
||||
biology.values.map { |b| SwfAsset.from_biology_data(body_id, b) }
|
||||
end
|
||||
end
|
|
@ -6,25 +6,17 @@ class PetState < ApplicationRecord
|
|||
has_many :contributions, :as => :contributed,
|
||||
:inverse_of => :contributed # in case of duplicates being merged
|
||||
has_many :outfits
|
||||
has_many :parent_swf_asset_relationships, :as => :parent
|
||||
has_many :parent_swf_asset_relationships, :as => :parent,
|
||||
:autosave => false
|
||||
has_many :swf_assets, :through => :parent_swf_asset_relationships
|
||||
|
||||
serialize :swf_asset_ids, coder: Serializers::IntegerSet, type: Array
|
||||
|
||||
belongs_to :pet_type
|
||||
|
||||
delegate :species_id, :species, :color_id, :color, to: :pet_type
|
||||
|
||||
alias_method :swf_asset_ids_from_association, :swf_asset_ids
|
||||
|
||||
scope :glitched, -> { where(glitched: true) }
|
||||
scope :needs_labeling, -> { unlabeled.where(glitched: false) }
|
||||
scope :unlabeled, -> { with_pose("UNKNOWN") }
|
||||
scope :usable, -> { where(labeled: true, glitched: false) }
|
||||
|
||||
scope :newest, -> { order(created_at: :desc) }
|
||||
scope :newest_pet_type, -> { joins(:pet_type).merge(PetType.newest) }
|
||||
scope :created_before, ->(time) { where(arel_table[:created_at].lt(time)) }
|
||||
|
||||
attr_writer :parent_swf_asset_relationships_to_update
|
||||
|
||||
# A simple ordering that tries to bring reliable pet states to the front.
|
||||
scope :emotion_order, -> {
|
||||
|
@ -103,16 +95,109 @@ class PetState < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def reassign_children_to!(main_pet_state)
|
||||
self.contributions.each do |contribution|
|
||||
contribution.contributed = main_pet_state
|
||||
contribution.save
|
||||
end
|
||||
self.outfits.each do |outfit|
|
||||
outfit.pet_state = main_pet_state
|
||||
outfit.save
|
||||
end
|
||||
ParentSwfAssetRelationship.where(ParentSwfAssetRelationship.arel_table[:parent_id].eq(self.id)).delete_all
|
||||
end
|
||||
|
||||
def reassign_duplicates!
|
||||
raise "This may only be applied to pet states that represent many duplicate entries" unless duplicate_ids
|
||||
pet_states = duplicate_ids.split(',').map do |id|
|
||||
PetState.find(id.to_i)
|
||||
end
|
||||
main_pet_state = pet_states.shift
|
||||
pet_states.each do |pet_state|
|
||||
pet_state.reassign_children_to!(main_pet_state)
|
||||
pet_state.destroy
|
||||
end
|
||||
end
|
||||
|
||||
def sort_swf_asset_ids!
|
||||
self.swf_asset_ids = swf_asset_ids_array.sort.join(',')
|
||||
end
|
||||
|
||||
def swf_asset_ids
|
||||
self['swf_asset_ids']
|
||||
end
|
||||
|
||||
def swf_asset_ids_array
|
||||
swf_asset_ids.split(',').map(&:to_i)
|
||||
end
|
||||
|
||||
def swf_asset_ids=(ids)
|
||||
self['swf_asset_ids'] = ids
|
||||
end
|
||||
|
||||
def handle_assets!
|
||||
@parent_swf_asset_relationships_to_update.each do |rel|
|
||||
rel.swf_asset.save!
|
||||
rel.save!
|
||||
end
|
||||
end
|
||||
|
||||
def to_param
|
||||
"#{id}-#{pose.split('_').map(&:capitalize).join('-')}"
|
||||
end
|
||||
|
||||
# Because our column is named `swf_asset_ids`, we need to ensure writes to
|
||||
# it go to the attribute, and not the thing ActiveRecord does of finding the
|
||||
# relevant `swf_assets`.
|
||||
# TODO: Consider renaming the column to `cached_swf_asset_ids`?
|
||||
def swf_asset_ids=(new_swf_asset_ids)
|
||||
write_attribute(:swf_asset_ids, new_swf_asset_ids)
|
||||
def self.from_pet_type_and_biology_info(pet_type, info)
|
||||
swf_asset_ids = []
|
||||
info.each do |zone_id, asset_info|
|
||||
if zone_id.present? && asset_info
|
||||
swf_asset_ids << asset_info[:part_id].to_i
|
||||
end
|
||||
end
|
||||
swf_asset_ids_str = swf_asset_ids.sort.join(',')
|
||||
if pet_type.new_record?
|
||||
pet_state = self.new :swf_asset_ids => swf_asset_ids_str
|
||||
else
|
||||
pet_state = self.find_or_initialize_by(
|
||||
pet_type_id: pet_type.id,
|
||||
swf_asset_ids: swf_asset_ids_str
|
||||
)
|
||||
end
|
||||
existing_swf_assets = SwfAsset.biology_assets.includes(:zone).
|
||||
where(remote_id: swf_asset_ids)
|
||||
existing_swf_assets_by_id = {}
|
||||
existing_swf_assets.each do |swf_asset|
|
||||
existing_swf_assets_by_id[swf_asset.remote_id] = swf_asset
|
||||
end
|
||||
existing_relationships_by_swf_asset_id = {}
|
||||
unless pet_state.new_record?
|
||||
pet_state.parent_swf_asset_relationships.each do |relationship|
|
||||
existing_relationships_by_swf_asset_id[relationship.swf_asset_id] = relationship
|
||||
end
|
||||
end
|
||||
pet_state.pet_type = pet_type # save the second case from having to look it up by ID
|
||||
relationships = []
|
||||
info.each do |zone_id, asset_info|
|
||||
if zone_id.present? && asset_info
|
||||
swf_asset_id = asset_info[:part_id].to_i
|
||||
swf_asset = existing_swf_assets_by_id[swf_asset_id]
|
||||
unless swf_asset
|
||||
swf_asset = SwfAsset.new
|
||||
swf_asset.remote_id = swf_asset_id
|
||||
end
|
||||
swf_asset.origin_biology_data = asset_info
|
||||
swf_asset.origin_pet_type = pet_type
|
||||
relationship = existing_relationships_by_swf_asset_id[swf_asset.id]
|
||||
unless relationship
|
||||
relationship ||= ParentSwfAssetRelationship.new
|
||||
relationship.parent = pet_state
|
||||
relationship.swf_asset_id = swf_asset.id
|
||||
end
|
||||
relationship.swf_asset = swf_asset
|
||||
relationships << relationship
|
||||
end
|
||||
end
|
||||
pet_state.parent_swf_asset_relationships_to_update = relationships
|
||||
pet_state
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -142,40 +227,5 @@ class PetState < ApplicationRecord
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.next_unlabeled_appearance(after_id: nil)
|
||||
# Rather than just getting the newest unlabeled pet state, prioritize the
|
||||
# newest *pet type*. This better matches the user's perception of what the
|
||||
# newest state is, because the Rainbow Pool UI is grouped by pet type!
|
||||
pet_states = needs_labeling.newest_pet_type.newest
|
||||
|
||||
# If `after_id` is given, convert it from a PetState ID to creation
|
||||
# timestamps, and find the next record prior to those timestamps. This
|
||||
# enables skipping past records the user doesn't want to label.
|
||||
if after_id
|
||||
begin
|
||||
after_pet_state = PetState.find(after_id)
|
||||
before_pt_created_at = after_pet_state.pet_type.created_at
|
||||
before_ps_created_at = after_pet_state.created_at
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
Rails.logger.warn "PetState.next_unlabeled_appearance: Could not " +
|
||||
"find pet state ##{after_id}"
|
||||
return nil
|
||||
end
|
||||
|
||||
# Because we sort by `newest_pet_type` first, then breaks ties by
|
||||
# `newest`, our filter needs to operate the same way. Kudos to:
|
||||
# https://brunoscheufler.com/blog/2022-01-01-paginating-large-ordered-datasets-with-cursor-based-pagination
|
||||
pet_states.merge!(
|
||||
PetType.created_before(before_pt_created_at).or(
|
||||
PetType.created_at(before_pt_created_at).and(
|
||||
PetState.created_before(before_ps_created_at)
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
pet_states.first
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -15,7 +15,10 @@ class PetType < ApplicationRecord
|
|||
species = Species.find_by_name!(species_name)
|
||||
where(color_id: color.id, species_id: species.id)
|
||||
}
|
||||
scope :newest, -> { order(created_at: :desc) }
|
||||
scope :matching_name_param, ->(name_param) {
|
||||
color_name, _, species_name = name_param.rpartition("-")
|
||||
matching_name(color_name, species_name)
|
||||
}
|
||||
scope :preferring_species, ->(species_id) {
|
||||
joins(:species).order([Arel.sql("species_id = ? DESC"), species_id])
|
||||
}
|
||||
|
@ -27,16 +30,6 @@ class PetType < ApplicationRecord
|
|||
merge(Species.order(name: :asc)).
|
||||
merge(Color.order(basic: :desc, standard: :desc, name: :asc))
|
||||
}
|
||||
scope :released_before, ->(time) {
|
||||
# We use DTI's creation timestamp as an estimate of when it was released.
|
||||
where('created_at <= ?', time)
|
||||
}
|
||||
scope :created_before, ->(time) {
|
||||
where(arel_table[:created_at].lt(time))
|
||||
}
|
||||
scope :created_at, ->(time) {
|
||||
where(arel_table[:created_at].eq(time))
|
||||
}
|
||||
|
||||
def self.random_basic_per_species(species_ids)
|
||||
random_pet_types = []
|
||||
|
@ -64,14 +57,6 @@ class PetType < ApplicationRecord
|
|||
basic_image_hash || self['image_hash'] || 'deadbeef'
|
||||
end
|
||||
|
||||
def consider_pet_image(pet_name)
|
||||
# If we already have a basic image hash, don't worry about it!
|
||||
return if basic_image_hash?
|
||||
|
||||
# Otherwise, use this as the new image hash for this pet type.
|
||||
self.image_hash = Neopets::CustomPets.fetch_image_hash(pet_name)
|
||||
end
|
||||
|
||||
def possibly_new_color
|
||||
self.color || Color.new(id: self.color_id)
|
||||
end
|
||||
|
@ -86,6 +71,11 @@ class PetType < ApplicationRecord
|
|||
species_human_name: possibly_new_species.human_name)
|
||||
end
|
||||
|
||||
def add_pet_state_from_biology!(biology)
|
||||
pet_state = PetState.from_pet_type_and_biology_info(self, biology)
|
||||
pet_state
|
||||
end
|
||||
|
||||
def canonical_pet_state
|
||||
# For consistency (randomness is always scary!), we use the PetType ID to
|
||||
# determine which gender to prefer, if it's not built into the color. That
|
||||
|
@ -123,7 +113,7 @@ class PetType < ApplicationRecord
|
|||
end
|
||||
|
||||
def to_param
|
||||
"#{possibly_new_color.to_param}-#{possibly_new_species.to_param}"
|
||||
"#{color.human_name}-#{species.human_name}"
|
||||
end
|
||||
|
||||
def fully_labeled?
|
||||
|
@ -143,19 +133,6 @@ class PetType < ApplicationRecord
|
|||
pet_states.count { |ps| ps.pose == "UNKNOWN" }
|
||||
end
|
||||
|
||||
def reference
|
||||
PetType.where(species_id: species).basic.merge(Color.alphabetical).first
|
||||
end
|
||||
|
||||
def self.find_by_param!(param)
|
||||
raise ActiveRecord::RecordNotFound unless param.include?("-")
|
||||
color_param, _, species_param = param.rpartition("-")
|
||||
where(
|
||||
color_id: Color.param_to_id(color_param),
|
||||
species_id: Species.param_to_id(species_param),
|
||||
).first!
|
||||
end
|
||||
|
||||
def self.basic_body_ids
|
||||
PetType.basic.distinct.pluck(:body_id)
|
||||
end
|
||||
|
|
|
@ -16,10 +16,6 @@ class Species < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def to_param
|
||||
name? ? human_name : id.to_s
|
||||
end
|
||||
|
||||
# Given a list of body IDs, return a hash from body ID to Species.
|
||||
# (We assume that each body ID belongs to just one species; if not, which
|
||||
# species we return for that body ID is undefined.)
|
||||
|
@ -30,8 +26,4 @@ class Species < ApplicationRecord
|
|||
to_h { |s| [s.id, s] }
|
||||
species_ids_by_body_id.transform_values { |id| species_by_id[id] }
|
||||
end
|
||||
|
||||
def self.param_to_id(param)
|
||||
param.match?(/\A\d+\Z/) ? param.to_i : find_by_name!(param).id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
require 'addressable/template'
|
||||
require 'async'
|
||||
require 'async/barrier'
|
||||
require 'async/semaphore'
|
||||
|
||||
class SwfAsset < ApplicationRecord
|
||||
# We use the `type` column to mean something other than what Rails means!
|
||||
|
@ -38,7 +41,7 @@ class SwfAsset < ApplicationRecord
|
|||
{
|
||||
swf: url,
|
||||
png: image_url,
|
||||
svg: svg_url,
|
||||
svg: manifest_asset_urls[:svg],
|
||||
canvas_library: manifest_asset_urls[:js],
|
||||
manifest: manifest_url,
|
||||
}
|
||||
|
@ -183,18 +186,6 @@ class SwfAsset < ApplicationRecord
|
|||
nil
|
||||
end
|
||||
|
||||
def image_url?
|
||||
image_url.present?
|
||||
end
|
||||
|
||||
def svg_url
|
||||
manifest_asset_urls[:svg]
|
||||
end
|
||||
|
||||
def svg_url?
|
||||
svg_url.present?
|
||||
end
|
||||
|
||||
def canvas_movie?
|
||||
canvas_movie_library_url.present?
|
||||
end
|
||||
|
@ -329,12 +320,30 @@ class SwfAsset < ApplicationRecord
|
|||
swf_asset
|
||||
end
|
||||
|
||||
def self.from_wardrobe_link_params(ids)
|
||||
where((
|
||||
arel_table[:remote_id].in(ids[:biology]).and(arel_table[:type].eq('biology'))
|
||||
).or(
|
||||
arel_table[:remote_id].in(ids[:object]).and(arel_table[:type].eq('object'))
|
||||
))
|
||||
end
|
||||
|
||||
# Given a list of SWF assets, ensure all of their manifests are loaded, with
|
||||
# fast concurrent execution!
|
||||
def self.preload_manifests(swf_assets)
|
||||
DTIRequests.load_many(max_at_once: 10) do |task|
|
||||
swf_assets.each do |swf_asset|
|
||||
task.async do
|
||||
# Blocks all tasks beneath it.
|
||||
barrier = Async::Barrier.new
|
||||
|
||||
Sync do
|
||||
# Only allow 10 manifests to be loaded at a time.
|
||||
semaphore = Async::Semaphore.new(10, parent: barrier)
|
||||
|
||||
# Load all the manifests in async tasks. This will load them 10 at a time
|
||||
# rather than all at once (because of the semaphore), and the
|
||||
# NeopetsMediaArchive will share a pool of persistent connections for
|
||||
# them.
|
||||
swf_assets.map do |swf_asset|
|
||||
semaphore.async do
|
||||
begin
|
||||
# Don't save changes in this big async situation; we'll do it all
|
||||
# in one batch after, to avoid too much database concurrency!
|
||||
|
@ -345,6 +354,11 @@ class SwfAsset < ApplicationRecord
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Wait until all tasks are done.
|
||||
barrier.wait
|
||||
ensure
|
||||
barrier.stop # If something goes wrong, clean up all tasks.
|
||||
end
|
||||
|
||||
SwfAsset.transaction do
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
require "addressable/template"
|
||||
require "async/http/internet/instance"
|
||||
|
||||
module Neopets::NCMall
|
||||
# Share a pool of persistent connections, rather than reconnecting on
|
||||
# each request. (This library does that automatically!)
|
||||
INTERNET = Async::HTTP::Internet.instance
|
||||
|
||||
# Load the NC Mall home page content area, and return its useful data.
|
||||
HOME_PAGE_URL = "https://ncmall.neopets.com/mall/ajax/home_page.phtml"
|
||||
def self.load_home_page
|
||||
|
@ -21,10 +26,12 @@ module Neopets::NCMall
|
|||
PAGE_LINK_PATTERN = /load_items_pane\(['"](.+?)['"], ([0-9]+)\).+?>(.+?)</
|
||||
def self.load_page_links
|
||||
html = Sync do
|
||||
DTIRequests.get(ROOT_DOCUMENT_URL) do |response|
|
||||
INTERNET.get(ROOT_DOCUMENT_URL, [
|
||||
["User-Agent", Rails.configuration.user_agent_for_neopets],
|
||||
]) do |response|
|
||||
if response.status != 200
|
||||
raise ResponseNotOK.new(response.status),
|
||||
"expected status 200 but got #{response.status} (#{ROOT_DOCUMENT_URL})"
|
||||
"expected status 200 but got #{response.status} (#{url})"
|
||||
end
|
||||
|
||||
response.read
|
||||
|
@ -38,41 +45,13 @@ module Neopets::NCMall
|
|||
uniq
|
||||
end
|
||||
|
||||
STYLING_STUDIO_URL = "https://www.neopets.com/np-templates/ajax/stylingstudio/studio.php"
|
||||
def self.load_styles(species_id:, neologin:)
|
||||
Sync do
|
||||
DTIRequests.post(
|
||||
STYLING_STUDIO_URL,
|
||||
[
|
||||
["Content-Type", "application/x-www-form-urlencoded"],
|
||||
["Cookie", "neologin=#{neologin}"],
|
||||
["X-Requested-With", "XMLHttpRequest"],
|
||||
],
|
||||
{tab: 1, mode: "getStyles", species: species_id}.to_query,
|
||||
) do |response|
|
||||
if response.status != 200
|
||||
raise ResponseNotOK.new(response.status),
|
||||
"expected status 200 but got #{response.status} (#{STYLING_STUDIO_URL})"
|
||||
end
|
||||
|
||||
begin
|
||||
data = JSON.parse(response.read).deep_symbolize_keys
|
||||
|
||||
# HACK: styles is a hash, unless it's empty, in which case it's an
|
||||
# array? Weird. Normalize this by converting to hash.
|
||||
data.fetch(:styles).to_h.values
|
||||
rescue JSON::ParserError, KeyError
|
||||
raise UnexpectedResponseFormat
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.load_page_by_url(url)
|
||||
Sync do
|
||||
DTIRequests.get(url) do |response|
|
||||
INTERNET.get(url, [
|
||||
["User-Agent", Rails.configuration.user_agent_for_neopets],
|
||||
]) do |response|
|
||||
if response.status != 200
|
||||
raise ResponseNotOK.new(response.status),
|
||||
"expected status 200 but got #{response.status} (#{url})"
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
require "async/http/internet/instance"
|
||||
|
||||
# While most of our NeoPass logic is built into Devise -> OmniAuth -> OIDC
|
||||
# OmniAuth plugin, NeoPass also offers some supplemental APIs that we use here.
|
||||
module Neopets::NeoPass
|
||||
# Share a pool of persistent connections, rather than reconnecting on
|
||||
# each request. (This library does that automatically!)
|
||||
INTERNET = Async::HTTP::Internet.instance
|
||||
|
||||
def self.load_main_neopets_username(access_token)
|
||||
linkages = load_linkages(access_token)
|
||||
|
||||
|
@ -26,10 +32,10 @@ module Neopets::NeoPass
|
|||
LINKAGE_URL = "https://oidc.neopets.com/linkage/all"
|
||||
def self.load_linkages(access_token)
|
||||
linkages_str = Sync do
|
||||
DTIRequests.get(
|
||||
LINKAGE_URL,
|
||||
[["Authorization", "Bearer #{access_token}"]],
|
||||
) do |response|
|
||||
INTERNET.get(LINKAGE_URL, [
|
||||
["User-Agent", Rails.configuration.user_agent_for_neopets],
|
||||
["Authorization", "Bearer #{access_token}"],
|
||||
]) do |response|
|
||||
if response.status != 200
|
||||
raise ResponseNotOK.new(response.status),
|
||||
"expected status 200 but got #{response.status} (#{LINKAGE_URL})"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require "addressable/uri"
|
||||
require "async/http/internet/instance"
|
||||
require "json"
|
||||
|
||||
# The Neopets Media Archive is a service that mirrors images.neopets.com files
|
||||
|
@ -10,6 +11,10 @@ require "json"
|
|||
# long-term archive, not dependent on their services having 100% uptime in
|
||||
# order for us to operate. We never discard old files, we just keep going!
|
||||
module NeopetsMediaArchive
|
||||
# Share a pool of persistent connections, rather than reconnecting on
|
||||
# each request. (This library does that automatically!)
|
||||
INTERNET = Async::HTTP::Internet.instance
|
||||
|
||||
ROOT_PATH = Pathname.new(Rails.configuration.neopets_media_archive_root)
|
||||
|
||||
# Load the file from the given `images.neopets.com` URI.
|
||||
|
@ -67,7 +72,9 @@ module NeopetsMediaArchive
|
|||
# We use this in the `swf_assets:manifests:load` task to perform many
|
||||
# requests in parallel!
|
||||
Sync do
|
||||
DTIRequests.get(uri) do |response|
|
||||
INTERNET.get(uri, [
|
||||
["User-Agent", Rails.configuration.user_agent_for_neopets],
|
||||
]) do |response|
|
||||
if response.status != 200
|
||||
raise ResponseNotOK.new(response.status),
|
||||
"expected status 200 but got #{response.status} (#{uri})"
|
||||
|
|
|
@ -13,25 +13,28 @@
|
|||
|
||||
= image_tag @alt_style.preview_image_url, class: "alt-style-preview"
|
||||
|
||||
= support_form_with model: @alt_style, class: "support-form" do |f|
|
||||
= f.errors
|
||||
|
||||
= f.fields do
|
||||
= f.field do
|
||||
= f.label :real_series_name, "Series"
|
||||
= f.text_field :real_series_name, autofocus: !@alt_style.real_series_name?,
|
||||
placeholder: AltStyle.placeholder_name
|
||||
|
||||
= f.field do
|
||||
= f.label :thumbnail_url, "Thumbnail"
|
||||
= f.thumbnail_input :thumbnail_url
|
||||
|
||||
= f.actions do
|
||||
= form_with model: @alt_style, class: "alt-style-form" do |f|
|
||||
- if @alt_style.errors.any?
|
||||
%p
|
||||
Could not save:
|
||||
%ul.errors
|
||||
- @alt_style.errors.each do |error|
|
||||
%li= error.full_message
|
||||
%fieldset
|
||||
= f.label :real_series_name, "Series"
|
||||
= f.text_field :real_series_name
|
||||
= f.label :thumbnail_url, "Thumbnail"
|
||||
.thumbnail-field
|
||||
- if @alt_style.thumbnail_url?
|
||||
= image_tag @alt_style.thumbnail_url
|
||||
= f.url_field :thumbnail_url
|
||||
.actions
|
||||
= f.submit "Save changes"
|
||||
= f.go_to_next_field title: "If checked, takes you to the next unlabeled pet style, if any. Useful for labeling in bulk!" do
|
||||
= f.go_to_next_check_box "unlabeled-style"
|
||||
%label{title: "If checked, takes you to the next unlabeled pet style, if any. Useful for labeling in bulk!"}
|
||||
= check_box_tag "next", "unlabeled-style",
|
||||
checked: params[:next] == "unlabeled-style"
|
||||
Then: Go to unlabeled style
|
||||
|
||||
- content_for :stylesheets do
|
||||
= stylesheet_link_tag "application/breadcrumbs", "application/support-form"
|
||||
= stylesheet_link_tag "application/breadcrumbs"
|
||||
= page_stylesheet_link_tag "alt_styles/edit"
|
||||
|
|
|
@ -21,9 +21,7 @@
|
|||
}
|
||||
- if swf_asset.canvas_movie?
|
||||
%iframe{src: swf_asset_path(swf_asset, playing: outfit_viewer_is_playing ? true : nil)}
|
||||
- elsif preferred_image_format == :svg && swf_asset.svg_url?
|
||||
= image_tag swf_asset.svg_url, alt: "", loading: "lazy"
|
||||
- elsif swf_asset.image_url?
|
||||
- elsif swf_asset.image_url.present?
|
||||
= image_tag swf_asset.image_url, alt: "", loading: "lazy"
|
||||
- else
|
||||
/ No movie or image available for SWF asset: #{swf_asset.url}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
- if form.object.errors.any?
|
||||
%section.errors
|
||||
Could not save:
|
||||
|
||||
%ul
|
||||
- form.object.errors.each do |error|
|
||||
%li= error.full_message
|
|
@ -1,4 +0,0 @@
|
|||
= form.field("data-type": "radio", **options) do
|
||||
%fieldset
|
||||
%legend= legend
|
||||
%ul= content
|
|
@ -1,5 +0,0 @@
|
|||
- url = form.object.send(method)
|
||||
.thumbnail-input
|
||||
- if url.present?
|
||||
= image_tag url, alt: "Thumbnail"
|
||||
= form.url_field method
|
|
@ -46,8 +46,6 @@
|
|||
= link_to t('items.show.closet_hangers.button'),
|
||||
user_closet_hangers_path(current_user),
|
||||
class: 'user-lists-form-opener'
|
||||
- if support_staff?
|
||||
= link_to "Edit", edit_item_path(item)
|
||||
|
||||
- if user_signed_in?
|
||||
= form_tag update_quantities_user_item_closet_hangers_path(user_id: current_user, item_id: item), method: :put, class: 'user-lists-form', hidden: item_header_user_lists_form_state != "open" do
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
- title "Editing \"#{@item.name}\""
|
||||
- use_responsive_design
|
||||
|
||||
%h1#title Editing "#{@item.name}"
|
||||
|
||||
:markdown
|
||||
Heads up: the modeling process controls some of these fields by default! If
|
||||
you change something, but it doesn't match what we're seeing on Neopets.com,
|
||||
it will probably be reverted automatically when someone models it.
|
||||
|
||||
= support_form_with model: @item, class: "support-form" do |f|
|
||||
= f.errors
|
||||
|
||||
= f.fields do
|
||||
= f.field do
|
||||
= f.label :name
|
||||
= f.text_field :name
|
||||
|
||||
= f.field do
|
||||
= f.label :thumbnail_url, "Thumbnail"
|
||||
= f.thumbnail_input :thumbnail_url
|
||||
|
||||
= f.field do
|
||||
= f.label :description
|
||||
= f.text_field :description
|
||||
|
||||
= f.radio_fieldset "Item kind" do
|
||||
= f.radio_field title: "NC items generally have a rarity value of 500.\nPaintbrush items generally contain a special message in the description." do
|
||||
= f.radio_button :is_manually_nc, false
|
||||
Automatic: Based on rarity and description
|
||||
= f.radio_field title: "Use this when Neopets releases an NC item, but labels the rarity as something other than 500, usually by mistake." do
|
||||
= f.radio_button :is_manually_nc, true
|
||||
Manually NC: From the NC Mall, but not r500
|
||||
|
||||
= f.radio_fieldset "Modeling status" do
|
||||
= f.radio_field title: "If we fit two or more species of a standard color, assume we also fit the other standard-color pets that were released at the time.\nRepeat for special colors like Baby and Maraquan." do
|
||||
= f.radio_button :modeling_status_hint, ""
|
||||
Automatic: Fits 2+ species → Should fit all
|
||||
= f.radio_field title: "Use this when e.g. there simply is no Acara version of the item." do
|
||||
= f.radio_button :modeling_status_hint, "done"
|
||||
Done: Neopets.com is missing some models
|
||||
= f.radio_field title: "Use this when e.g. this fits the Blue Vandagyre even though it's a Maraquan item.\nBehaves identically to Done, but helps us remember why we did this!" do
|
||||
= f.radio_button :modeling_status_hint, "glitchy"
|
||||
Glitchy: Neopets.com has <em>too many</em> models
|
||||
|
||||
= f.radio_fieldset "Body fit" do
|
||||
= f.radio_field title: "When an asset in a zone like Background is modeled, assume it fits all pets the same, and assign it body ID \#0.\nOtherwise, assume it fits only the kind of pet it was modeled on." do
|
||||
= f.radio_button :explicitly_body_specific, false
|
||||
Automatic: Some zones fit all species
|
||||
= f.radio_field title: "Use this when an item uses a generally-universal zone like Static, but is body-specific regardless. \"Encased in Ice\" is one example.\nThis prevents these uncommon items from breaking every time they're modeled." do
|
||||
= f.radio_button :explicitly_body_specific, true
|
||||
Body-specific: Fits all species differently
|
||||
|
||||
= f.actions do
|
||||
= f.submit "Save changes"
|
||||
|
||||
- content_for :stylesheets do
|
||||
= page_stylesheet_link_tag "application/support-form"
|
|
@ -4,19 +4,20 @@
|
|||
|
||||
%p#pet-not-found.alert= t 'pets.load.not_found'
|
||||
|
||||
- hide_after Date.new(2024, 12, 8) do
|
||||
- if show_announcement?
|
||||
%section.announcement
|
||||
= image_tag "about/announcement.png", width: 70, height: 70,
|
||||
srcset: {"about/announcement@2x.png": "2x"}
|
||||
.content
|
||||
%p
|
||||
%strong Oh wow, it's busy this time of year!
|
||||
We've temporarily moved to a bigger server, to help us handle the extra
|
||||
load. Hopefully this keeps us running smooth!
|
||||
%strong
|
||||
🎃
|
||||
= link_to "New pet styles are out today!", alt_styles_path
|
||||
If you've seen one we don't have yet, please model it by entering the
|
||||
pet's name in the box below. Thank you!!
|
||||
%p
|
||||
Happy holidays, everyone! Here's hoping you, and your families, and your
|
||||
precious pets—both online and off—stay happy and healthy for the year
|
||||
to come 💜
|
||||
By the way, we had a bug where modeling new styles wasn't working for a
|
||||
little while. Fixed now! 🤞
|
||||
|
||||
#outfit-forms
|
||||
#pet-preview
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
= content_tag "support-outfit-viewer", **html_options do
|
||||
= turbo_frame_tag "support-outfit-viewer-preview" do
|
||||
%div
|
||||
-# Render an outfit viewer in a magnifier. Use SVG by default for clarity,
|
||||
-# but also offer an option to switch to PNG if it looks wrong.
|
||||
%magic-magnifier
|
||||
= outfit_viewer outfit,
|
||||
preferred_image_format: params[:preferred_image_format] == "png" ? :png : :svg
|
||||
|
||||
= form_with method: :get, class: "outfit-viewer-controls" do |f|
|
||||
%fieldset
|
||||
%legend Format
|
||||
%label
|
||||
= f.radio_button "preferred_image_format", "svg",
|
||||
checked: params[:preferred_image_format] != "png"
|
||||
SVG
|
||||
%label
|
||||
= f.radio_button "preferred_image_format", "png",
|
||||
checked: params[:preferred_image_format] == "png"
|
||||
PNG
|
||||
= f.submit "Update"
|
||||
|
||||
%table
|
||||
%thead
|
||||
%tr
|
||||
%th{scope: "col"} DTI ID
|
||||
%th{scope: "col"} Zone
|
||||
%th{scope: "col"} Links
|
||||
%tbody
|
||||
- outfit.visible_layers.each do |swf_asset|
|
||||
%tr
|
||||
%th{scope: "row", "data-field": "id"}
|
||||
= swf_asset.id
|
||||
%td
|
||||
= swf_asset.zone.label
|
||||
(##{swf_asset.zone.id})
|
||||
%td{"data-field": "links"}
|
||||
%ul
|
||||
- if swf_asset.image_url?
|
||||
%li= link_to "PNG", swf_asset.image_url, target: "_blank"
|
||||
- if swf_asset.svg_url?
|
||||
%li= link_to "SVG", swf_asset.svg_url, target: "_blank"
|
||||
%li= link_to "SWF", swf_asset.url, target: "_blank"
|
||||
- if swf_asset.manifest_url?
|
||||
%li= link_to "Manifest", swf_asset.manifest_url, target: "_blank"
|
|
@ -5,54 +5,44 @@
|
|||
%li
|
||||
= link_to "Rainbow Pool", pet_types_path
|
||||
%li
|
||||
= link_to @pet_type.possibly_new_color.human_name,
|
||||
pet_types_path(color: @pet_type.possibly_new_color.human_name)
|
||||
= link_to @pet_type.color.human_name,
|
||||
pet_types_path(color: @pet_type.color.human_name)
|
||||
%li{"data-relation-to-prev": "sibling"}
|
||||
= link_to @pet_type.possibly_new_species.human_name,
|
||||
pet_types_path(species: @pet_type.possibly_new_species.human_name)
|
||||
= link_to @pet_type.species.human_name,
|
||||
pet_types_path(species: @pet_type.species.human_name)
|
||||
%li
|
||||
= link_to "Appearances", @pet_type
|
||||
%li
|
||||
\##{@pet_state.id}
|
||||
|
||||
= support_outfit_viewer pet_state: @pet_state
|
||||
= outfit_viewer pet_state: @pet_state
|
||||
|
||||
= support_form_with model: [@pet_type, @pet_state] do |f|
|
||||
= f.errors
|
||||
|
||||
= f.fields do
|
||||
= f.radio_grid_fieldset "Pose" do
|
||||
- pose_options.each do |pose|
|
||||
= f.radio_field do
|
||||
= f.radio_button :pose, pose
|
||||
= pose_name(pose)
|
||||
- if @reference_pet_type
|
||||
= link_to @reference_pet_type, target: "_blank", class: "reference-link" do
|
||||
= pet_type_image @reference_pet_type, :happy, :face
|
||||
%span Reference: #{@reference_pet_type.human_name}
|
||||
= external_link_icon
|
||||
|
||||
= f.field do
|
||||
= f.label :glitched, "Glitched?"
|
||||
= form_with model: [@pet_type, @pet_state] do |f|
|
||||
- if @pet_state.errors.any?
|
||||
%p
|
||||
Could not save:
|
||||
%ul.errors
|
||||
- @pet_state.errors.each do |error|
|
||||
%li= error.full_message
|
||||
%dl
|
||||
%dt Pose
|
||||
%dd
|
||||
%ul.pose-options
|
||||
- pose_options.each do |pose|
|
||||
%li
|
||||
%label
|
||||
= f.radio_button :pose, pose
|
||||
= pose_name pose
|
||||
%dt Glitched?
|
||||
%dd
|
||||
= f.select :glitched, [["✅ Not marked as Glitched", false],
|
||||
["👾 Yes, it's bad news bonko'd", true]]
|
||||
|
||||
= f.actions do
|
||||
= f.submit "Save changes"
|
||||
= f.go_to_next_field after: @pet_state.id,
|
||||
title: "If checked, takes you to the first unlabeled appearance in the database, if any. Useful for labeling in bulk!" do
|
||||
= f.go_to_next_check_box "unlabeled-appearance"
|
||||
Then: Go to next unlabeled appearance
|
||||
["👾 Yes, it's bad news bonko'd", true]]
|
||||
= f.submit "Save"
|
||||
|
||||
- content_for :stylesheets do
|
||||
= stylesheet_link_tag "application/breadcrumbs"
|
||||
= stylesheet_link_tag "application/magic-magnifier"
|
||||
= stylesheet_link_tag "application/outfit-viewer"
|
||||
= stylesheet_link_tag "application/support-form"
|
||||
= stylesheet_link_tag "pet_states/support-outfit-viewer"
|
||||
= page_stylesheet_link_tag "pet_states/edit"
|
||||
|
||||
- content_for :javascripts do
|
||||
= javascript_include_tag "magic-magnifier"
|
||||
= javascript_include_tag "outfit-viewer"
|
||||
= javascript_include_tag "pet_states/support-outfit-viewer"
|
||||
|
|
|
@ -10,18 +10,6 @@
|
|||
|
||||
[1]: #{alt_styles_path}
|
||||
|
||||
- if support_staff?
|
||||
%p
|
||||
%strong 💡 Support summary:
|
||||
✅ #{number_with_delimiter @counts[:usable]} usable
|
||||
+ 👾 #{number_with_delimiter @counts[:glitched]} glitched
|
||||
- if @unlabeled_appearance
|
||||
+ ❓️
|
||||
= link_to "#{number_with_delimiter @counts[:needs_labeling]} unknown",
|
||||
edit_pet_type_pet_state_path(@unlabeled_appearance.pet_type,
|
||||
@unlabeled_appearance, next: "unlabeled-appearance")
|
||||
\= #{number_with_delimiter @counts[:total]} total
|
||||
|
||||
= form_with method: :get, class: "rainbow-pool-filters" do |form|
|
||||
%fieldset
|
||||
%legend Filter by:
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
%li
|
||||
= link_to "Rainbow Pool", pet_types_path
|
||||
%li
|
||||
= link_to @pet_type.possibly_new_color.human_name,
|
||||
pet_types_path(color: @pet_type.possibly_new_color.human_name)
|
||||
= link_to @pet_type.color.human_name,
|
||||
pet_types_path(color: @pet_type.color.human_name)
|
||||
%li{"data-relation-to-prev": "sibling"}
|
||||
= link_to @pet_type.possibly_new_species.human_name,
|
||||
pet_types_path(species: @pet_type.possibly_new_species.human_name)
|
||||
= link_to @pet_type.species.human_name,
|
||||
pet_types_path(species: @pet_type.species.human_name)
|
||||
%li
|
||||
Appearances
|
||||
|
||||
|
|
|
@ -103,10 +103,6 @@ Rails.application.configure do
|
|||
# Allow connections on Vagrant's private network.
|
||||
config.web_console.permissions = '10.0.2.2'
|
||||
|
||||
# Allow pets to model new data. (If modeling is ever broken, disable this in
|
||||
# production while we fix it!)
|
||||
config.modeling_enabled = true
|
||||
|
||||
# Use a local copy of Impress 2020, presumably running on port 4000. (Can
|
||||
# override this with the IMPRESS_2020_ORIGIN environment variable!)
|
||||
config.impress_2020_origin = ENV.fetch("IMPRESS_2020_ORIGIN",
|
||||
|
|
|
@ -122,10 +122,6 @@ Rails.application.configure do
|
|||
# Skip DNS rebinding protection for the default health check endpoint.
|
||||
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
|
||||
|
||||
# Allow pets to model new data. (If modeling is ever broken, disable this
|
||||
# here while we fix it!)
|
||||
config.modeling_enabled = true
|
||||
|
||||
# Use the live copy of Impress 2020. (Can override this with the
|
||||
# IMPRESS_2020_ORIGIN environment variable!)
|
||||
config.impress_2020_origin = ENV.fetch("IMPRESS_2020_ORIGIN",
|
||||
|
|
|
@ -62,10 +62,6 @@ Rails.application.configure do
|
|||
# Raise error when a before_action's only/except options reference missing actions
|
||||
config.action_controller.raise_on_missing_callback_actions = true
|
||||
|
||||
# Allow pets to model new data. (If modeling is ever broken, disable this in
|
||||
# production while we fix it!)
|
||||
config.modeling_enabled = true
|
||||
|
||||
# Use a local copy of Impress 2020, presumably running on port 4000. (Can
|
||||
# override this with the IMPRESS_2020_ORIGIN environment variable!)
|
||||
config.impress_2020_origin = ENV.fetch("IMPRESS_2020_ORIGIN",
|
||||
|
|
|
@ -16,15 +16,13 @@
|
|||
# end
|
||||
|
||||
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
# `lib/rocketamf` => `RocketAMF`
|
||||
# Teach Zeitwerk that `RocketAMF` is what to expect in `lib/rocketamf`.
|
||||
inflect.acronym "RocketAMF"
|
||||
|
||||
# `neopass.rb` => `NeoPass`
|
||||
# Teach Zeitwerk that `NeoPass` is what to expect in `neopass.rb`.
|
||||
inflect.acronym "NeoPass"
|
||||
|
||||
# `nc_mall.rb` => `NCMall`
|
||||
# Teach Zeitwerk that "NCMall" is what to expect in `nc_mall.rb`.
|
||||
# (We do this by teaching it the word "NC".)
|
||||
inflect.acronym "NC"
|
||||
|
||||
# `dti_requests.rb` => `DTIRequests`
|
||||
inflect.acronym "DTI"
|
||||
end
|
||||
|
|
|
@ -229,7 +229,7 @@ en:
|
|||
swf_asset_html: "%{item_description} on a new body type"
|
||||
pet_type_html: "%{pet_type_description} for the first time"
|
||||
pet_state_html: "a new pose for %{pet_type_description}"
|
||||
alt_style_html: "a new NC Style of the %{alt_style_name}"
|
||||
alt_style_html: "a new Alt Style of the %{alt_style_name}"
|
||||
|
||||
contribution:
|
||||
description_html: "%{user_link} showed us %{contributed_description}"
|
||||
|
|
|
@ -19,7 +19,7 @@ OpenneoImpressItems::Application.routes.draw do
|
|||
get '/users/current-user/outfits', to: redirect('/your-outfits')
|
||||
|
||||
# Our customization data! Both the item pages, and JSON API endpoints.
|
||||
resources :items, only: [:index, :show, :edit, :update] do
|
||||
resources :items, :only => [:index, :show] do
|
||||
resources :trades, path: 'trades/:type', controller: 'item_trades',
|
||||
only: [:index], constraints: {type: /offering|seeking/}
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
class IncreasePetTypeColorIdAndSpeciesIdLimit < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
reversible do |direction|
|
||||
change_table :pet_types do |t|
|
||||
direction.up do
|
||||
t.change :color_id, :integer, null: false
|
||||
t.change :species_id, :integer, null: false
|
||||
end
|
||||
|
||||
direction.down do
|
||||
t.change :color_id, :integer, limit: 1, null: false
|
||||
t.change :species_id, :integer, limit: 1, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
class IncreaseIdLimits < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
reversible do |direction|
|
||||
direction.up do
|
||||
change_column :parents_swf_assets, :parent_id, :integer, null: false
|
||||
change_column :parents_swf_assets, :swf_asset_id, :integer, null: false
|
||||
change_column :pet_states, :pet_type_id, :integer, null: false
|
||||
change_column :pets, :pet_type_id, :integer, null: false
|
||||
change_column :swf_assets, :zone_id, :integer, null: false
|
||||
end
|
||||
|
||||
direction.down do
|
||||
change_column :parents_swf_assets, :parent_id, :integer, limit: 3, null: false
|
||||
change_column :parents_swf_assets, :swf_asset_id, :integer, limit: 3, null: false
|
||||
change_column :pet_states, :pet_type_id, :integer, limit: 3, null: false
|
||||
change_column :pets, :pet_type_id, :integer, limit: 3, null: false
|
||||
change_column :swf_assets, :zone_id, :integer, limit: 1, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
class AddCachedPredictedFullyModeledToItems < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :items, :cached_predicted_fully_modeled, :boolean,
|
||||
default: false, null: false
|
||||
|
||||
reversible do |direction|
|
||||
direction.up do
|
||||
puts "Updating cached item fields for all items…"
|
||||
Item.includes(:swf_assets).find_in_batches.with_index do |items, batch|
|
||||
puts "Updating item batch ##{batch+1}…"
|
||||
items.each(&:update_cached_fields)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
17
db/schema.rb
17
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_10_08_004715) do
|
||||
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||
t.integer "species_id", null: false
|
||||
t.integer "color_id", null: false
|
||||
|
@ -139,7 +139,6 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
|
|||
t.integer "dyeworks_base_item_id"
|
||||
t.string "cached_occupied_zone_ids", default: ""
|
||||
t.text "cached_compatible_body_ids", default: ""
|
||||
t.boolean "cached_predicted_fully_modeled", default: false, null: false
|
||||
t.index ["dyeworks_base_item_id"], name: "index_items_on_dyeworks_base_item_id"
|
||||
t.index ["modeling_status_hint", "created_at", "id"], name: "items_modeling_status_hint_and_created_at_and_id"
|
||||
t.index ["modeling_status_hint", "created_at"], name: "items_modeling_status_hint_and_created_at"
|
||||
|
@ -197,8 +196,8 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
|
|||
end
|
||||
|
||||
create_table "parents_swf_assets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||
t.integer "parent_id", null: false
|
||||
t.integer "swf_asset_id", null: false
|
||||
t.integer "parent_id", limit: 3, null: false
|
||||
t.integer "swf_asset_id", limit: 3, null: false
|
||||
t.string "parent_type", limit: 8, null: false
|
||||
t.index ["parent_id", "parent_type"], name: "index_parents_swf_assets_on_parent_id_and_parent_type"
|
||||
t.index ["parent_id", "swf_asset_id"], name: "unique_parents_swf_assets", unique: true
|
||||
|
@ -212,7 +211,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
|
|||
end
|
||||
|
||||
create_table "pet_states", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||
t.integer "pet_type_id", null: false
|
||||
t.integer "pet_type_id", limit: 3, null: false
|
||||
t.text "swf_asset_ids", size: :medium, null: false
|
||||
t.boolean "female"
|
||||
t.integer "mood_id"
|
||||
|
@ -226,8 +225,8 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
|
|||
end
|
||||
|
||||
create_table "pet_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||
t.integer "color_id", null: false
|
||||
t.integer "species_id", null: false
|
||||
t.integer "color_id", limit: 1, null: false
|
||||
t.integer "species_id", limit: 1, null: false
|
||||
t.datetime "created_at", precision: nil, null: false
|
||||
t.integer "body_id", limit: 2, null: false
|
||||
t.string "image_hash", limit: 8
|
||||
|
@ -241,7 +240,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
|
|||
|
||||
create_table "pets", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||
t.string "name", limit: 20, null: false
|
||||
t.integer "pet_type_id", null: false
|
||||
t.integer "pet_type_id", limit: 3, null: false
|
||||
t.index ["name"], name: "pets_name", unique: true
|
||||
t.index ["pet_type_id"], name: "pets_pet_type_id"
|
||||
end
|
||||
|
@ -254,7 +253,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
|
|||
t.string "type", limit: 7, null: false
|
||||
t.integer "remote_id", limit: 3, null: false
|
||||
t.text "url", size: :long, null: false
|
||||
t.integer "zone_id", null: false
|
||||
t.integer "zone_id", limit: 1, null: false
|
||||
t.text "zones_restrict", size: :medium, null: false
|
||||
t.datetime "created_at", precision: nil, null: false
|
||||
t.integer "body_id", limit: 2, null: false
|
||||
|
|
|
@ -442,21 +442,13 @@
|
|||
mode: "755"
|
||||
state: directory
|
||||
|
||||
- name: Remove 10min cron job to run `rails nc_mall:sync`
|
||||
- name: Create 10min cron job to run `rails nc_mall:sync`
|
||||
become_user: impress
|
||||
cron:
|
||||
state: absent
|
||||
name: "Impress: sync NC Mall data"
|
||||
minute: "*/10"
|
||||
job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails nc_mall:sync'"
|
||||
|
||||
- name: Create 10min cron job to run `rails neopets:import:nc_mall`
|
||||
become_user: impress
|
||||
cron:
|
||||
name: "Impress: import NC Mall data"
|
||||
minute: "*/10"
|
||||
job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails neopets:import:nc_mall'"
|
||||
|
||||
- name: Create weekly cron job to run `rails public_data:commit`
|
||||
become_user: impress
|
||||
cron:
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
require "async"
|
||||
require "async/barrier"
|
||||
require "async/http/internet/instance"
|
||||
|
||||
module DTIRequests
|
||||
class << self
|
||||
def get(url, headers = [], &block)
|
||||
Async::HTTP::Internet.get(url, add_headers(headers), &block)
|
||||
end
|
||||
|
||||
def post(url, headers = [], body = nil, &block)
|
||||
Async::HTTP::Internet.post(url, add_headers(headers), body, &block)
|
||||
end
|
||||
|
||||
def load_many(max_at_once: 10)
|
||||
barrier = Async::Barrier.new
|
||||
semaphore = Async::Semaphore.new(max_at_once, parent: barrier)
|
||||
|
||||
Sync do
|
||||
block_return_value = yield semaphore
|
||||
barrier.wait # Load all the subtasks.
|
||||
block_return_value
|
||||
ensure
|
||||
barrier.stop # If any subtasks failed, cancel the rest.
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_headers(headers)
|
||||
if headers.none? { |(k, v)| k.downcase == "user-agent" }
|
||||
headers += [["User-Agent", Rails.configuration.user_agent_for_neopets]]
|
||||
end
|
||||
headers
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
namespace :items do
|
||||
desc "Update cached fields for all items (useful if logic changes)"
|
||||
task :update_cached_fields => :environment do
|
||||
puts "Updating cached item fields for all items…"
|
||||
Item.includes(:swf_assets).find_in_batches.with_index do |items, batch|
|
||||
puts "Updating item batch ##{batch+1}…"
|
||||
Item.transaction do
|
||||
items.each(&:update_cached_fields)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +1,9 @@
|
|||
namespace "neopets:import" do
|
||||
namespace :nc_mall do
|
||||
desc "Sync our NCMallRecord table with the live NC Mall"
|
||||
task :nc_mall => :environment do
|
||||
task :sync => :environment do
|
||||
# Log to STDOUT.
|
||||
Rails.logger = Logger.new(STDOUT)
|
||||
|
||||
puts "Importing from NC Mall…"
|
||||
|
||||
# First, load all records of what's being sold in the live NC Mall. We load
|
||||
# the homepage and all pages linked from the main document, and extract the
|
||||
# items from each. (We also de-duplicate the items, which is important
|
||||
|
@ -85,10 +83,15 @@ def load_all_nc_mall_pages
|
|||
links = Neopets::NCMall.load_page_links
|
||||
|
||||
# Next, load the linked pages, 10 at a time.
|
||||
linked_page_tasks = DTIRequests.load_many(max_at_once: 10) do |task|
|
||||
links.map do |link|
|
||||
task.async { Neopets::NCMall.load_page link[:type], link[:cat] }
|
||||
barrier = Async::Barrier.new
|
||||
semaphore = Async::Semaphore.new(10, parent: barrier)
|
||||
begin
|
||||
linked_page_tasks = links.map do |link|
|
||||
semaphore.async { Neopets::NCMall.load_page link[:type], link[:cat] }
|
||||
end
|
||||
barrier.wait # Load all the pages.
|
||||
ensure
|
||||
barrier.stop # If any pages failed, cancel the rest.
|
||||
end
|
||||
|
||||
# Finally, return all the pages: the homepage, and the linked pages.
|
|
@ -1,31 +0,0 @@
|
|||
module Neologin
|
||||
def self.cookie
|
||||
raise "must run neopets:import:neologin first" if @cookie.nil?
|
||||
@cookie
|
||||
end
|
||||
|
||||
def self.cookie?
|
||||
@cookie.present?
|
||||
end
|
||||
|
||||
def self.cookie=(new_cookie)
|
||||
@cookie = new_cookie
|
||||
end
|
||||
end
|
||||
|
||||
namespace :neopets do
|
||||
task :import => [
|
||||
"neopets:import:neologin",
|
||||
"neopets:import:nc_mall",
|
||||
"neopets:import:rainbow_pool",
|
||||
"neopets:import:styling_studio",
|
||||
]
|
||||
|
||||
namespace :import do
|
||||
task :neologin do
|
||||
unless Neologin.cookie?
|
||||
Neologin.cookie = STDIN.getpass("Neologin cookie: ")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,80 +0,0 @@
|
|||
namespace "neopets:import" do
|
||||
desc "Import alt style info from the NC Styling Studio"
|
||||
task :styling_studio => ["neopets:import:neologin", :environment] do
|
||||
puts "Importing from Styling Studio…"
|
||||
|
||||
all_species = Species.order(:name).to_a
|
||||
|
||||
# Load 10 species pages from the NC Mall at a time.
|
||||
styles_by_species_id = {}
|
||||
DTIRequests.load_many(max_at_once: 10) do |task|
|
||||
num_loaded = 0
|
||||
num_total = all_species.size
|
||||
print "0/#{num_total} species loaded"
|
||||
|
||||
all_species.each do |species|
|
||||
task.async {
|
||||
begin
|
||||
styles_by_species_id[species.id] = Neopets::NCMall.load_styles(
|
||||
species_id: species.id,
|
||||
neologin: Neologin.cookie,
|
||||
)
|
||||
rescue => error
|
||||
puts "\n⚠️ Error loading for #{species.human_name}, skipping: #{error.message}"
|
||||
end
|
||||
num_loaded += 1
|
||||
print "\r#{num_loaded}/#{num_total} species loaded"
|
||||
}
|
||||
end
|
||||
end
|
||||
print "\n"
|
||||
|
||||
style_ids = styles_by_species_id.values.flatten(1).map { |s| s[:oii] }
|
||||
style_records_by_id =
|
||||
AltStyle.where(id: style_ids).to_h { |as| [as.id, as] }
|
||||
|
||||
all_species.each do |species|
|
||||
styles = styles_by_species_id[species.id]
|
||||
next if styles.nil?
|
||||
|
||||
counts = {changed: 0, unchanged: 0, skipped: 0}
|
||||
styles.each do |style|
|
||||
record = style_records_by_id[style[:oii]]
|
||||
label = "#{style[:name]} (#{style[:oii]})"
|
||||
if record.nil?
|
||||
puts "⚠️ [#{label}]: Not modeled yet, skipping"
|
||||
counts[:skipped] += 1
|
||||
next
|
||||
end
|
||||
|
||||
if !record.real_thumbnail_url?
|
||||
record.thumbnail_url = style[:image]
|
||||
puts "✅ [#{label}]: Thumbnail URL is now #{style[:image].inspect}"
|
||||
elsif record.thumbnail_url != style[:image]
|
||||
puts "⚠️ [#{label}: Thumbnail URL may have changed, handle manually? " +
|
||||
"#{record.thumbnail_url.inspect} -> #{style[:image].inspect}"
|
||||
end
|
||||
|
||||
new_series_name = style[:name].match(/\A\S+/)[0] # first word
|
||||
if !record.real_series_name?
|
||||
record.series_name = new_series_name
|
||||
puts "✅ [#{label}]: Series name is now #{new_series_name.inspect}"
|
||||
elsif record.series_name != new_series_name
|
||||
puts "⚠️ [#{label}: Series name may have changed, handle manually? " +
|
||||
"#{record.series_name.inspect} -> #{new_series_name.inspect}"
|
||||
end
|
||||
|
||||
if record.changed?
|
||||
counts[:changed] += 1
|
||||
else
|
||||
counts[:unchanged] += 1
|
||||
end
|
||||
|
||||
record.save!
|
||||
end
|
||||
|
||||
puts "#{species.human_name}: #{counts[:changed]} changed, " +
|
||||
"#{counts[:unchanged]} unchanged, #{counts[:skipped]} skipped"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,39 +1,26 @@
|
|||
require "addressable/template"
|
||||
require "async/http/internet/instance"
|
||||
|
||||
namespace "neopets:import" do
|
||||
namespace :rainbow_pool do
|
||||
desc "Import all basic image hashes from the Rainbow Pool, onto PetTypes"
|
||||
task :rainbow_pool => ["neopets:import:neologin", :environment] do
|
||||
puts "Importing from Rainbow Pool…"
|
||||
task :import => :environment do
|
||||
neologin = STDIN.getpass("Neologin cookie: ")
|
||||
|
||||
all_species = Species.order(:name).to_a
|
||||
all_pet_types = PetType.all.to_a
|
||||
all_pet_types_by_species_id_and_color_id = all_pet_types.
|
||||
to_h { |pt| [[pt.species_id, pt.color_id], pt] }
|
||||
all_colors_by_name = Color.all.to_h { |c| [c.human_name.downcase, c] }
|
||||
|
||||
hashes_by_color_name_by_species_id = {}
|
||||
DTIRequests.load_many(max_at_once: 10) do |task|
|
||||
num_loaded = 0
|
||||
num_total = all_species.size
|
||||
print "0/#{num_total} species loaded"
|
||||
|
||||
all_species.each do |species|
|
||||
task.async do
|
||||
begin
|
||||
hashes_by_color_name_by_species_id[species.id] =
|
||||
RainbowPool.load_hashes_for_species(species.id, Neologin.cookie)
|
||||
rescue => error
|
||||
puts "Failed to load #{species.name} page, skipping: #{error.message}"
|
||||
end
|
||||
num_loaded += 1
|
||||
print "\r#{num_loaded}/#{num_total} species loaded"
|
||||
end
|
||||
# TODO: Do these in parallel? I set up the HTTP requests to be able to
|
||||
# handle it, and just didn't set up the rest of the code for it, lol
|
||||
Species.order(:name).each do |species|
|
||||
begin
|
||||
hashes_by_color_name = RainbowPool.load_hashes_for_species(
|
||||
species.id, neologin)
|
||||
rescue => error
|
||||
puts "Failed to load #{species.name} page, skipping: #{error.message}"
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
all_species.each do |species|
|
||||
hashes_by_color_name = hashes_by_color_name_by_species_id[species.id]
|
||||
next if hashes_by_color_name.nil?
|
||||
|
||||
changed_pet_types = []
|
||||
|
||||
|
@ -73,6 +60,10 @@ namespace "neopets:import" do
|
|||
end
|
||||
|
||||
module RainbowPool
|
||||
# Share a pool of persistent connections, rather than reconnecting on
|
||||
# each request. (This library does that automatically!)
|
||||
INTERNET = Async::HTTP::Internet.instance
|
||||
|
||||
class << self
|
||||
SPECIES_PAGE_URL_TEMPLATE = Addressable::Template.new(
|
||||
"https://www.neopets.com/pool/all_pb.phtml{?f_species_id}"
|
||||
|
@ -80,10 +71,10 @@ module RainbowPool
|
|||
def load_hashes_for_species(species_id, neologin)
|
||||
Sync do
|
||||
url = SPECIES_PAGE_URL_TEMPLATE.expand(f_species_id: species_id)
|
||||
DTIRequests.get(
|
||||
url,
|
||||
[["Cookie", "neologin=#{neologin}"]],
|
||||
) do |response|
|
||||
INTERNET.get(url, [
|
||||
["User-Agent", Rails.configuration.user_agent_for_neopets],
|
||||
["Cookie", "neologin=#{neologin}"],
|
||||
]) do |response|
|
||||
if response.status != 200
|
||||
raise "expected status 200 but got #{response.status} (#{url})"
|
||||
end
|
19
spec/fixtures/colors.yml
vendored
19
spec/fixtures/colors.yml
vendored
|
@ -1,28 +1,9 @@
|
|||
blue:
|
||||
id: 8
|
||||
name: blue
|
||||
basic: true
|
||||
green:
|
||||
id: 34
|
||||
name: green
|
||||
basic: true
|
||||
maraquan:
|
||||
id: 44
|
||||
name: maraquan
|
||||
standard: false
|
||||
purple:
|
||||
id: 57
|
||||
name: purple
|
||||
red:
|
||||
id: 61
|
||||
name: red
|
||||
basic: true
|
||||
robot:
|
||||
id: 62
|
||||
name: robot
|
||||
striped:
|
||||
id: 77
|
||||
name: striped
|
||||
swamp_gas:
|
||||
id: 93
|
||||
name: "swamp gas"
|
||||
|
|
30
spec/fixtures/items.yml
vendored
30
spec/fixtures/items.yml
vendored
|
@ -1,30 +0,0 @@
|
|||
straw_hat:
|
||||
id: 58
|
||||
name: Straw Hat
|
||||
description: "This straw hat will keep the sun out of your pets eyes in
|
||||
bright sunlight."
|
||||
thumbnail_url: https://images.neopets.com/items/straw-hat.gif
|
||||
type: Clothes
|
||||
category: Clothes
|
||||
rarity: Very Rare
|
||||
rarity_index: 90
|
||||
price: 376
|
||||
weight_lbs: 1
|
||||
zones_restrict: "0000000000000000000000000001000000001010000000000000"
|
||||
species_support_ids: "35"
|
||||
created_at: "2011-03-28T14:33:36-07:00"
|
||||
|
||||
birthday_bg:
|
||||
id: 89876
|
||||
name: Birthday Bash Background
|
||||
description: This place is all set for a brilliant birthday bash!
|
||||
thumbnail_url: https://images.neopets.com/items/9a4gd6g6c0.gif
|
||||
type: none
|
||||
category: None
|
||||
rarity: Special
|
||||
rarity_index: 101
|
||||
price: 0
|
||||
weight_lbs: 1
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000"
|
||||
species_support_ids: ""
|
||||
created_at: "2024-11-15T18:15:22-08:00"
|
19
spec/fixtures/pet_types.yml
vendored
19
spec/fixtures/pet_types.yml
vendored
|
@ -1,19 +0,0 @@
|
|||
blue_acara:
|
||||
color_id: 8
|
||||
species_id: 1
|
||||
body_id: 123
|
||||
|
||||
newcolor_acara:
|
||||
color_id: 123
|
||||
species_id: 1
|
||||
body_id: 123
|
||||
|
||||
blue_newspecies:
|
||||
color_id: 8
|
||||
species_id: 456
|
||||
body_id: 123
|
||||
|
||||
newcolor_newspecies:
|
||||
color_id: 123
|
||||
species_id: 456
|
||||
body_id: 123
|
9
spec/fixtures/species.yml
vendored
9
spec/fixtures/species.yml
vendored
|
@ -1,6 +1,3 @@
|
|||
acara:
|
||||
id: 1
|
||||
name: acara
|
||||
blumaroo:
|
||||
id: 3
|
||||
name: blumaroo
|
||||
|
@ -10,9 +7,3 @@ chia:
|
|||
jetsam:
|
||||
id: 20
|
||||
name: jetsam
|
||||
mynci:
|
||||
id: 35
|
||||
name: mynci
|
||||
vandagyre:
|
||||
id: 55
|
||||
name: vandagyre
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe Color do
|
||||
fixtures :colors
|
||||
|
||||
describe '#to_param' do
|
||||
it("uses name when possible") do
|
||||
expect(colors(:blue).to_param).to eq "Blue"
|
||||
end
|
||||
|
||||
it("uses spaces for multi-word names") do
|
||||
expect(colors(:swamp_gas).to_param).to eq "Swamp Gas"
|
||||
end
|
||||
|
||||
it("uses IDs for new colors") do
|
||||
expect(Color.new(id: 12345).to_param).to eq "12345"
|
||||
end
|
||||
end
|
||||
|
||||
describe ".param_to_id" do
|
||||
it("looks up by name") do
|
||||
expect(Color.param_to_id("blue")).to eq colors(:blue).id
|
||||
end
|
||||
|
||||
it("is case-insensitive for name") do
|
||||
expect(Color.param_to_id("bLUe")).to eq colors(:blue).id
|
||||
end
|
||||
|
||||
it("returns ID when the param is just a number, even if it doesn't exist") do
|
||||
expect(Color.param_to_id("123456")).to eq 123456
|
||||
end
|
||||
|
||||
it("raises RecordNotFound if no name matches") do
|
||||
expect { Color.param_to_id("nonexistant") }.
|
||||
to raise_error ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,276 +0,0 @@
|
|||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe Item do
|
||||
fixtures :items, :colors, :species, :zones
|
||||
|
||||
context "modeling status:" do
|
||||
# Rather than using fixtures of real-world data, we create very specific
|
||||
# pet types, to be able to create small encapsulated test cases where there
|
||||
# are only a few bodies.
|
||||
#
|
||||
# We create some basic color pet types, and some Maraquan pet types—and,
|
||||
# just like irl, the Maraquan Mynci has the same body as the basic Mynci.
|
||||
#
|
||||
# These pet types default to an early creation date of 2005, except the
|
||||
# Vandagyre, which was released in 2014.
|
||||
before do
|
||||
PetType.destroy_all # Make sure no leftovers from e.g. PetType's spec!
|
||||
|
||||
build_pt(colors(:blue), species(:acara), body_id: 1).save!
|
||||
build_pt(colors(:red), species(:acara), body_id: 1).save!
|
||||
build_pt(colors(:blue), species(:blumaroo), body_id: 2).save!
|
||||
build_pt(colors(:green), species(:chia), body_id: 3).save!
|
||||
build_pt(colors(:red), species(:mynci), body_id: 4).save!
|
||||
build_pt(colors(:blue), species(:vandagyre), body_id: 5).tap do |pt|
|
||||
pt.created_at = Date.new(2014, 11, 14)
|
||||
pt.save!
|
||||
end
|
||||
|
||||
build_pt(colors(:maraquan), species(:acara), body_id: 11).save!
|
||||
build_pt(colors(:maraquan), species(:blumaroo), body_id: 12).save!
|
||||
build_pt(colors(:maraquan), species(:chia), body_id: 13).save!
|
||||
build_pt(colors(:maraquan), species(:mynci), body_id: 4).save!
|
||||
end
|
||||
|
||||
def build_pt(color, species, body_id:)
|
||||
PetType.new(color:, species:, body_id:, created_at: Time.new(2005))
|
||||
end
|
||||
|
||||
def build_item_asset(zone, body_id:)
|
||||
@remote_id = (@remote_id || 0) + 1
|
||||
url = "https://images.neopets.example/#{@remote_id}.swf"
|
||||
SwfAsset.new(type: "object", remote_id: @remote_id, url:,
|
||||
zones_restrict: "", zone:, body_id:)
|
||||
end
|
||||
|
||||
shared_examples "a fully-modeled item" do
|
||||
it("is considered fully modeled") { should be_predicted_fully_modeled }
|
||||
it("predicts no more compatible bodies") do
|
||||
expect(item.predicted_missing_body_ids).to be_empty
|
||||
end
|
||||
it("appears in Item.is_modeled") do
|
||||
expect(Item.is_modeled.find_by_id(item.id)).to be_present
|
||||
end
|
||||
it("does not appear in Item.is_not_modeled") do
|
||||
expect(Item.is_not_modeled.find_by_id(item.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "a not-fully-modeled item" do
|
||||
it("is not fully modeled") { should_not be_predicted_fully_modeled }
|
||||
it("does not appear in Item.is_modeled") do
|
||||
expect(Item.is_modeled.find_by_id(item.id)).to be_nil
|
||||
end
|
||||
it("appears in Item.is_not_modeled") do
|
||||
expect(Item.is_not_modeled.find_by_id(item.id)).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item without any modeling data" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
it_behaves_like "a not-fully-modeled item"
|
||||
it("has no compatible body IDs") do
|
||||
expect(item.compatible_body_ids).to be_empty
|
||||
end
|
||||
it("predicts all standard bodies are compatible") do
|
||||
expect(item.predicted_missing_body_ids).to contain_exactly(
|
||||
1, 2, 3, 4, 5)
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item with one species modeled" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||
end
|
||||
|
||||
it_behaves_like "a fully-modeled item"
|
||||
it("has one compatible body ID") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item with two species modeled" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||
end
|
||||
|
||||
it_behaves_like "a not-fully-modeled item"
|
||||
it("has two compatible body IDs") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(1, 2)
|
||||
end
|
||||
it("predicts remaining standard bodies are compatible") do
|
||||
expect(item.predicted_missing_body_ids).to contain_exactly(3, 4, 5)
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item with all standard species modeled" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 3)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 4)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 5)
|
||||
end
|
||||
|
||||
it_behaves_like "a fully-modeled item"
|
||||
it("is compatible with all standard body IDs") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(1, 2, 3, 4, 5)
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item that fits all pets the same" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:background), body_id: 0)
|
||||
end
|
||||
|
||||
it_behaves_like "a fully-modeled item"
|
||||
it("is compatible with all bodies (body ID = 0)") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item with one Maraquan pet modeled" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
||||
end
|
||||
|
||||
it_behaves_like "a fully-modeled item"
|
||||
it("has one compatible body ID") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(11)
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item with two Maraquan pets modeled" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 12)
|
||||
end
|
||||
|
||||
it_behaves_like "a not-fully-modeled item"
|
||||
it("has two compatible body IDs") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(11, 12)
|
||||
end
|
||||
it("predicts remaining Maraquan body IDs are compatible") do
|
||||
expect(item.predicted_missing_body_ids).to contain_exactly(13, 4)
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item with all Maraquan species modeled" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 11)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 12)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 13)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 4)
|
||||
end
|
||||
|
||||
it_behaves_like "a fully-modeled item"
|
||||
it("is compatible with all Maraquan body IDs") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(11, 12, 13, 4)
|
||||
end
|
||||
end
|
||||
|
||||
describe "a pre-Vandagyre item without any modeling data" do
|
||||
subject(:item) { items(:straw_hat) }
|
||||
|
||||
it_behaves_like "a not-fully-modeled item"
|
||||
it("has no compatible body IDs") do
|
||||
expect(item.compatible_body_ids).to be_empty
|
||||
end
|
||||
it("predicts all standard bodies except Vandagyre are compatible") do
|
||||
expect(item.predicted_missing_body_ids).to contain_exactly(1, 2, 3, 4)
|
||||
end
|
||||
end
|
||||
|
||||
# Skipping "pre-Vanda with one species modeled", because it's identical.
|
||||
|
||||
describe "a pre-Vandagyre item with two species modeled" do
|
||||
subject(:item) { items(:straw_hat) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||
end
|
||||
|
||||
it_behaves_like "a not-fully-modeled item"
|
||||
it("has two compatible body IDs") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(1, 2)
|
||||
end
|
||||
it("predicts remaining standard bodies (sans Vandagyre) are compatible") do
|
||||
expect(item.predicted_missing_body_ids).to contain_exactly(3, 4)
|
||||
end
|
||||
end
|
||||
|
||||
describe "a pre-Vandagyre item with all other standard species modeled" do
|
||||
subject(:item) { items(:straw_hat) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 3)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 4)
|
||||
end
|
||||
|
||||
it_behaves_like "a fully-modeled item"
|
||||
it("is compatible with all non-Vandagyre standard body IDs") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(1, 2, 3, 4)
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item without any modeling data, but hinted as done" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before { item.update!(modeling_status_hint: :done) }
|
||||
|
||||
it_behaves_like "a fully-modeled item"
|
||||
it("has no compatible body IDs") do
|
||||
expect(item.compatible_body_ids).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item with two species modeled, but hinted as done" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||
item.update!(modeling_status_hint: :done)
|
||||
end
|
||||
|
||||
it_behaves_like "a fully-modeled item"
|
||||
it("has two compatible body IDs") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(1, 2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "an item with two species modeled, but hinted as glitchy" do
|
||||
subject(:item) { items(:birthday_bg) }
|
||||
|
||||
before do
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 1)
|
||||
item.swf_assets << build_item_asset(zones(:wings), body_id: 2)
|
||||
item.update!(modeling_status_hint: :glitchy)
|
||||
end
|
||||
|
||||
it_behaves_like "a fully-modeled item"
|
||||
it("has two compatible body IDs") do
|
||||
expect(item.compatible_body_ids).to contain_exactly(1, 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
require_relative '../rails_helper'
|
||||
require 'rails_helper'
|
||||
require_relative '../support/mocks/custom_pets'
|
||||
require_relative '../support/matchers/a_record_matching'
|
||||
|
||||
|
@ -32,57 +32,23 @@ RSpec.describe Pet, type: :model do
|
|||
it("is saved when saving the pet") { pet.save!; should be_persisted }
|
||||
|
||||
describe "its biology assets" do
|
||||
# TODO: I wish biology assets were set up before saving.
|
||||
# Once we change this, we can un-mark some tests as pending.
|
||||
before { pet.save! }
|
||||
|
||||
subject(:biology_assets) { pet_state.swf_assets }
|
||||
let(:asset_ids) { biology_assets.map(&:remote_id) }
|
||||
|
||||
they("are all new") { should all be_new_record }
|
||||
they("match the expected IDs (before saving)") do
|
||||
expect(asset_ids).to contain_exactly(10083, 11613, 14187, 14189)
|
||||
they("are all new") do
|
||||
pending("Currently, pets must be saved before assets are assigned.")
|
||||
should all be_new_record
|
||||
end
|
||||
they("match the expected IDs (after saving)") do
|
||||
pet.save! # TODO: Remove this test once the above passes.
|
||||
they("match the expected IDs") do
|
||||
expect(asset_ids).to contain_exactly(10083, 11613, 14187, 14189)
|
||||
end
|
||||
they("are saved when saving the pet") { pet.save!; should all be_persisted }
|
||||
they("have the expected asset metadata (before saving)") do
|
||||
should contain_exactly(
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 10083,
|
||||
zone_id: 37,
|
||||
url: "https://images.neopets.com/cp/bio/swf/000/000/010/10083_8a1111a13f.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/bio/data/000/000/010/10083_8a1111a13f/manifest.json",
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000",
|
||||
),
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 11613,
|
||||
zone_id: 15,
|
||||
url: "https://images.neopets.com/cp/bio/swf/000/000/011/11613_f7d8d377ab.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/bio/data/000/000/011/11613_f7d8d377ab/manifest.json",
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000",
|
||||
),
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 14187,
|
||||
zone_id: 34,
|
||||
url: "https://images.neopets.com/cp/bio/swf/000/000/014/14187_0e65c2082f.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/bio/data/000/000/014/14187_0e65c2082f/manifest.json",
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000",
|
||||
),
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 14189,
|
||||
zone_id: 33,
|
||||
url: "https://images.neopets.com/cp/bio/swf/000/000/014/14189_102e4991e9.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/bio/data/000/000/014/14189_102e4991e9/manifest.json",
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000",
|
||||
)
|
||||
)
|
||||
end
|
||||
they("have the expected asset metadata (after saving)") do
|
||||
pet.save! # TODO: Remove this test once the above passes.
|
||||
should contain_exactly(
|
||||
they("have the expected asset metadata") do
|
||||
expect(pet_state.swf_assets).to contain_exactly(
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 10083,
|
||||
|
@ -130,7 +96,7 @@ RSpec.describe Pet, type: :model do
|
|||
it("already exists") { should be_persisted }
|
||||
it("is the same as before") { should eq pet.pet_type }
|
||||
it "is not changed when saving the pet" do
|
||||
new_pet.save!; expect(pet_type.previous_changes).to be_empty
|
||||
expect { new_pet.save! }.not_to change { pet_type.attributes }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -140,7 +106,7 @@ RSpec.describe Pet, type: :model do
|
|||
it("already exists") { should be_persisted }
|
||||
it("is the same as before") { should eq pet.pet_state }
|
||||
it "is not changed when saving the pet" do
|
||||
new_pet.save!; expect(pet_state.previous_changes).to be_empty
|
||||
expect { new_pet.save! }.not_to change { pet_state.attributes }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -150,7 +116,7 @@ RSpec.describe Pet, type: :model do
|
|||
they("already exist") { should all be_persisted }
|
||||
they("are the same as before") { should eq pet.pet_state.swf_assets }
|
||||
they("are not changed when saving the pet") do
|
||||
new_pet.save!; expect(biology_assets.map(&:previous_changes)).to all be_empty
|
||||
expect { new_pet.save! }.not_to change { biology_assets.map(&:attributes) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -165,7 +131,7 @@ RSpec.describe Pet, type: :model do
|
|||
it("already exists") { should be_persisted }
|
||||
it("is the same as before") { should eq pet.pet_type }
|
||||
it "is not changed when saving the pet" do
|
||||
new_pet.save!; expect(pet_type.previous_changes).to be_empty
|
||||
expect { new_pet.save! }.not_to change { pet_type.attributes }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -178,66 +144,23 @@ RSpec.describe Pet, type: :model do
|
|||
it("is saved when saving the pet") { new_pet.save!; should be_persisted }
|
||||
|
||||
describe "its biology assets" do
|
||||
# TODO: I wish biology assets were set up before saving.
|
||||
# Once we change this, we can un-mark some tests as pending.
|
||||
before { new_pet.save! }
|
||||
|
||||
subject(:biology_assets) { pet_state.swf_assets }
|
||||
let(:asset_ids) { biology_assets.map(&:remote_id) }
|
||||
let(:persisted_asset_ids) {
|
||||
biology_assets.select(&:persisted?).map(&:remote_id)
|
||||
}
|
||||
let(:new_asset_ids) {
|
||||
biology_assets.select(&:new_record?).map(&:remote_id)
|
||||
}
|
||||
|
||||
they("are partially new, partially existing") do
|
||||
expect(persisted_asset_ids).to contain_exactly(10083, 11613)
|
||||
expect(new_asset_ids).to contain_exactly(10448, 10451)
|
||||
pending("Currently, pets must be saved before assets are assigned.")
|
||||
fail # TODO: Write this test once we have the ability to see it pass!
|
||||
end
|
||||
they("match the expected IDs (before saving)") do
|
||||
expect(asset_ids).to contain_exactly(10083, 11613, 10448, 10451)
|
||||
end
|
||||
they("match the expected IDs (after saving)") do
|
||||
new_pet.save! # TODO: Remove this test once the above passes.
|
||||
they("match the expected IDs") do
|
||||
expect(asset_ids).to contain_exactly(10083, 11613, 10448, 10451)
|
||||
end
|
||||
they("are saved when saving the pet") { new_pet.save!; should all be_persisted }
|
||||
they("have the expected asset metadata (before saving)") do
|
||||
should contain_exactly(
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 10083,
|
||||
zone_id: 37,
|
||||
url: "https://images.neopets.com/cp/bio/swf/000/000/010/10083_8a1111a13f.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/bio/data/000/000/010/10083_8a1111a13f/manifest.json",
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000",
|
||||
),
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 11613,
|
||||
zone_id: 15,
|
||||
url: "https://images.neopets.com/cp/bio/swf/000/000/011/11613_f7d8d377ab.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/bio/data/000/000/011/11613_f7d8d377ab/manifest.json",
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000",
|
||||
),
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 10448,
|
||||
zone_id: 34,
|
||||
url: "https://images.neopets.com/cp/bio/swf/000/000/010/10448_0b238e79e2.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/bio/data/000/000/010/10448_0b238e79e2/manifest.json",
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000",
|
||||
),
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 10451,
|
||||
zone_id: 33,
|
||||
url: "https://images.neopets.com/cp/bio/swf/000/000/010/10451_cd4a8a8e47.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/bio/data/000/000/010/10451_cd4a8a8e47/manifest.json",
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000",
|
||||
)
|
||||
)
|
||||
end
|
||||
they("have the expected asset metadata (after saving)") do
|
||||
new_pet.save! # TODO: Remove this test once the above passes.
|
||||
should contain_exactly(
|
||||
they("have the expected asset metadata") do
|
||||
expect(pet_state.swf_assets).to contain_exactly(
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 10083,
|
||||
|
@ -285,15 +208,18 @@ RSpec.describe Pet, type: :model do
|
|||
it("is a Striped Blumaroo") { expect(pet.pet_type.human_name).to eq "Striped Blumaroo" }
|
||||
|
||||
describe "its biology assets" do
|
||||
# TODO: I wish biology assets were set up before saving.
|
||||
# Once we change this, we can un-mark some tests as pending.
|
||||
before { pet.save! }
|
||||
|
||||
subject(:biology_assets) { pet.pet_state.swf_assets }
|
||||
let(:asset_ids) { biology_assets.map(&:remote_id) }
|
||||
|
||||
they("are all new") { should all be_new_record }
|
||||
they("match the expected IDs (before saving)") do
|
||||
expect(asset_ids).to contain_exactly(331, 332, 333, 23760, 23411)
|
||||
they("are all new") do
|
||||
pending("Currently, pets must be saved before assets are assigned.")
|
||||
should all be_new_record
|
||||
end
|
||||
they("match the expected IDs (after saving)") do
|
||||
pet.save! # TODO: Remove this test once the above passes.
|
||||
they("match the expected IDs") do
|
||||
expect(asset_ids).to contain_exactly(331, 332, 333, 23760, 23411)
|
||||
end
|
||||
they("are saved when saving the pet") { pet.save!; should all be_persisted }
|
||||
|
@ -302,14 +228,13 @@ RSpec.describe Pet, type: :model do
|
|||
describe "its items" do
|
||||
subject(:items) { pet.items }
|
||||
let(:item_ids) { items.map(&:id) }
|
||||
let(:compatible_body_ids) { items.to_h { |i| [i.id, i.compatible_body_ids] } }
|
||||
|
||||
they("are all new") { should all be_new_record }
|
||||
they("match the expected IDs") do
|
||||
expect(item_ids).to contain_exactly(39552, 53874, 71706)
|
||||
end
|
||||
they("are saved when saving the pet") { pet.save! ; should all be_persisted }
|
||||
they("have the expected item metadata (without even saving first)") do
|
||||
they("have the expected item metadata") do
|
||||
should contain_exactly(
|
||||
a_record_matching(
|
||||
id: 39552,
|
||||
|
@ -355,66 +280,26 @@ RSpec.describe Pet, type: :model do
|
|||
),
|
||||
)
|
||||
end
|
||||
they("should be marked compatible with this pet's body ID") do
|
||||
pet.save!
|
||||
expect(compatible_body_ids).to eq(
|
||||
39552 => [47],
|
||||
53874 => [47],
|
||||
71706 => [0],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "its item assets" do
|
||||
# TODO: I wish item assets were set up before saving.
|
||||
# Once we change this, we can un-mark some tests as pending.
|
||||
before { pet.save! }
|
||||
|
||||
let(:assets_by_item) { pet.items.to_h { |item| [item.id, item.swf_assets.to_a] } }
|
||||
subject(:item_assets) { assets_by_item.values.flatten(1) }
|
||||
let(:asset_ids) { item_assets.map(&:remote_id) }
|
||||
|
||||
they("are all new") { should all be_new_record }
|
||||
pending("match the expected IDs (before saving)") do
|
||||
expect(asset_ids).to contain_exactly(16933, 108567, 410722)
|
||||
they("are all new") do
|
||||
pending("Currently, pets must be saved before assets are assigned.")
|
||||
should all be_new_record
|
||||
end
|
||||
they("match the expected IDs (after saving)") do
|
||||
pet.save! # TODO: Remove this test once the above passes.
|
||||
they("match the expected IDs") do
|
||||
expect(asset_ids).to contain_exactly(16933, 108567, 410722)
|
||||
end
|
||||
they("are saved when saving the pet") { pet.save! ; should all be_persisted }
|
||||
pending("match the expected metadata (before saving)") do
|
||||
expect(assets_by_item).to match(
|
||||
39552 => a_collection_containing_exactly(
|
||||
a_record_matching(
|
||||
type: "object",
|
||||
remote_id: 16933,
|
||||
zone_id: 35,
|
||||
url: "https://images.neopets.com/cp/items/swf/000/000/016/16933_0833353c4f.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/items/data/000/000/016/16933_0833353c4f/manifest.json?v=1706",
|
||||
zones_restrict: "",
|
||||
)
|
||||
),
|
||||
53874 => a_collection_containing_exactly(
|
||||
a_record_matching(
|
||||
type: "object",
|
||||
remote_id: 108567,
|
||||
zone_id: 23,
|
||||
url: "https://images.neopets.com/cp/items/swf/000/000/108/108567_ee88141325.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/items/data/000/000/108/108567_ee88141325/manifest.json?v=1706",
|
||||
zones_restrict: "",
|
||||
)
|
||||
),
|
||||
71706 => a_collection_containing_exactly(
|
||||
a_record_matching(
|
||||
type: "object",
|
||||
remote_id: 410722,
|
||||
zone_id: 3,
|
||||
url: "https://images.neopets.com/cp/items/swf/000/000/410/410722_3bcd2f5e11.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/items/data/000/000/410/410722_3bcd2f5e11/manifest.json?v=1706",
|
||||
zones_restrict: "",
|
||||
)
|
||||
),
|
||||
)
|
||||
end
|
||||
they("match the expected metadata (after saving)") do
|
||||
pet.save! # TODO: Remove this test after the above passes.
|
||||
they("match the expected metadata") do
|
||||
expect(assets_by_item).to match(
|
||||
39552 => a_collection_containing_exactly(
|
||||
a_record_matching(
|
||||
|
@ -460,7 +345,7 @@ RSpec.describe Pet, type: :model do
|
|||
it("already exists") { should be_persisted }
|
||||
it("is the same as before") { should eq pet.pet_type }
|
||||
it "is not changed when saving the pet" do
|
||||
new_pet.save!; expect(pet_type.previous_changes).to be_empty
|
||||
expect { new_pet.save! }.not_to change { pet_type.attributes }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -470,7 +355,7 @@ RSpec.describe Pet, type: :model do
|
|||
it("already exists") { should be_persisted }
|
||||
it("is the same as before") { should eq pet.pet_state }
|
||||
it "is not changed when saving the pet" do
|
||||
new_pet.save!; expect(pet_state.previous_changes).to be_empty
|
||||
expect { new_pet.save! }.not_to change { pet_state.attributes }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -480,7 +365,7 @@ RSpec.describe Pet, type: :model do
|
|||
they("already exist") { should all be_persisted }
|
||||
they("are the same as before") { should eq pet.pet_state.swf_assets }
|
||||
they("are not changed when saving the pet") do
|
||||
new_pet.save!; expect(biology_assets.map(&:previous_changes)).to all be_empty
|
||||
expect { new_pet.save! }.not_to change { biology_assets.map(&:attributes) }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -490,7 +375,7 @@ RSpec.describe Pet, type: :model do
|
|||
they("already exist") { should all be_persisted }
|
||||
they("are the same as before") { should eq pet.items }
|
||||
they("are not changed when saving the pet") do
|
||||
new_pet.save!; expect(items.map(&:previous_changes)).to all be_empty
|
||||
expect { new_pet.save! }.not_to change { items.map(&:attributes) }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -500,26 +385,7 @@ RSpec.describe Pet, type: :model do
|
|||
they("already exist") { should all be_persisted }
|
||||
they("are the same as before") { should eq pet.items.map(&:swf_assets).flatten(1) }
|
||||
they("are not changed when saving the pet") do
|
||||
new_pet.save!; expect(item_assets.map(&:previous_changes)).to all be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when modeled a second time, but as a Blue Acara" do
|
||||
before { pet.save! }
|
||||
subject(:new_pet) { Pet.load("matts_bat:acara") }
|
||||
|
||||
describe "its items" do
|
||||
subject(:items) { new_pet.items }
|
||||
let(:compatible_body_ids) { items.to_h { |i| [i.id, i.compatible_body_ids] } }
|
||||
|
||||
they("should be marked compatible with both pets' body IDs") do
|
||||
new_pet.save!
|
||||
expect(compatible_body_ids).to eq(
|
||||
39552 => [47, 93],
|
||||
53874 => [47, 93],
|
||||
71706 => [0],
|
||||
)
|
||||
expect { new_pet.save! }.not_to change { item_assets.map(&:attributes) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -550,31 +416,10 @@ RSpec.describe Pet, type: :model do
|
|||
it("has no series name yet") { expect(alt_style.real_series_name?).to be false }
|
||||
it("has no thumbnail yet") { expect(alt_style.thumbnail_url?).to be false }
|
||||
it("is saved when saving the pet") { pet.save!; should be_persisted }
|
||||
|
||||
describe "its assets" do
|
||||
subject(:assets) { alt_style.swf_assets }
|
||||
let(:asset_ids) { assets.map(&:remote_id) }
|
||||
|
||||
they("are all new") { should all be_new_record }
|
||||
they("match the expected IDs") do
|
||||
expect(asset_ids).to contain_exactly(56223)
|
||||
end
|
||||
they("are saved when saving the pet") { pet.save!; should all be_persisted }
|
||||
they("have the expected asset metadata") do
|
||||
should contain_exactly(
|
||||
a_record_matching(
|
||||
type: "biology",
|
||||
remote_id: 56223,
|
||||
zone_id: 15,
|
||||
url: "https://images.neopets.com/cp/bio/swf/000/000/056/56223_dc26edc764.swf",
|
||||
manifest_url: "https://images.neopets.com/cp/bio/data/000/000/056/56223_dc26edc764/manifest.json",
|
||||
zones_restrict: "0000000000000000000000000000000000000000000000000000",
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Alt style assets!
|
||||
|
||||
context "when modeled a second time" do
|
||||
before { pet.save! }
|
||||
subject(:new_pet) { Pet.load("Majal_Kita") }
|
||||
|
@ -585,29 +430,11 @@ RSpec.describe Pet, type: :model do
|
|||
it("already exists") { should be_persisted }
|
||||
it("is the same as before") { should eq pet.alt_style }
|
||||
it "is not changed when saving the pet" do
|
||||
new_pet.save!; expect(alt_style.previous_changes).to be_empty
|
||||
end
|
||||
|
||||
describe "its assets" do
|
||||
subject(:assets) { alt_style.swf_assets }
|
||||
|
||||
they("already exist") { should all be_persisted }
|
||||
they("are the same as before") { should eq pet.alt_style.swf_assets }
|
||||
they("are not changed when saving the pet") do
|
||||
new_pet.save!; expect(assets.map(&:previous_changes)).to all be_empty
|
||||
end
|
||||
expect { new_pet.save! }.not_to change { alt_style.attributes }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when modeling is disabled" do
|
||||
before { allow(Rails.configuration).to receive(:modeling_enabled) { false } }
|
||||
|
||||
it("raises an error") do
|
||||
expect { Pet.load("matts_bat") }.to raise_error(Pet::ModelingDisabled)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe PetState do
|
||||
fixtures :colors, :species, :zones
|
||||
|
||||
let(:blue) { colors(:blue) }
|
||||
let(:green) { colors(:green) }
|
||||
let(:red) { colors(:red) }
|
||||
let(:acara) { species(:acara) }
|
||||
|
||||
describe ".next_unlabeled_appearance" do
|
||||
before { PetType.destroy_all }
|
||||
|
||||
def create_sa
|
||||
swf_asset = SwfAsset.create!(
|
||||
type: "biology", remote_id: (SwfAsset.maximum(:remote_id) || 0) + 1,
|
||||
url: "https://images.neopets.example/hello.swf",
|
||||
zone: zones(:body), zones_restrict: [], body_id: 0)
|
||||
end
|
||||
|
||||
def create_pt(color, species, created_at = nil)
|
||||
PetType.create! color:, species:, created_at:,
|
||||
body_id: (PetType.maximum(:body_id) || 0) + 1
|
||||
end
|
||||
|
||||
def create_ps(pet_type, pose, created_at = nil, **options)
|
||||
# HACK: PetStates without any assets don't save correctly.
|
||||
# https://github.com/rails/rails/issues/52340
|
||||
swf_assets = [create_sa]
|
||||
PetState.create! pet_type:, pose:, created_at:, swf_assets:,
|
||||
swf_asset_ids: swf_assets.map(&:id), **options
|
||||
end
|
||||
|
||||
it "returns nil where there are no pet states" do
|
||||
expect(PetState.next_unlabeled_appearance).to be_nil
|
||||
end
|
||||
|
||||
it "returns nil where there are only labeled pet states" do
|
||||
pt = PetType.create! color: blue, species: acara, body_id: 1
|
||||
ps = create_ps(pt, "HAPPY_MASC").tap(&:save!)
|
||||
expect(PetState.next_unlabeled_appearance).to be_nil
|
||||
end
|
||||
|
||||
it "returns the only pet state when it is unlabeled" do
|
||||
pt = PetType.create! color: blue, species: acara, body_id: 1
|
||||
ps = create_ps(pt, "UNKNOWN").tap(&:save!)
|
||||
expect(PetState.next_unlabeled_appearance).to eq ps
|
||||
end
|
||||
|
||||
describe "with multiple unlabeled pet states" do
|
||||
before do
|
||||
# Create three pet types, with ascending order of creation date.
|
||||
@pt1 = create_pt blue, acara, Date.new(2000)
|
||||
@pt2 = create_pt green, acara, Date.new(2005)
|
||||
@pt3 = create_pt red, acara, Date.new(2010)
|
||||
|
||||
# Give each a pet state, but created in a different order.
|
||||
@ps1 = create_ps @pt1, "UNKNOWN", Date.new(2020)
|
||||
@ps2 = create_ps @pt2, "UNKNOWN", Date.new(2025)
|
||||
@ps3 = create_ps @pt3, "UNKNOWN", Date.new(2015)
|
||||
end
|
||||
|
||||
it "returns the latest pet type's pet state" do
|
||||
expect(PetState.next_unlabeled_appearance).to eq @ps3
|
||||
end
|
||||
|
||||
it "excludes fully-labeled pet types" do
|
||||
# Label the latest pet state, then see it move to the next.
|
||||
@ps3.update!(pose: "HAPPY_FEM")
|
||||
expect(PetState.next_unlabeled_appearance).to eq @ps2
|
||||
end
|
||||
|
||||
it "excludes labeled pet states" do
|
||||
# Create an older pet state on the latest pet type, than label the
|
||||
# latest pet state, and see it move back to the older one.
|
||||
ps3_a = create_ps @pt3, "UNKNOWN", Date.new(2014)
|
||||
@ps3.update!(pose: "HAPPY_FEM")
|
||||
expect(PetState.next_unlabeled_appearance).to eq ps3_a
|
||||
end
|
||||
|
||||
it "sorts pet states within the latest pet type by newest" do
|
||||
# Create a few pet types on the latest pet type, and see that we get
|
||||
# the latest back.
|
||||
ps3_a = create_ps @pt3, "UNKNOWN", Date.new(2016)
|
||||
ps3_b = create_ps @pt3, "UNKNOWN", Date.new(2017)
|
||||
ps3_c = create_ps @pt3, "UNKNOWN", Date.new(2018)
|
||||
ps3_d = create_ps @pt3, "UNKNOWN", Date.new(2019)
|
||||
expect(PetState.next_unlabeled_appearance).to eq ps3_d
|
||||
end
|
||||
|
||||
it "can find the next after the latest pet state" do
|
||||
expect(PetState.next_unlabeled_appearance(after_id: @ps3.id)).to eq @ps2
|
||||
end
|
||||
|
||||
it "can find the next after any given pet state" do
|
||||
expect(PetState.next_unlabeled_appearance(after_id: @ps2.id)).to eq @ps1
|
||||
end
|
||||
|
||||
it "can find the next after the latest pet state, even within the same pet type" do
|
||||
ps3_a = create_ps @pt3, "UNKNOWN", Date.new(2014)
|
||||
expect(PetState.next_unlabeled_appearance(after_id: @ps3.id)).to eq ps3_a
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,41 +0,0 @@
|
|||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe PetType do
|
||||
fixtures :colors, :species, :pet_types
|
||||
|
||||
describe '#to_param' do
|
||||
it('uses color and species name when possible ("Blue-Acara")') do
|
||||
expect(pet_types(:blue_acara).to_param).to eq "Blue-Acara"
|
||||
end
|
||||
|
||||
it('uses color ID for new colors (123-Acara)') do
|
||||
expect(pet_types(:newcolor_acara).to_param).to eq "123-Acara"
|
||||
end
|
||||
|
||||
it('uses species ID for new colors (Blue-456)') do
|
||||
expect(pet_types(:blue_newspecies).to_param).to eq "Blue-456"
|
||||
end
|
||||
|
||||
it('uses color ID and species ID when both are new (123-456)') do
|
||||
expect(pet_types(:newcolor_newspecies).to_param).to eq "123-456"
|
||||
end
|
||||
end
|
||||
|
||||
describe ".find_by_param!" do
|
||||
it('looks up by species and color name ("Blue-Acara")') do
|
||||
expect(PetType.find_by_param!("Blue-Acara")).to eq pet_types(:blue_acara)
|
||||
end
|
||||
|
||||
it('looks up by color ID for new colors ("123-Acara")') do
|
||||
expect(PetType.find_by_param!("123-Acara")).to eq pet_types(:newcolor_acara)
|
||||
end
|
||||
|
||||
it('looks up by species ID for new species ("Blue-456")') do
|
||||
expect(PetType.find_by_param!("Blue-456")).to eq pet_types(:blue_newspecies)
|
||||
end
|
||||
|
||||
it('looks up by color ID and species ID when both are new ("123-456")') do
|
||||
expect(PetType.find_by_param!("123-456")).to eq pet_types(:newcolor_newspecies)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,34 +0,0 @@
|
|||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe Species do
|
||||
fixtures :species
|
||||
|
||||
describe '#to_param' do
|
||||
it("uses name when possible") do
|
||||
expect(species(:acara).to_param).to eq "Acara"
|
||||
end
|
||||
|
||||
it("uses IDs for new species") do
|
||||
expect(Species.new(id: 12345).to_param).to eq "12345"
|
||||
end
|
||||
end
|
||||
|
||||
describe ".param_to_id" do
|
||||
it("looks up by name") do
|
||||
expect(Species.param_to_id("acara")).to eq species(:acara).id
|
||||
end
|
||||
|
||||
it("is case-insensitive for name") do
|
||||
expect(Species.param_to_id("aCaRa")).to eq species(:acara).id
|
||||
end
|
||||
|
||||
it("returns ID when the param is just a number, even if no record exists") do
|
||||
expect(Species.param_to_id("123456")).to eq 123456
|
||||
end
|
||||
|
||||
it("raises RecordNotFound if no name matches") do
|
||||
expect { Species.param_to_id("nonexistant") }.
|
||||
to raise_error ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,90 +0,0 @@
|
|||
require 'webmock/rspec'
|
||||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe Neopets::NCMall, type: :model do
|
||||
describe ".load_styles" do
|
||||
def stub_styles_request
|
||||
stub_request(:post, "https://www.neopets.com/np-templates/ajax/stylingstudio/studio.php").
|
||||
with(
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Cookie": "neologin=STUB_NEOLOGIN",
|
||||
"User-Agent": Rails.configuration.user_agent_for_neopets,
|
||||
},
|
||||
body: "mode=getStyles&species=2&tab=1",
|
||||
)
|
||||
end
|
||||
|
||||
subject(:styles) do
|
||||
Neopets::NCMall.load_styles(
|
||||
species_id: 2,
|
||||
neologin: "STUB_NEOLOGIN",
|
||||
)
|
||||
end
|
||||
|
||||
it "loads current NC styles from the NC Mall" do
|
||||
stub_styles_request.to_return(
|
||||
body: '{"success":true,"styles":{"87966":{"oii":87966,"name":"Nostalgic Alien Aisha","image":"https:\/\/images.neopets.com\/items\/nostalgic_alien_aisha.gif","limited":false},"87481":{"oii":87481,"name":"Nostalgic Sponge Aisha","image":"https:\/\/images.neopets.com\/items\/nostalgic_sponge_aisha.gif","limited":false},"90031":{"oii":90031,"name":"Celebratory Anniversary Aisha","image":"https:\/\/images.neopets.com\/items\/624dc08bcf.gif","limited":true},"90050":{"oii":90050,"name":"Nostalgic Tyrannian Aisha","image":"https:\/\/images.neopets.com\/items\/b225e06541.gif","limited":true}}}',
|
||||
)
|
||||
|
||||
expect(styles).to contain_exactly(
|
||||
{
|
||||
oii: 87481,
|
||||
name: "Nostalgic Sponge Aisha",
|
||||
image: "https://images.neopets.com/items/nostalgic_sponge_aisha.gif",
|
||||
limited: false,
|
||||
},
|
||||
{
|
||||
oii: 87966,
|
||||
name: "Nostalgic Alien Aisha",
|
||||
image: "https://images.neopets.com/items/nostalgic_alien_aisha.gif",
|
||||
limited: false,
|
||||
},
|
||||
{
|
||||
oii: 90031,
|
||||
name: "Celebratory Anniversary Aisha",
|
||||
image: "https://images.neopets.com/items/624dc08bcf.gif",
|
||||
limited: true,
|
||||
},
|
||||
{
|
||||
oii: 90050,
|
||||
name: "Nostalgic Tyrannian Aisha",
|
||||
image: "https://images.neopets.com/items/b225e06541.gif",
|
||||
limited: true,
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it "handles the NC Mall's odd API behavior for zero styles" do
|
||||
stub_styles_request.to_return(
|
||||
# You'd think styles would be `{}` in this case, but it's `[]`. Huh!
|
||||
body: '{"success":true,"styles":[]}',
|
||||
)
|
||||
|
||||
expect(styles).to be_empty
|
||||
end
|
||||
|
||||
it "raises an error if the request returns a non-200 status" do
|
||||
stub_styles_request.to_return(status: 400)
|
||||
|
||||
expect { styles }.to raise_error(Neopets::NCMall::ResponseNotOK)
|
||||
end
|
||||
|
||||
it "raises an error if the request returns a non-JSON response" do
|
||||
stub_styles_request.to_return(
|
||||
body: "Oops, this request failed for some weird reason!",
|
||||
)
|
||||
|
||||
expect { styles }.to raise_error(Neopets::NCMall::UnexpectedResponseFormat)
|
||||
end
|
||||
|
||||
it "raises an error if the request returns unexpected JSON" do
|
||||
stub_styles_request.to_return(
|
||||
body: '{"success": false}',
|
||||
)
|
||||
|
||||
expect { styles }.to raise_error(Neopets::NCMall::UnexpectedResponseFormat)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,194 +0,0 @@
|
|||
{
|
||||
"dti_comment": "This is matts_bat, hand-modified to be a Blue Acara wearing the same items.",
|
||||
"custom_pet": {
|
||||
"name": "matts_bat",
|
||||
"owner": "matchu1993",
|
||||
"slot": 1.0,
|
||||
"scale": 0.5,
|
||||
"muted": true,
|
||||
"body_id": 93.0,
|
||||
"species_id": 1.0,
|
||||
"color_id": 8.0,
|
||||
"alt_style": false,
|
||||
"alt_color": 8.0,
|
||||
"style_closet_id": null,
|
||||
"biology_by_zone": {
|
||||
"30": {
|
||||
"part_id": 32185.0,
|
||||
"zone_id": 30.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/032/32185_dc8f076ae3.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/032/32185_dc8f076ae3/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"15": {
|
||||
"part_id": 2425.0,
|
||||
"zone_id": 15.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/002/2425_501f596cef.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/002/2425_501f596cef/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"5": {
|
||||
"part_id": 2426.0,
|
||||
"zone_id": 5.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/002/2426_898928db88.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/002/2426_898928db88/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"38": {
|
||||
"part_id": 2427.0,
|
||||
"zone_id": 38.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/002/2427_f12853f18a.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/002/2427_f12853f18a/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"34": {
|
||||
"part_id": 19157.0,
|
||||
"zone_id": 34.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/019/19157_f2e42f30e9.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/019/19157_f2e42f30e9/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"33": {
|
||||
"part_id": 18945.0,
|
||||
"zone_id": 33.0,
|
||||
"asset_url": "https://images.neopets.com/cp/bio/swf/000/000/018/18945_45623865d6.swf",
|
||||
"manifest": "https://images.neopets.com/cp/bio/data/000/000/018/18945_45623865d6/manifest.json",
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
"equipped_by_zone": {
|
||||
"35": {
|
||||
"asset_id": 16931.0,
|
||||
"zone_id": 35.0,
|
||||
"closet_obj_id": 2549145.0
|
||||
},
|
||||
"23": {
|
||||
"asset_id": 108565.0,
|
||||
"zone_id": 23.0,
|
||||
"closet_obj_id": 16955628.0
|
||||
},
|
||||
"3": {
|
||||
"asset_id": 410722.0,
|
||||
"zone_id": 3.0,
|
||||
"closet_obj_id": 17147987.0
|
||||
}
|
||||
},
|
||||
"original_biology": [
|
||||
|
||||
]
|
||||
},
|
||||
"closet_items": {
|
||||
"2549145": {
|
||||
"closet_obj_id": 2549145.0,
|
||||
"obj_info_id": 39552.0,
|
||||
"applied_to": "matts_bat",
|
||||
"is_wishlist": false,
|
||||
"expiration": "N/A"
|
||||
},
|
||||
"16955628": {
|
||||
"closet_obj_id": 16955628.0,
|
||||
"obj_info_id": 53874.0,
|
||||
"applied_to": "matts_bat",
|
||||
"is_wishlist": false,
|
||||
"expiration": "N/A"
|
||||
},
|
||||
"17147987": {
|
||||
"closet_obj_id": 17147987.0,
|
||||
"obj_info_id": 71706.0,
|
||||
"applied_to": "matts_bat",
|
||||
"is_wishlist": false,
|
||||
"expiration": "N/A"
|
||||
}
|
||||
},
|
||||
"object_info_registry": {
|
||||
"39552": {
|
||||
"obj_info_id": 39552.0,
|
||||
"assets_by_zone": {
|
||||
"35": 16931.0
|
||||
},
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000",
|
||||
"is_compatible": true,
|
||||
"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
|
||||
},
|
||||
"53874": {
|
||||
"obj_info_id": 53874.0,
|
||||
"assets_by_zone": {
|
||||
"23": 108565.0
|
||||
},
|
||||
"zones_restrict": "0000000000000000000000000000000000000000000000000000",
|
||||
"is_compatible": true,
|
||||
"is_paid": false,
|
||||
"thumbnail_url": "https://images.neopets.com/items/clo_404_shirt.gif",
|
||||
"name": "404 Shirt",
|
||||
"description": "When Neopets is down, the shirt comes on!",
|
||||
"category": "Clothes",
|
||||
"type": "Clothes",
|
||||
"rarity": "Rare",
|
||||
"rarity_index": 88.0,
|
||||
"price": 1701.0,
|
||||
"weight_lbs": 1.0,
|
||||
"species_support": [
|
||||
3.0
|
||||
],
|
||||
"converted": true
|
||||
},
|
||||
"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": {
|
||||
"16931": {
|
||||
"asset_id": 16931.0,
|
||||
"zone_id": 35.0,
|
||||
"asset_url": "https://images.neopets.com/cp/items/swf/000/000/016/16931_aa01126387.swf",
|
||||
"obj_info_id": 39552.0,
|
||||
"manifest": "https://images.neopets.com/cp/items/data/000/000/016/16931_aa01126387/manifest.json?v=1706"
|
||||
},
|
||||
"108565": {
|
||||
"asset_id": 108565.0,
|
||||
"zone_id": 23.0,
|
||||
"asset_url": "https://images.neopets.com/cp/items/swf/000/000/108/108565_80d238dbaf.swf",
|
||||
"obj_info_id": 53874.0,
|
||||
"manifest": "https://images.neopets.com/cp/items/data/000/000/108/108565_80d238dbaf/manifest.json?v=1706"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
BIN
vendor/cache/crack-1.0.0.gem
vendored
BIN
vendor/cache/crack-1.0.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/debug-1.9.2.gem
vendored
BIN
vendor/cache/debug-1.9.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/hashdiff-1.1.2.gem
vendored
BIN
vendor/cache/hashdiff-1.1.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/webmock-3.24.0.gem
vendored
BIN
vendor/cache/webmock-3.24.0.gem
vendored
Binary file not shown.
Loading…
Reference in a new issue