2024-05-27 16:21:22 -07:00
require " async "
require " async/barrier "
2023-08-02 16:05:02 -07:00
class Item < ApplicationRecord
2011-07-26 15:49:52 -07:00
include PrettyParam
2024-06-18 15:21:43 -07:00
include Item :: Dyeworks
2013-01-11 14:20:06 -08:00
2023-07-22 12:23:14 -07:00
# We use the `type` column to mean something other than what Rails means!
self . inheritance_column = nil
2011-07-26 15:49:52 -07:00
2010-05-19 16:17:33 -07:00
SwfAssetType = 'object'
2011-05-02 15:07:56 -07:00
2011-07-12 16:37:16 -07:00
has_many :closet_hangers
2013-03-05 18:51:24 -08:00
has_one :contribution , :as = > :contributed , :inverse_of = > :contributed
2024-05-14 00:09:27 -07:00
has_one :nc_mall_record
2012-01-12 15:17:59 -08:00
has_many :parent_swf_asset_relationships , :as = > :parent
has_many :swf_assets , :through = > :parent_swf_asset_relationships
2024-06-07 20:10:06 -07:00
belongs_to :dyeworks_base_item , class_name : " Item " ,
default : - > { inferred_dyeworks_base_item } , optional : true
2011-05-02 15:07:56 -07:00
2011-07-22 13:18:15 -07:00
attr_writer :current_body_id , :owned , :wanted
2011-05-02 15:07:56 -07:00
2010-09-08 19:49:39 -07:00
NCRarities = [ 0 , 500 ]
2011-05-02 15:07:56 -07:00
PAINTBRUSH_SET_DESCRIPTION = 'This item is part of a deluxe paint brush set!'
2012-09-29 10:40:55 -07:00
SPECIAL_COLOR_DESCRIPTION_REGEX =
2024-02-17 12:50:35 -08:00
/ This item is only wearable by [a-zA-Z]+ painted ([a-zA-Z]+) \ .|WARNING: This [a-zA-Z]+ can be worn by ([a-zA-Z]+) [a-zA-Z]+ ONLY!|If your Neopet is not painted ([a-zA-Z]+), it will not be able to wear this item \ . /
2011-05-02 15:07:56 -07:00
2023-07-22 14:04:01 -07:00
scope :newest , - > {
order ( arel_table [ :created_at ] . desc ) if arel_table [ :created_at ]
}
2011-08-04 07:01:44 -07:00
2023-07-22 14:04:01 -07:00
scope :sitemap , - > { order ( [ :id ] ) . limit ( 49999 ) }
2011-05-20 17:49:48 -07:00
2024-02-25 15:00:22 -08:00
scope :name_includes , - > ( value ) {
2024-02-20 16:04:41 -08:00
Item . where ( " name LIKE ? " , " % " + sanitize_sql_like ( value ) + " % " )
2023-07-26 11:15:35 -07:00
}
2024-02-25 15:00:22 -08:00
scope :name_excludes , - > ( value ) {
2024-02-20 16:04:41 -08:00
Item . where ( " name NOT LIKE ? " , " % " + sanitize_sql_like ( value ) + " % " )
2023-07-26 11:15:35 -07:00
}
2023-07-22 18:13:11 -07:00
scope :is_nc , - > {
i = Item . arel_table
Fix crash when searching for "is:nc"
Huh, Arel can *sometimes* handle just having an attribute stand in as
"X is true" in a condition, but sometimes gets upset about it. I guess
this changed in Rails since we recently wrote this?
Specifically, item search would crash on "is:nc" (but *not* "is:np"),
saying:
```
undefined method `fetch_attribute' for #<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x0000000109a67110 @name="items", @klass=Item(…), @type_caster=#<ActiveRecord::TypeCaster::Map:0x0000000109a66e90 @klass=Item(…)>, @table_alias=nil>, name="is_manually_nc">
```
The traceback was a bit misleading (it happened at the part where we
merge all the scopes together), but that hinted to me that it working
with an attribute in a place where it expected a conditional. So I
converted the attribute in the `is_nc` scope to a conditional, and made
the matching change in `is_np`, and that fixed it! Ok phew!
2023-10-25 12:46:48 -07:00
where ( i [ :rarity_index ] . in ( Item :: NCRarities ) . or ( i [ :is_manually_nc ] . eq ( true ) ) )
2023-07-22 18:13:11 -07:00
}
2024-02-25 12:57:04 -08:00
scope :is_not_nc , - > {
2023-07-22 18:13:11 -07:00
i = Item . arel_table
Fix crash when searching for "is:nc"
Huh, Arel can *sometimes* handle just having an attribute stand in as
"X is true" in a condition, but sometimes gets upset about it. I guess
this changed in Rails since we recently wrote this?
Specifically, item search would crash on "is:nc" (but *not* "is:np"),
saying:
```
undefined method `fetch_attribute' for #<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x0000000109a67110 @name="items", @klass=Item(…), @type_caster=#<ActiveRecord::TypeCaster::Map:0x0000000109a66e90 @klass=Item(…)>, @table_alias=nil>, name="is_manually_nc">
```
The traceback was a bit misleading (it happened at the part where we
merge all the scopes together), but that hinted to me that it working
with an attribute in a place where it expected a conditional. So I
converted the attribute in the `is_nc` scope to a conditional, and made
the matching change in `is_np`, and that fixed it! Ok phew!
2023-10-25 12:46:48 -07:00
where ( i [ :rarity_index ] . in ( Item :: NCRarities ) . or ( i [ :is_manually_nc ] . eq ( true ) ) . not )
2023-07-22 18:13:11 -07:00
}
2024-02-25 12:57:04 -08:00
scope :is_np , - > {
self . is_not_nc . is_not_pb
}
scope :is_not_np , - > {
self . merge Item . is_nc . or ( Item . is_pb )
}
2023-07-26 11:51:52 -07:00
scope :is_pb , - > {
2024-02-20 16:04:41 -08:00
where ( 'description LIKE ?' ,
'%' + sanitize_sql_like ( PAINTBRUSH_SET_DESCRIPTION ) + '%' )
2023-07-26 11:51:52 -07:00
}
scope :is_not_pb , - > {
2024-02-20 16:04:41 -08:00
where ( 'description NOT LIKE ?' ,
'%' + sanitize_sql_like ( PAINTBRUSH_SET_DESCRIPTION ) + '%' )
2023-07-26 11:51:52 -07:00
}
2024-01-23 05:43:00 -08:00
scope :occupies , - > ( zone_label ) {
zone_ids = Zone . matching_label ( zone_label ) . map ( & :id )
2024-06-03 11:45:51 -07:00
# NOTE: In searches, this query performs much better using a subquery
# instead of joins! This is because, in the joins case, filtering by an
# `swf_assets` field but sorting by an `items` field causes the query
# planner to only be able to use an index for *one* of them. In this case,
# MySQL can use the `swf_assets`.`zone_id` index to get the item IDs for
# the subquery, then use the `items`.`name` index to sort them.
i = arel_table
psa = ParentSwfAssetRelationship . arel_table
2023-07-26 12:28:25 -07:00
sa = SwfAsset . arel_table
2024-06-03 11:45:51 -07:00
where (
ParentSwfAssetRelationship . joins ( :swf_asset ) .
where ( sa [ :zone_id ] . in ( zone_ids ) ) .
where ( psa [ :parent_type ] . eq ( " Item " ) ) .
where ( psa [ :parent_id ] . eq ( i [ :id ] ) ) .
arel . exists
)
2023-07-26 12:28:25 -07:00
}
2024-01-23 05:43:00 -08:00
scope :not_occupies , - > ( zone_label ) {
zone_ids = Zone . matching_label ( zone_label ) . map ( & :id )
2023-07-26 12:28:25 -07:00
i = Item . arel_table
sa = SwfAsset . arel_table
2023-07-28 14:10:13 -07:00
# Querying for "has NO swf_assets matching these zone IDs" is trickier than
# the positive case! To do it, we GROUP_CONCAT the zone_ids together for
# each item, then use FIND_IN_SET to search the result for each zone ID,
# and assert that it must not find a match. (This is uhh, not exactly fast,
# so it helps to have other tighter conditions applied first!)
# TODO: I feel like this could also be solved with a LEFT JOIN, idk if that
# performs any better? In Rails 5+ `left_outer_joins` is built in so!
condition = zone_ids . map { 'FIND_IN_SET(?, GROUP_CONCAT(zone_id)) = 0' } . join ( ' AND ' )
joins ( :swf_assets ) . group ( i [ :id ] ) . having ( condition , * zone_ids ) . distinct
2023-07-26 12:41:37 -07:00
}
2024-01-23 05:43:00 -08:00
scope :restricts , - > ( zone_label ) {
zone_ids = Zone . matching_label ( zone_label ) . map ( & :id )
2023-07-29 13:11:53 -07:00
condition = zone_ids . map { '(SUBSTR(items.zones_restrict, ?, 1) = "1")' } . join ( ' OR ' )
2023-07-26 12:41:37 -07:00
where ( condition , * zone_ids )
}
2024-01-23 05:43:00 -08:00
scope :not_restricts , - > ( zone_label ) {
zone_ids = Zone . matching_label ( zone_label ) . map ( & :id )
2023-07-29 13:11:53 -07:00
condition = zone_ids . map { '(SUBSTR(items.zones_restrict, ?, 1) = "1")' } . join ( ' OR ' )
2023-07-26 12:41:37 -07:00
where ( " NOT ( #{ condition } ) " , * zone_ids )
2023-07-26 12:28:25 -07:00
}
2023-07-28 14:45:10 -07:00
scope :fits , - > ( body_id ) {
2024-02-25 14:47:37 -08:00
joins ( :swf_assets ) . where ( swf_assets : { body_id : [ body_id , 0 ] } ) . distinct
2023-07-28 14:45:10 -07:00
}
scope :not_fits , - > ( body_id ) {
i = Item . arel_table
sa = SwfAsset . arel_table
# Querying for "has NO swf_assets matching these body IDs" is trickier than
# the positive case! To do it, we GROUP_CONCAT the body_ids together for
# each item, then use FIND_IN_SET to search the result for the body ID,
# and assert that it must not find a match. (This is uhh, not exactly fast,
# so it helps to have other tighter conditions applied first!)
#
# TODO: I feel like this could also be solved with a LEFT JOIN, idk if that
# performs any better? In Rails 5+ `left_outer_joins` is built in so!
#
# NOTE: The `fits` and `not_fits` counts don't perfectly add up to the
# total number of items, 5 items aren't accounted for? I'm not going to
# bother looking into this, but one thing I notice is items with no assets
# somehow would not match either scope in this impl (but LEFT JOIN would!)
joins ( :swf_assets ) . group ( i [ :id ] ) .
2024-02-27 14:07:20 -08:00
having (
" FIND_IN_SET(?, GROUP_CONCAT(body_id)) = 0 AND " +
" FIND_IN_SET(0, GROUP_CONCAT(body_id)) = 0 " ,
body_id
) .
2023-07-28 14:45:10 -07:00
distinct
}
2023-07-22 18:13:11 -07:00
2023-11-03 16:20:02 -07:00
def nc_trade_value
return nil unless nc?
2024-05-27 16:21:22 -07:00
# Load the trade value, if we haven't already. Note that, because the trade
# value may be nil, we also save an explicit boolean for whether we've
# already looked it up, rather than checking if the saved value is empty.
return @nc_trade_value if @nc_trade_value_loaded
@nc_trade_value = begin
Rails . logger . debug " Item #{ id } ( #{ name } ) <lookup> "
2024-02-20 15:37:07 -08:00
OwlsValueGuide . find_by_name ( name )
2023-11-03 16:20:02 -07:00
rescue OwlsValueGuide :: NotFound = > error
2024-02-20 15:37:07 -08:00
Rails . logger . debug ( " No NC trade value listed for #{ name } ( #{ id } ) " )
2024-05-27 16:21:22 -07:00
nil
2023-11-03 16:20:02 -07:00
rescue OwlsValueGuide :: NetworkError = > error
Rails . logger . error ( " Couldn't load nc_trade_value: #{ error . full_message } " )
2024-05-27 16:21:22 -07:00
nil
2023-11-03 16:20:02 -07:00
end
2024-05-27 16:21:22 -07:00
@nc_trade_value_loaded = true
@nc_trade_value
2011-07-12 22:21:48 -07:00
end
2012-10-24 20:09:05 -07:00
# Return an OrderedHash mapping users to the number of times they
# contributed to this item's assets, from most contributions to least.
def contributors_with_counts
# Get contributing users' IDs
swf_asset_ids = swf_assets . select ( SwfAsset . arel_table [ :id ] ) . map ( & :id )
swf_asset_contributions = Contribution . select ( 'user_id' ) .
where ( :contributed_type = > 'SwfAsset' , :contributed_id = > swf_asset_ids )
contributor_ids = swf_asset_contributions . map ( & :user_id )
# Get the users, mapped by ID
contributors_by_id = { }
User . find ( contributor_ids ) . each { | u | contributors_by_id [ u . id ] = u }
# Count each user's contributions
contributor_counts_by_id = Hash . new ( 0 )
contributor_ids . each { | id | contributor_counts_by_id [ id ] += 1 }
# Build an OrderedHash mapping users to counts in descending order
contributors_with_counts = ActiveSupport :: OrderedHash . new
contributor_counts_by_id . sort_by { | k , v | v } . reverse . each do | id , count |
contributor = contributors_by_id [ id ]
contributors_with_counts [ contributor ] = count
end
contributors_with_counts
end
2011-05-02 15:07:56 -07:00
2010-09-08 19:49:39 -07:00
def nc?
2023-11-03 16:27:39 -07:00
is_manually_nc? || NCRarities . include? ( rarity_index )
2010-09-08 19:49:39 -07:00
end
globalized search first draft
Confirmed features:
* Output (retrieval, sorting, etc.)
* Name (positive and negative, but new behavior)
* Flags (positive and negative)
Planned features:
* users:owns, user:wants
Known issues:
* Sets are broken
* Don't render properly
* Shouldn't actually be done as joined sets, anyway, since
we actually want (set1_zone1 OR set1_zone2) AND
(set2_zone1 OR set2_zone2), which will require breaking
it into multiple terms queries.
* Name has regressed: ignores phrases, doesn't require *all*
words. While we're breaking sets into multiple queries,
maybe we'll do something similar for name. In fact, we
really kinda have to if we're gonna keep sorting by name,
since "straw hat" returns all hats. Eww.
2013-01-18 21:23:37 -08:00
def pb?
2013-01-26 07:52:21 -08:00
I18n . with_locale ( :en ) { self . description == PAINTBRUSH_SET_DESCRIPTION }
globalized search first draft
Confirmed features:
* Output (retrieval, sorting, etc.)
* Name (positive and negative, but new behavior)
* Flags (positive and negative)
Planned features:
* users:owns, user:wants
Known issues:
* Sets are broken
* Don't render properly
* Shouldn't actually be done as joined sets, anyway, since
we actually want (set1_zone1 OR set1_zone2) AND
(set2_zone1 OR set2_zone2), which will require breaking
it into multiple terms queries.
* Name has regressed: ignores phrases, doesn't require *all*
words. While we're breaking sets into multiple queries,
maybe we'll do something similar for name. In fact, we
really kinda have to if we're gonna keep sorting by name,
since "straw hat" returns all hats. Eww.
2013-01-18 21:23:37 -08:00
end
2011-05-02 15:07:56 -07:00
Add bare-bones Item Getting Guide page
TNT requested that we figure out ways to connect the dots between
people's intentions on DTI to their purchases in the NC Mall.
But rather than just slam ad links everywhere, our plan is to design an
actually useful feature about it: the "Item Getting Guide". It'll break
down items by how you can actually get them (NP economy, NC Mall,
retired NC, Dyeworks, etc), and we're planning some cute actions you
can take, like shortcuts for getting them onto trade wishlists or into
your NC Mall cart.
This is just a little demo version of the page, just breaking down
items specified in the URL into NC/NP/PB! Later we'll do more granular
breakdown than this, with more info and actions—and we'll also like,
link to it at all, which isn't the case yet! (The main way we expect
people to get here is by a "Get these items" button we'll add to the
outfit editor, but there might be other paths, too.)
2024-05-06 20:37:59 -07:00
def np?
! nc? && ! pb?
end
2024-05-14 00:09:27 -07:00
def currently_in_mall?
nc_mall_record . present?
end
2024-06-16 12:37:53 -07:00
def source
if dyeworks_buyable?
:dyeworks
elsif currently_in_mall?
:nc_mall
elsif nc?
:other_nc
elsif np?
:np
elsif pb?
:pb
else
raise " Item has no matching source (should not happen?) "
end
end
2011-07-22 13:18:15 -07:00
def owned?
2024-02-23 10:44:50 -08:00
@owned || false
2011-07-22 13:18:15 -07:00
end
def wanted?
2024-02-23 10:44:50 -08:00
@wanted || false
2011-07-22 13:18:15 -07:00
end
2024-05-14 16:03:35 -07:00
def current_nc_price
2024-05-21 17:24:55 -07:00
nc_mall_record & . current_price
2024-05-14 16:03:35 -07:00
end
2024-05-22 15:41:46 -07:00
# If this is a PB item, return the corresponding Color, inferred from the
2024-06-05 19:46:12 -07:00
# item name. If it's not a PB item, or we fail to infer a specific color,
# return nil. (This is expected to be nil for some PB items, like the "Aisha
# Collar", which belong to many colors. It can also be nil for PB items for
# new colors we haven't manually added to the database yet, or if a PB item
# is named strangely in the future.)
2024-05-22 15:41:46 -07:00
def pb_color
return nil unless pb?
# NOTE: To handle colors like "Royalboy", where the items aren't consistent
# with the color name regarding whether or not there's spaces, we remove
# all spaces from the item name and color name when matching. We also
# hackily handle the fact that "Elderlyboy" color has items named "Elderly
# Male" (and same for Girl/Female) by replacing those words, too. These
# hacks could cause false matches in theory, but I'm not aware of any rn!
normalized_name = name . downcase . gsub ( " female " , " girl " ) . gsub ( " male " , " boy " ) .
gsub ( / \ s / , " " )
Color . order ( :name ) .
find { | c | normalized_name . include? ( c . name . downcase . gsub ( / \ s / , " " ) ) }
end
2024-05-22 17:53:52 -07:00
# If this is a PB item, return the corresponding Species, inferred from the
2024-06-05 19:46:12 -07:00
# item name. If it's not a PB item, or we fail to infer a specific species,
# return nil. (This is not expected to be nil in general, but could be for PB
# items for new species we haven't manually added to the database yet, or if
# a PB item is named strangely in the future.)
2024-05-22 17:53:52 -07:00
def pb_species
return nil unless pb?
normalized_name = name . downcase
Species . order ( :name ) . find { | s | normalized_name . include? ( s . name . downcase ) }
end
2024-05-22 15:41:46 -07:00
def pb_item_name
pb_color & . pb_item_name
end
2013-01-21 17:34:39 -08:00
def restricted_zones ( options = { } )
2023-11-09 21:35:42 -08:00
options [ :scope ] || = Zone . all
2013-01-21 17:34:39 -08:00
options [ :scope ] . find ( restricted_zone_ids )
2010-09-08 19:49:39 -07:00
end
globalized search first draft
Confirmed features:
* Output (retrieval, sorting, etc.)
* Name (positive and negative, but new behavior)
* Flags (positive and negative)
Planned features:
* users:owns, user:wants
Known issues:
* Sets are broken
* Don't render properly
* Shouldn't actually be done as joined sets, anyway, since
we actually want (set1_zone1 OR set1_zone2) AND
(set2_zone1 OR set2_zone2), which will require breaking
it into multiple terms queries.
* Name has regressed: ignores phrases, doesn't require *all*
words. While we're breaking sets into multiple queries,
maybe we'll do something similar for name. In fact, we
really kinda have to if we're gonna keep sorting by name,
since "straw hat" returns all hats. Eww.
2013-01-18 21:23:37 -08:00
def restricted_zone_ids
2013-01-21 17:34:39 -08:00
unless @restricted_zone_ids
@restricted_zone_ids = [ ]
zones_restrict . split ( / / ) . each_with_index do | switch , id |
@restricted_zone_ids << ( id . to_i + 1 ) if switch == '1'
end
end
@restricted_zone_ids
globalized search first draft
Confirmed features:
* Output (retrieval, sorting, etc.)
* Name (positive and negative, but new behavior)
* Flags (positive and negative)
Planned features:
* users:owns, user:wants
Known issues:
* Sets are broken
* Don't render properly
* Shouldn't actually be done as joined sets, anyway, since
we actually want (set1_zone1 OR set1_zone2) AND
(set2_zone1 OR set2_zone2), which will require breaking
it into multiple terms queries.
* Name has regressed: ignores phrases, doesn't require *all*
words. While we're breaking sets into multiple queries,
maybe we'll do something similar for name. In fact, we
really kinda have to if we're gonna keep sorting by name,
since "straw hat" returns all hats. Eww.
2013-01-18 21:23:37 -08:00
end
def occupied_zone_ids
occupied_zones . map ( & :id )
end
2011-05-02 15:07:56 -07:00
2013-01-21 17:34:39 -08:00
def occupied_zones ( options = { } )
2023-11-09 21:35:42 -08:00
options [ :scope ] || = Zone . all
2010-09-08 19:49:39 -07:00
all_body_ids = [ ]
zone_body_ids = { }
selected_assets = swf_assets . select ( 'body_id, zone_id' ) . each do | swf_asset |
zone_body_ids [ swf_asset . zone_id ] || = [ ]
body_ids = zone_body_ids [ swf_asset . zone_id ]
body_ids << swf_asset . body_id unless body_ids . include? ( swf_asset . body_id )
all_body_ids << swf_asset . body_id unless all_body_ids . include? ( swf_asset . body_id )
end
2013-01-21 17:34:39 -08:00
zones = options [ :scope ] . find ( zone_body_ids . keys )
zones_by_id = zones . inject ( { } ) { | h , z | h [ z . id ] = z ; h }
2010-09-08 19:49:39 -07:00
total_body_ids = all_body_ids . size
zone_body_ids . each do | zone_id , body_ids |
2013-01-21 17:34:39 -08:00
zones_by_id [ zone_id ] . sometimes = true if body_ids . size < total_body_ids
2010-09-08 19:49:39 -07:00
end
zones
end
2011-05-02 15:07:56 -07:00
2011-01-27 13:35:46 -08:00
def affected_zones
restricted_zones + occupied_zones
end
2011-05-02 15:07:56 -07:00
def special_color
@special_color || = determine_special_color
end
2013-12-05 13:22:43 -08:00
def special_color_id
special_color . try ( :id )
end
2011-05-02 15:07:56 -07:00
protected
def determine_special_color
2013-01-21 14:01:41 -08:00
I18n . with_locale ( I18n . default_locale ) do
# Rather than go find the special description in all locales, let's just
# run this logic in English.
if description . include? ( PAINTBRUSH_SET_DESCRIPTION )
2013-02-03 11:31:22 -08:00
name_words = name . downcase . split
2013-01-21 14:01:41 -08:00
Color . nonstandard . each do | color |
2013-02-03 11:31:22 -08:00
return color if name_words . include? ( color . name )
2013-01-21 14:01:41 -08:00
end
2011-05-02 15:07:56 -07:00
end
2013-01-21 14:01:41 -08:00
match = description . match ( SPECIAL_COLOR_DESCRIPTION_REGEX )
if match
2013-06-23 22:58:17 -07:00
# Since there are multiple formats in the one regex, there are multiple
# possible color name captures. So, take the first non-nil capture.
color = match . captures . detect ( & :present? )
2013-01-21 14:01:41 -08:00
return Color . find_by_name ( color . downcase )
end
2014-12-16 22:54:55 -08:00
2017-10-21 13:32:01 -07:00
# HACK: this should probably be a flag on the record instead of
# being hardcoded :P
2018-02-25 14:45:10 -08:00
if [ 71893 , 76192 , 76202 , 77367 , 77368 , 77369 , 77370 ] . include? ( id )
2014-12-16 22:54:55 -08:00
return Color . find_by_name ( 'baby' )
end
2017-10-21 13:32:01 -07:00
if [ 76198 ] . include? ( id )
return Color . find_by_name ( 'mutant' )
end
if [ 75372 ] . include? ( id )
return Color . find_by_name ( 'maraquan' )
end
2018-05-09 14:10:20 -07:00
if manual_special_color_id?
return Color . find ( manual_special_color_id )
end
2011-05-02 15:07:56 -07:00
end
end
public
2010-05-15 08:38:45 -07:00
def species_support_ids
2010-11-27 15:41:06 -08:00
@species_support_ids_array || = read_attribute ( 'species_support_ids' ) . split ( ',' ) . map ( & :to_i ) rescue nil
2010-05-15 08:38:45 -07:00
end
2011-05-02 15:07:56 -07:00
2010-05-15 08:38:45 -07:00
def species_support_ids = ( replacement )
2010-05-16 12:01:38 -07:00
@species_support_ids_array = nil
2010-05-15 08:38:45 -07:00
replacement = replacement . join ( ',' ) if replacement . is_a? ( Array )
write_attribute ( 'species_support_ids' , replacement )
end
2013-12-05 13:22:43 -08:00
2013-01-21 12:55:48 -08:00
def support_species? ( species )
species_support_ids . blank? || species_support_ids . include? ( species . id )
end
2011-05-02 15:07:56 -07:00
2013-12-14 15:19:27 -08:00
def modeled_body_ids
@modeled_body_ids || = swf_assets . select ( 'DISTINCT body_id' ) . map ( & :body_id )
end
def modeled_color_ids
# Might be empty if modeled_body_ids is 0. But it's currently not called
# in that scenario, so, whatever.
@modeled_color_ids || = PetType . select ( 'DISTINCT color_id' ) .
where ( body_id : modeled_body_ids ) .
map ( & :color_id )
end
Update missing body ID prediction to handle, say, the Maraquan Mynci.
It turns out that some pets for seemingly nonstandard colors have the
standard body type anyway, and vice-versa. This implies that we should
stop relying on a color's standardness, but, for the time being, we've
just revised the prediction model:
Old model:
* If I see a body_id, I find the corresponding color_ids, and it's wearable
by all pet types with those color_ids.
New model:
* If I see a body_id,
* If it also belongs to a basic pet type, it's a standard body ID.
* It therefore fits all pet types of standard color (if there's
more than one body ID modeled already). (Not really,
because of weird exceptions like Orange Chia. Should that be
standard or not?)
* If it doesn't also belong to a basic pet type, it's a nonstandard
body ID.
* It therefore only belongs to one color, and therefore the item
fits all pet types of the same color.
2014-01-20 13:29:01 -08:00
def basic_body_ids
@basic_body_ids || = begin
basic_color_ids || = Color . select ( [ :id ] ) . basic . map ( & :id )
PetType . select ( 'DISTINCT body_id' ) .
where ( color_id : basic_color_ids ) . map ( & :body_id )
end
2013-12-14 15:19:27 -08:00
end
def predicted_body_ids
@predicted_body_ids || = if modeled_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
# it, that indicates a glitched item, but this method chooses to reflect
# behavior elsewhere in the app by saying that we can put this item on
# anybody. (Heh. Any body.))
modeled_body_ids
elsif modeled_body_ids . size == 1
# 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.
modeled_body_ids
else
# If an item is worn by more than one body, then it must be wearable by
# all bodies of the same color. (To my knowledge, anyway. I'm not aware
# of any exceptions.) So, let's find those bodies by first finding those
# colors.
Update missing body ID prediction to handle, say, the Maraquan Mynci.
It turns out that some pets for seemingly nonstandard colors have the
standard body type anyway, and vice-versa. This implies that we should
stop relying on a color's standardness, but, for the time being, we've
just revised the prediction model:
Old model:
* If I see a body_id, I find the corresponding color_ids, and it's wearable
by all pet types with those color_ids.
New model:
* If I see a body_id,
* If it also belongs to a basic pet type, it's a standard body ID.
* It therefore fits all pet types of standard color (if there's
more than one body ID modeled already). (Not really,
because of weird exceptions like Orange Chia. Should that be
standard or not?)
* If it doesn't also belong to a basic pet type, it's a nonstandard
body ID.
* It therefore only belongs to one color, and therefore the item
fits all pet types of the same color.
2014-01-20 13:29:01 -08:00
basic_modeled_body_ids , nonbasic_modeled_body_ids = modeled_body_ids .
partition { | bi | basic_body_ids . include? ( bi ) }
output = [ ]
if basic_modeled_body_ids . present?
output += basic_body_ids
end
if nonbasic_modeled_body_ids . present?
nonbasic_modeled_color_ids = PetType . select ( 'DISTINCT color_id' ) .
where ( body_id : nonbasic_modeled_body_ids ) .
map ( & :color_id )
output += PetType . select ( 'DISTINCT body_id' ) .
where ( color_id : nonbasic_modeled_color_ids ) .
map ( & :body_id )
end
output
2013-12-14 15:19:27 -08:00
end
end
def predicted_missing_body_ids
@predicted_missing_body_ids || = predicted_body_ids - modeled_body_ids
end
2014-01-01 07:15:58 -08:00
def predicted_missing_standard_body_ids_by_species_id
@predicted_missing_standard_body_ids_by_species_id || =
PetType . select ( 'DISTINCT body_id, species_id' ) .
joins ( :color ) .
where ( body_id : predicted_missing_body_ids ,
colors : { standard : true } ) .
inject ( { } ) { | h , pt | h [ pt . species_id ] = pt . body_id ; h }
2013-12-14 15:19:27 -08:00
end
2024-01-23 05:23:57 -08:00
def predicted_missing_standard_body_ids_by_species
species = Species . where ( id : predicted_missing_standard_body_ids_by_species_id . keys )
2014-01-01 07:15:58 -08:00
species_by_id = species . inject ( { } ) { | h , s | h [ s . id ] = s ; h }
predicted_missing_standard_body_ids_by_species_id . inject ( { } ) { | h , ( sid , bid ) |
h [ species_by_id [ sid ] ] = bid ; h }
2013-12-14 15:19:27 -08:00
end
def predicted_missing_nonstandard_body_pet_types
PetType . joins ( :color ) .
Update missing body ID prediction to handle, say, the Maraquan Mynci.
It turns out that some pets for seemingly nonstandard colors have the
standard body type anyway, and vice-versa. This implies that we should
stop relying on a color's standardness, but, for the time being, we've
just revised the prediction model:
Old model:
* If I see a body_id, I find the corresponding color_ids, and it's wearable
by all pet types with those color_ids.
New model:
* If I see a body_id,
* If it also belongs to a basic pet type, it's a standard body ID.
* It therefore fits all pet types of standard color (if there's
more than one body ID modeled already). (Not really,
because of weird exceptions like Orange Chia. Should that be
standard or not?)
* If it doesn't also belong to a basic pet type, it's a nonstandard
body ID.
* It therefore only belongs to one color, and therefore the item
fits all pet types of the same color.
2014-01-20 13:29:01 -08:00
where ( body_id : predicted_missing_body_ids - basic_body_ids ,
2013-12-14 15:19:27 -08:00
colors : { standard : false } )
end
2024-01-23 05:23:57 -08:00
def predicted_missing_nonstandard_body_ids_by_species_by_color
2013-12-14 15:19:27 -08:00
pet_types = predicted_missing_nonstandard_body_pet_types
species_by_id = { }
2024-01-23 05:23:57 -08:00
Species . find ( pet_types . map ( & :species_id ) ) . each do | species |
2013-12-14 15:19:27 -08:00
species_by_id [ species . id ] = species
end
colors_by_id = { }
2024-01-23 05:23:57 -08:00
Color . find ( pet_types . map ( & :color_id ) ) . each do | color |
2013-12-14 15:19:27 -08:00
colors_by_id [ color . id ] = color
end
2014-01-01 07:15:58 -08:00
body_ids_by_species_by_color = { }
2013-12-14 15:19:27 -08:00
pet_types . each do | pt |
color = colors_by_id [ pt . color_id ]
2014-01-01 07:15:58 -08:00
body_ids_by_species_by_color [ color ] || = { }
body_ids_by_species_by_color [ color ] [ species_by_id [ pt . species_id ] ] = pt . body_id
2013-12-14 15:19:27 -08:00
end
2014-01-01 07:15:58 -08:00
body_ids_by_species_by_color
2013-12-14 15:19:27 -08:00
end
def predicted_fully_modeled?
predicted_missing_body_ids . empty?
end
def predicted_modeled_ratio
modeled_body_ids . size . to_f / predicted_body_ids . size
end
2013-06-27 00:10:55 -07:00
def as_json ( options = { } )
2023-11-11 08:41:31 -08:00
super ( {
only : [ :id , :name , :description , :thumbnail_url , :rarity_index ] ,
methods : [ :zones_restrict ] ,
} . merge ( options ) )
2010-06-22 10:00:55 -07:00
end
2011-05-02 15:07:56 -07:00
2010-11-06 15:08:42 -07:00
def handle_assets!
2010-10-09 09:22:40 -07:00
if @parent_swf_asset_relationships_to_update && @current_body_id
new_swf_asset_ids = @parent_swf_asset_relationships_to_update . map ( & :swf_asset_id )
rels = ParentSwfAssetRelationship . arel_table
swf_assets = SwfAsset . arel_table
2012-01-26 11:51:30 -08:00
2011-07-22 12:08:17 -07:00
# If a relationship used to bind an item and asset for this body type,
# but doesn't in this sample, the two have been unbound. Delete the
# relationship.
2010-10-09 09:22:40 -07:00
ids_to_delete = self . parent_swf_asset_relationships .
2012-01-26 11:51:30 -08:00
select ( rels [ :id ] ) .
2012-01-12 15:17:59 -08:00
joins ( :swf_asset ) .
2010-11-16 14:26:06 -08:00
where ( rels [ :swf_asset_id ] . not_in ( new_swf_asset_ids ) ) .
2010-10-09 09:22:40 -07:00
where ( swf_assets [ :body_id ] . in ( [ @current_body_id , 0 ] ) ) .
2012-01-26 11:51:30 -08:00
map ( & :id )
2010-10-09 09:22:40 -07:00
unless ids_to_delete . empty?
2012-01-26 11:51:30 -08:00
ParentSwfAssetRelationship . where ( :id = > ids_to_delete ) . delete_all
2010-10-09 09:22:40 -07:00
end
2012-01-26 11:51:30 -08:00
2011-10-23 12:09:53 -07:00
@parent_swf_asset_relationships_to_update . each do | rel |
rel . save!
rel . swf_asset . save!
end
2010-10-09 09:22:40 -07:00
end
end
2013-01-22 22:25:09 -08:00
def body_specific?
# If there are species support IDs (it's not empty), the item is
# body-specific. If it's empty, it fits everyone the same.
2017-10-29 15:08:22 -07:00
explicitly_body_specific? || ! species_support_ids . empty?
2013-01-22 22:25:09 -08:00
end
2011-05-02 15:07:56 -07:00
2015-07-27 10:23:46 -07:00
def add_origin_registry_info ( info , locale )
2010-10-07 07:46:23 -07:00
# bear in mind that numbers from registries are floats
2013-12-08 20:59:36 -08:00
species_support_strs = info [ 'species_support' ] || [ ]
self . species_support_ids = species_support_strs . map ( & :to_i )
2015-07-27 10:23:46 -07:00
2024-05-02 13:00:10 -07:00
# NOTE: If some of these fields are missing, it could cause saving the item
# to fail, because many of these columns are non-nullable.
2024-02-20 15:52:03 -08:00
self . name = info [ 'name' ]
self . description = info [ 'description' ]
self . thumbnail_url = info [ 'thumbnail_url' ]
self . category = info [ 'category' ]
self . type = info [ 'type' ]
self . rarity = info [ 'rarity' ]
self . rarity_index = info [ 'rarity_index' ] . to_i
self . price = info [ 'price' ] . to_i
self . weight_lbs = info [ 'weight_lbs' ] . to_i
self . zones_restrict = info [ 'zones_restrict' ]
2010-10-07 07:46:23 -07:00
end
2011-05-02 15:07:56 -07:00
2010-11-06 15:08:42 -07:00
def pending_swf_assets
@parent_swf_asset_relationships_to_update . inject ( [ ] ) do | all_swf_assets , relationship |
all_swf_assets << relationship . swf_asset
end
end
2011-05-02 15:07:56 -07:00
2010-10-09 09:22:40 -07:00
def parent_swf_asset_relationships_to_update = ( rels )
@parent_swf_asset_relationships_to_update = rels
end
2011-05-02 15:07:56 -07:00
2024-02-25 12:06:20 -08:00
# NOTE: Adding the JSON serializer makes `as_json` treat this like a model
# instead of like a hash, so you can target its children with things like
# the `include` option. This feels clunky though, I wish I had something a
# bit more suited to it!
Appearance = Struct . new ( :body , :swf_assets ) do
include ActiveModel :: Serializers :: JSON
def attributes
{ body : body , swf_assets : swf_assets }
end
end
Appearance :: Body = Struct . new ( :id , :species ) do
include ActiveModel :: Serializers :: JSON
def attributes
{ id : id , species : species }
end
end
2023-11-11 07:14:48 -08:00
def appearances
all_swf_assets = swf_assets . to_a
# If there are no assets yet, there are no appearances.
return [ ] if all_swf_assets . empty?
# Get all SWF assets, and separate the ones that fit everyone (body_id=0).
swf_assets_by_body_id = all_swf_assets . group_by ( & :body_id )
swf_assets_for_all_bodies = swf_assets_by_body_id . delete ( 0 ) || [ ]
# If there are no body-specific assets, return one appearance for them all.
if swf_assets_by_body_id . empty?
2024-02-24 16:14:30 -08:00
body = Appearance :: Body . new ( 0 , nil )
2023-11-11 08:13:07 -08:00
return [ Appearance . new ( body , swf_assets_for_all_bodies ) ]
2023-11-11 07:14:48 -08:00
end
# Otherwise, create an appearance for each real (nonzero) body ID. We don't
# generally expect body_id = 0 and body_id != 0 to mix, but if they do,
# uhh, let's merge the body_id = 0 ones in?
swf_assets_by_body_id . map do | body_id , body_specific_assets |
swf_assets_for_body = body_specific_assets + swf_assets_for_all_bodies
2023-11-11 07:54:56 -08:00
species = Species . with_body_id ( body_id ) . first!
2024-02-23 10:44:50 -08:00
body = Appearance :: Body . new ( body_id , species )
2023-11-11 07:54:56 -08:00
Appearance . new ( body , swf_assets_for_body )
2023-11-11 07:14:48 -08:00
end
use proxies for item html, too
Some lame benchmarking on my box, dev, cache classes, many items:
No proxies:
Fresh JSON: 175, 90, 90, 93, 82, 88, 158, 150, 85, 167 = 117.8
Cached JSON: (none)
Fresh HTML: 371, 327, 355, 328, 322, 346 = 341.5
Cached HTML: 173, 123, 175, 187, 171, 179 = 168
Proxies:
Fresh JSON: 175, 183, 269, 219, 195, 178 = 203.17
Cached JSON: 88, 70, 89, 162, 80, 77 = 94.3
Fresh HTML: 494, 381, 350, 334, 451, 372 = 397
Cached HTML: 176, 170, 104, 101, 111, 116 = 129.7
So, overhead is significant, but the gains when cached (and that should be
all the time, since we currently have 0 evictions) are definitely worth
it. Worth pushing, and probably putting some future effort into reducing
overhead.
On production (again, lame), items#index was consistently averaging
73-74ms when super healthy, and 82ms when pets#index was being louder
than usual. For reference is all. This will probably perform
significantly worse at first (in JSON, anyway, since HTML is already
mostly cached), so it might be worth briefly warming the cache after
pushing.
2013-06-26 23:39:04 -07:00
end
2024-02-27 16:11:06 -08:00
# Given a list of item IDs, return how they look on the given target (either
# a pet type or an alt style).
def self . appearances_for ( item_ids , target , swf_asset_includes : [ ] )
# First, load all the relationships for these items that also fit this
# body.
relationships = ParentSwfAssetRelationship .
includes ( swf_asset : swf_asset_includes ) .
where ( parent_type : " Item " , parent_id : item_ids ) .
where ( swf_asset : { body_id : [ target . body_id , 0 ] } )
pet_type_body = Appearance :: Body . new ( target . body_id , target . species )
all_pets_body = Appearance :: Body . new ( 0 , nil )
# Then, convert this into a hash from item ID to SWF assets.
assets_by_item_id = relationships . group_by ( & :parent_id ) .
transform_values { | rels | rels . map ( & :swf_asset ) }
# Finally, for each item, return an appearance—even if it's empty!
item_ids . to_h do | item_id |
assets = assets_by_item_id . fetch ( item_id , [ ] )
fits_all_pets = assets . present? && assets . all? { | a | a . body_id == 0 }
body = fits_all_pets ? all_pets_body : pet_type_body
[ item_id , Appearance . new ( body , assets ) ]
end
end
2010-11-06 08:52:58 -07:00
def self . all_by_ids_or_children ( ids , swf_assets )
swf_asset_ids = [ ]
swf_assets_by_id = { }
swf_assets_by_parent_id = { }
swf_assets . each do | swf_asset |
id = swf_asset . id
swf_assets_by_id [ id ] = swf_asset
swf_asset_ids << id
end
2023-07-22 13:32:29 -07:00
SwfAsset . object_assets . joins ( :parent_swf_asset_relationships ) .
where ( SwfAsset . arel_table [ :id ] . in ( swf_asset_ids ) ) . select ( [
2012-01-12 15:17:59 -08:00
SwfAsset . arel_table [ :id ] ,
ParentSwfAssetRelationship . arel_table [ :parent_id ]
2023-07-22 13:32:29 -07:00
] ) . each do | row |
2010-11-06 08:52:58 -07:00
item_id = row . parent_id . to_i
swf_assets_by_parent_id [ item_id ] || = [ ]
swf_assets_by_parent_id [ item_id ] << swf_assets_by_id [ row . id . to_i ]
ids << item_id
end
find ( ids ) . tap do | items |
items . each do | item |
swf_assets = swf_assets_by_parent_id [ item . id ]
if swf_assets
swf_assets . each do | swf_asset |
swf_asset . item = item
end
end
end
end
end
2011-05-02 15:07:56 -07:00
2024-05-27 16:21:22 -07:00
def self . preload_nc_trade_values ( items )
# 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 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
end
2023-11-09 21:35:42 -08:00
def self . collection_from_pet_type_and_registries ( pet_type , info_registry , asset_registry , scope = Item . all )
2010-10-07 07:46:23 -07:00
# bear in mind that registries are arrays with many nil elements,
# due to how the parser works
2011-07-22 12:08:17 -07:00
# Collect existing items
2010-10-07 07:46:23 -07:00
items = { }
item_ids = [ ]
2011-01-13 14:22:07 -08:00
info_registry . each do | item_id , info |
2010-10-09 08:06:59 -07:00
if info && info [ :is_compatible ]
2011-01-13 14:22:07 -08:00
item_ids << item_id . to_i
2010-10-07 07:46:23 -07:00
end
end
2011-07-22 12:08:17 -07:00
# Collect existing relationships
2010-10-07 07:46:23 -07:00
existing_relationships_by_item_id_and_swf_asset_id = { }
2023-10-12 18:57:39 -07:00
existing_items = scope . where ( id : item_ids ) .
2023-10-12 21:49:09 -07:00
includes ( :parent_swf_asset_relationships )
2010-10-07 07:46:23 -07:00
existing_items . each do | item |
items [ item . id ] = item
relationships_by_swf_asset_id = { }
item . parent_swf_asset_relationships . each do | relationship |
relationships_by_swf_asset_id [ relationship . swf_asset_id ] = relationship
end
existing_relationships_by_item_id_and_swf_asset_id [ item . id ] =
relationships_by_swf_asset_id
end
2011-07-22 12:08:17 -07:00
# Collect existing assets
2010-10-07 07:46:23 -07:00
swf_asset_ids = [ ]
2011-01-13 14:22:07 -08:00
asset_registry . each do | asset_id , asset_data |
swf_asset_ids << asset_id . to_i if asset_data
2010-10-07 07:46:23 -07:00
end
2013-01-28 00:19:37 -08:00
existing_swf_assets = SwfAsset . object_assets . includes ( :zone ) .
2023-10-12 18:57:39 -07:00
where ( remote_id : swf_asset_ids )
2012-01-13 13:56:31 -08:00
existing_swf_assets_by_remote_id = { }
2010-10-07 07:46:23 -07:00
existing_swf_assets . each do | swf_asset |
2012-01-13 13:56:31 -08:00
existing_swf_assets_by_remote_id [ swf_asset . remote_id ] = swf_asset
2010-10-07 07:46:23 -07:00
end
2011-07-22 12:08:17 -07:00
# With each asset in the registry,
2010-10-07 07:46:23 -07:00
relationships_by_item_id = { }
2011-01-13 14:22:07 -08:00
asset_registry . each do | asset_id , asset_data |
2010-10-07 07:46:23 -07:00
if asset_data
2011-07-22 12:08:17 -07:00
# Build and update the item
2010-10-07 07:46:23 -07:00
item_id = asset_data [ :obj_info_id ] . to_i
2011-07-22 12:08:17 -07:00
next unless item_ids . include? ( item_id ) # skip incompatible (Uni Bug)
2010-10-07 07:46:23 -07:00
item = items [ item_id ]
unless item
item = Item . new
item . id = item_id
items [ item_id ] = item
end
2015-07-27 10:23:46 -07:00
item . add_origin_registry_info info_registry [ item . id . to_s ] , I18n . default_locale
2010-10-09 09:22:40 -07:00
item . current_body_id = pet_type . body_id
2011-07-22 12:08:17 -07:00
# Build and update the SWF
2012-01-13 13:56:31 -08:00
swf_asset_remote_id = asset_data [ :asset_id ] . to_i
swf_asset = existing_swf_assets_by_remote_id [ swf_asset_remote_id ]
2010-10-07 07:46:23 -07:00
unless swf_asset
swf_asset = SwfAsset . new
2012-01-13 13:56:31 -08:00
swf_asset . remote_id = swf_asset_remote_id
2010-10-07 07:46:23 -07:00
end
swf_asset . origin_object_data = asset_data
swf_asset . origin_pet_type = pet_type
2012-10-05 18:56:52 -07:00
swf_asset . item = item
2011-07-22 12:08:17 -07:00
# Build and update the relationship
2012-01-12 20:02:12 -08:00
relationship = existing_relationships_by_item_id_and_swf_asset_id [ item . id ] [ swf_asset . id ] rescue nil
2010-10-07 07:46:23 -07:00
unless relationship
relationship = ParentSwfAssetRelationship . new
2012-01-12 15:17:59 -08:00
relationship . parent = item
2010-10-07 07:46:23 -07:00
end
2012-01-12 15:17:59 -08:00
relationship . swf_asset = swf_asset
2010-10-07 07:46:23 -07:00
relationships_by_item_id [ item_id ] || = [ ]
relationships_by_item_id [ item_id ] << relationship
end
end
2011-07-22 12:08:17 -07:00
# Set up the relationships to be updated on item save
2010-10-07 07:46:23 -07:00
relationships_by_item_id . each do | item_id , relationships |
2010-10-09 09:22:40 -07:00
items [ item_id ] . parent_swf_asset_relationships_to_update = relationships
2010-10-07 07:46:23 -07:00
end
2011-07-22 12:08:17 -07:00
2010-10-07 07:46:23 -07:00
items . values
end
2010-05-14 15:41:40 -07:00
end