forked from OpenNeo/impress
Add image comparison tests for OutfitImageRenderer
This commit is contained in:
parent
55fa50c22a
commit
aa45ea17b3
6 changed files with 102 additions and 52 deletions
BIN
spec/fixtures/outfit_images/Blue Acara With Hat.png
vendored
Normal file
BIN
spec/fixtures/outfit_images/Blue Acara With Hat.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
spec/fixtures/outfit_images/Blue Acara.png
vendored
Normal file
BIN
spec/fixtures/outfit_images/Blue Acara.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
spec/fixtures/outfit_images/Hat.png
vendored
Normal file
BIN
spec/fixtures/outfit_images/Hat.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
54
spec/fixtures/zones.yml
vendored
54
spec/fixtures/zones.yml
vendored
|
|
@ -28,9 +28,9 @@ hindbiology:
|
||||||
type_id: 1
|
type_id: 1
|
||||||
label: Hind Biology
|
label: Hind Biology
|
||||||
plain_label: hindbiology
|
plain_label: hindbiology
|
||||||
markings:
|
markings1:
|
||||||
id: 31
|
id: 6
|
||||||
depth: 35
|
depth: 8
|
||||||
type_id: 2
|
type_id: 2
|
||||||
label: Markings
|
label: Markings
|
||||||
plain_label: markings
|
plain_label: markings
|
||||||
|
|
@ -88,6 +88,12 @@ body:
|
||||||
type_id: 1
|
type_id: 1
|
||||||
label: Body
|
label: Body
|
||||||
plain_label: body
|
plain_label: body
|
||||||
|
markings2:
|
||||||
|
id: 16
|
||||||
|
depth: 19
|
||||||
|
type_id: 2
|
||||||
|
label: Markings
|
||||||
|
plain_label: markings
|
||||||
bodydisease:
|
bodydisease:
|
||||||
id: 17
|
id: 17
|
||||||
depth: 20
|
depth: 20
|
||||||
|
|
@ -172,6 +178,12 @@ head:
|
||||||
type_id: 1
|
type_id: 1
|
||||||
label: Head
|
label: Head
|
||||||
plain_label: head
|
plain_label: head
|
||||||
|
markings3:
|
||||||
|
id: 31
|
||||||
|
depth: 35
|
||||||
|
type_id: 2
|
||||||
|
label: Markings
|
||||||
|
plain_label: markings
|
||||||
headdisease:
|
headdisease:
|
||||||
id: 32
|
id: 32
|
||||||
depth: 36
|
depth: 36
|
||||||
|
|
@ -196,9 +208,9 @@ glasses:
|
||||||
type_id: 2
|
type_id: 2
|
||||||
label: Glasses
|
label: Glasses
|
||||||
plain_label: glasses
|
plain_label: glasses
|
||||||
earrings:
|
earrings1:
|
||||||
id: 41
|
id: 36
|
||||||
depth: 45
|
depth: 39
|
||||||
type_id: 2
|
type_id: 2
|
||||||
label: Earrings
|
label: Earrings
|
||||||
plain_label: earrings
|
plain_label: earrings
|
||||||
|
|
@ -220,15 +232,21 @@ headdrippings:
|
||||||
type_id: 1
|
type_id: 1
|
||||||
label: Head Drippings
|
label: Head Drippings
|
||||||
plain_label: headdrippings
|
plain_label: headdrippings
|
||||||
hat:
|
hat1:
|
||||||
id: 50
|
id: 40
|
||||||
depth: 16
|
depth: 44
|
||||||
type_id: 2
|
type_id: 2
|
||||||
label: Hat
|
label: Hat
|
||||||
plain_label: hat
|
plain_label: hat
|
||||||
righthanditem:
|
earrings2:
|
||||||
id: 49
|
id: 41
|
||||||
depth: 5
|
depth: 45
|
||||||
|
type_id: 2
|
||||||
|
label: Earrings
|
||||||
|
plain_label: earrings
|
||||||
|
righthanditem1:
|
||||||
|
id: 42
|
||||||
|
depth: 46
|
||||||
type_id: 2
|
type_id: 2
|
||||||
label: Right-hand Item
|
label: Right-hand Item
|
||||||
plain_label: righthanditem
|
plain_label: righthanditem
|
||||||
|
|
@ -268,6 +286,18 @@ backgrounditem:
|
||||||
type_id: 3
|
type_id: 3
|
||||||
label: Background Item
|
label: Background Item
|
||||||
plain_label: backgrounditem
|
plain_label: backgrounditem
|
||||||
|
righthanditem2:
|
||||||
|
id: 49
|
||||||
|
depth: 5
|
||||||
|
type_id: 2
|
||||||
|
label: Right-hand Item
|
||||||
|
plain_label: righthanditem
|
||||||
|
hat2:
|
||||||
|
id: 50
|
||||||
|
depth: 16
|
||||||
|
type_id: 2
|
||||||
|
label: Hat
|
||||||
|
plain_label: hat
|
||||||
belt:
|
belt:
|
||||||
id: 51
|
id: 51
|
||||||
depth: 27
|
depth: 27
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,10 @@ require_relative '../rails_helper'
|
||||||
RSpec.describe OutfitImageRenderer do
|
RSpec.describe OutfitImageRenderer do
|
||||||
fixtures :zones, :colors, :species
|
fixtures :zones, :colors, :species
|
||||||
|
|
||||||
# Helper to create a simple PNG image (1x1 pixel) with a specific color
|
# Helper to load a fixture image
|
||||||
def create_test_png(red, green, blue, alpha = 255)
|
def load_fixture_image(filename)
|
||||||
require 'vips'
|
path = Rails.root.join('spec', 'fixtures', 'outfit_images', filename)
|
||||||
image = Vips::Image.black(1, 1, bands: 4)
|
File.read(path)
|
||||||
image = image.new_from_image([red, green, blue, alpha])
|
|
||||||
image.write_to_buffer('.png')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to create a pet state with specific swf_assets
|
# Helper to create a pet state with specific swf_assets
|
||||||
|
|
@ -84,25 +82,28 @@ RSpec.describe OutfitImageRenderer do
|
||||||
describe "#render" do
|
describe "#render" do
|
||||||
context "with a simple outfit" do
|
context "with a simple outfit" do
|
||||||
it "composites biology and item layers into a single PNG" do
|
it "composites biology and item layers into a single PNG" do
|
||||||
# Create test PNG data
|
# Load fixture images
|
||||||
red_png = create_test_png(255, 0, 0) # Red pixel
|
acara_png = load_fixture_image('Blue Acara.png')
|
||||||
blue_png = create_test_png(0, 0, 255) # Blue pixel
|
hat_png = load_fixture_image('Hat.png')
|
||||||
|
expected_composite_png = load_fixture_image('Blue Acara With Hat.png')
|
||||||
|
|
||||||
# Create biology and item assets
|
# Create biology and item assets
|
||||||
biology_asset = build_biology_asset(zones(:head), body_id: 1)
|
biology_asset = build_biology_asset(zones(:head), body_id: 1)
|
||||||
item_asset = build_item_asset(zones(:hat), body_id: 1)
|
item_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||||
|
|
||||||
# Stub HTTP requests for the actual image URLs that will be generated
|
# Stub HTTP requests for the actual image URLs that will be generated
|
||||||
stub_request(:get, biology_asset.image_url).
|
stub_request(:get, biology_asset.image_url).
|
||||||
to_return(body: red_png, status: 200)
|
to_return(body: acara_png, status: 200)
|
||||||
stub_request(:get, item_asset.image_url).
|
stub_request(:get, item_asset.image_url).
|
||||||
to_return(body: blue_png, status: 200)
|
to_return(body: hat_png, status: 200)
|
||||||
|
|
||||||
# Build outfit
|
# Build outfit
|
||||||
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
|
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
|
||||||
item = build_item("Test Hat", swf_assets: [item_asset])
|
item = build_item("Test Hat", swf_assets: [item_asset])
|
||||||
outfit = Outfit.new(pet_state: pet_state)
|
outfit = Outfit.new(
|
||||||
outfit.item_ids = { worn: [item.id], closeted: [] }
|
pet_state: pet_state,
|
||||||
|
worn_items: [item]
|
||||||
|
)
|
||||||
|
|
||||||
# Render
|
# Render
|
||||||
renderer = OutfitImageRenderer.new(outfit)
|
renderer = OutfitImageRenderer.new(outfit)
|
||||||
|
|
@ -114,29 +115,48 @@ RSpec.describe OutfitImageRenderer do
|
||||||
expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b) # PNG magic bytes
|
expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b) # PNG magic bytes
|
||||||
|
|
||||||
# Verify the result is a valid 600x600 PNG
|
# Verify the result is a valid 600x600 PNG
|
||||||
image = Vips::Image.new_from_buffer(result, "")
|
result_image = Vips::Image.new_from_buffer(result, "")
|
||||||
expect(image.width).to eq(600)
|
expect(result_image.width).to eq(600)
|
||||||
expect(image.height).to eq(600)
|
expect(result_image.height).to eq(600)
|
||||||
|
|
||||||
|
# Verify the composite matches the expected image pixel-perfectly
|
||||||
|
expected_image = Vips::Image.new_from_buffer(expected_composite_png, "")
|
||||||
|
|
||||||
|
# Calculate the absolute difference between images
|
||||||
|
diff = (result_image - expected_image).abs
|
||||||
|
max_diff = diff.max
|
||||||
|
|
||||||
|
# Allow a small tolerance for minor encoding/compositing differences
|
||||||
|
# The expected image was generated with a different method, so we expect
|
||||||
|
# very close but not necessarily pixel-perfect matches
|
||||||
|
tolerance = 2
|
||||||
|
if max_diff > tolerance
|
||||||
|
debug_path = Rails.root.join('tmp', 'test_render_result.png')
|
||||||
|
result_image.write_to_file(debug_path.to_s)
|
||||||
|
fail "Images should match within tolerance of #{tolerance}, but found max difference of #{max_diff}. Actual output saved to #{debug_path}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when a layer image fails to load" do
|
context "when a layer image fails to load" do
|
||||||
it "skips the failed layer and continues" do
|
it "skips the failed layer and continues" do
|
||||||
blue_png = create_test_png(0, 0, 255)
|
hat_png = load_fixture_image('Hat.png')
|
||||||
|
|
||||||
biology_asset = build_biology_asset(zones(:head), body_id: 1)
|
biology_asset = build_biology_asset(zones(:head), body_id: 1)
|
||||||
item_asset = build_item_asset(zones(:hat), body_id: 1)
|
item_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||||
|
|
||||||
# Stub one successful request and one failure
|
# Stub one successful request and one failure
|
||||||
stub_request(:get, biology_asset.image_url).
|
stub_request(:get, biology_asset.image_url).
|
||||||
to_return(status: 404)
|
to_return(status: 404)
|
||||||
stub_request(:get, item_asset.image_url).
|
stub_request(:get, item_asset.image_url).
|
||||||
to_return(body: blue_png, status: 200)
|
to_return(body: hat_png, status: 200)
|
||||||
|
|
||||||
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
|
pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset])
|
||||||
item = build_item("Test Hat", swf_assets: [item_asset])
|
item = build_item("Test Hat", swf_assets: [item_asset])
|
||||||
outfit = Outfit.new(pet_state: pet_state)
|
outfit = Outfit.new(
|
||||||
outfit.item_ids = { worn: [item.id], closeted: [] }
|
pet_state: pet_state,
|
||||||
|
worn_items: [item]
|
||||||
|
)
|
||||||
|
|
||||||
renderer = OutfitImageRenderer.new(outfit)
|
renderer = OutfitImageRenderer.new(outfit)
|
||||||
result = renderer.render
|
result = renderer.render
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ RSpec.describe Outfit do
|
||||||
pet_state = build_pet_state(@pet_type, swf_assets: [head, body])
|
pet_state = build_pet_state(@pet_type, swf_assets: [head, body])
|
||||||
|
|
||||||
# Create item layers
|
# Create item layers
|
||||||
hat_asset = build_item_asset(zones(:hat), body_id: 1)
|
hat_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||||
hat = build_item("Test Hat", swf_assets: [hat_asset])
|
hat = build_item("Test Hat", swf_assets: [hat_asset])
|
||||||
|
|
||||||
outfit = Outfit.new(pet_state: pet_state)
|
outfit = Outfit.new(pet_state: pet_state)
|
||||||
|
|
@ -134,7 +134,7 @@ RSpec.describe Outfit do
|
||||||
# Create a hat that restricts the hair zone
|
# Create a hat that restricts the hair zone
|
||||||
# zones_restrict is a bitfield where position 37 (Hair Front zone id) is "1"
|
# zones_restrict is a bitfield where position 37 (Hair Front zone id) is "1"
|
||||||
zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1
|
zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1
|
||||||
hat_asset = build_item_asset(zones(:hat), body_id: 1, zones_restrict: zones_restrict)
|
hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict)
|
||||||
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
|
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
|
||||||
|
|
||||||
outfit = Outfit.new(pet_state: pet_state)
|
outfit = Outfit.new(pet_state: pet_state)
|
||||||
|
|
@ -156,7 +156,7 @@ RSpec.describe Outfit do
|
||||||
|
|
||||||
# Create an item that restricts both Hair Front (37) and Head Transient Biology (38)
|
# Create an item that restricts both Hair Front (37) and Head Transient Biology (38)
|
||||||
zones_restrict = "0" * 36 + "11" + "0" * 20 # bits 37 and 38 = 1
|
zones_restrict = "0" * 36 + "11" + "0" * 20 # bits 37 and 38 = 1
|
||||||
hood_asset = build_item_asset(zones(:hat), body_id: 1, zones_restrict: zones_restrict)
|
hood_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict)
|
||||||
hood = build_item("Agent Hood", swf_assets: [hood_asset])
|
hood = build_item("Agent Hood", swf_assets: [hood_asset])
|
||||||
|
|
||||||
outfit = Outfit.new(pet_state: pet_state)
|
outfit = Outfit.new(pet_state: pet_state)
|
||||||
|
|
@ -222,7 +222,7 @@ RSpec.describe Outfit do
|
||||||
pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head, body])
|
pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head, body])
|
||||||
|
|
||||||
# Create both body-specific and body_id=0 items
|
# Create both body-specific and body_id=0 items
|
||||||
body_specific_asset = build_item_asset(zones(:hat), body_id: 1)
|
body_specific_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||||
body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset])
|
body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset])
|
||||||
|
|
||||||
universal_asset = build_item_asset(zones(:background), body_id: 0)
|
universal_asset = build_item_asset(zones(:background), body_id: 0)
|
||||||
|
|
@ -244,7 +244,7 @@ RSpec.describe Outfit do
|
||||||
pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head])
|
pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head])
|
||||||
|
|
||||||
# Create a body-specific item in a zone the pet doesn't restrict
|
# Create a body-specific item in a zone the pet doesn't restrict
|
||||||
hat_asset = build_item_asset(zones(:hat), body_id: 1)
|
hat_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||||
hat = build_item("Body-specific Hat", swf_assets: [hat_asset])
|
hat = build_item("Body-specific Hat", swf_assets: [hat_asset])
|
||||||
|
|
||||||
outfit = Outfit.new(pet_state: pet_state)
|
outfit = Outfit.new(pet_state: pet_state)
|
||||||
|
|
@ -296,7 +296,7 @@ RSpec.describe Outfit do
|
||||||
|
|
||||||
# Add an item that restricts Hair Front (37)
|
# Add an item that restricts Hair Front (37)
|
||||||
item_zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1
|
item_zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1
|
||||||
hat_asset = build_item_asset(zones(:hat), body_id: 1, zones_restrict: item_zones_restrict)
|
hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: item_zones_restrict)
|
||||||
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
|
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
|
||||||
|
|
||||||
outfit = Outfit.new(pet_state: pet_state)
|
outfit = Outfit.new(pet_state: pet_state)
|
||||||
|
|
@ -357,7 +357,7 @@ RSpec.describe Outfit do
|
||||||
|
|
||||||
# Add items at various depths
|
# Add items at various depths
|
||||||
bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4
|
bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4
|
||||||
hat_asset = build_item_asset(zones(:hat), body_id: 1) # depth 16
|
hat_asset = build_item_asset(zones(:hat1), body_id: 1) # depth 44
|
||||||
shirt_asset = build_item_asset(zones(:shirtdress), body_id: 1) # depth 26
|
shirt_asset = build_item_asset(zones(:shirtdress), body_id: 1) # depth 26
|
||||||
|
|
||||||
bg = build_item("Background Item", swf_assets: [bg_item])
|
bg = build_item("Background Item", swf_assets: [bg_item])
|
||||||
|
|
@ -370,10 +370,10 @@ RSpec.describe Outfit do
|
||||||
layers = outfit.visible_layers
|
layers = outfit.visible_layers
|
||||||
|
|
||||||
# Expected order by depth:
|
# Expected order by depth:
|
||||||
# background (3), bg_item (4), hat_asset (16), body_layer (18),
|
# background (3), bg_item (4), body_layer (18), shirt_asset (26),
|
||||||
# shirt_asset (26), head_layer (34)
|
# head_layer (34), hat_asset (44)
|
||||||
expect(layers.map(&:depth)).to eq([3, 4, 16, 18, 26, 34])
|
expect(layers.map(&:depth)).to eq([3, 4, 18, 26, 34, 44])
|
||||||
expect(layers).to eq([background, bg_item, hat_asset, body_layer, shirt_asset, head_layer])
|
expect(layers).to eq([background, bg_item, body_layer, shirt_asset, head_layer, hat_asset])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -417,7 +417,7 @@ RSpec.describe Outfit do
|
||||||
pet_state = build_pet_state(@pet_type)
|
pet_state = build_pet_state(@pet_type)
|
||||||
|
|
||||||
# Create a body-specific item for the alt style's body_id
|
# Create a body-specific item for the alt style's body_id
|
||||||
body_specific_asset = build_item_asset(zones(:hat), body_id: 999)
|
body_specific_asset = build_item_asset(zones(:hat1), body_id: 999)
|
||||||
body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset])
|
body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset])
|
||||||
|
|
||||||
# Create a universal item (body_id=0)
|
# Create a universal item (body_id=0)
|
||||||
|
|
@ -441,7 +441,7 @@ RSpec.describe Outfit do
|
||||||
pet_state = build_pet_state(@pet_type)
|
pet_state = build_pet_state(@pet_type)
|
||||||
|
|
||||||
# Create an item that fits the regular pet's body_id (1)
|
# Create an item that fits the regular pet's body_id (1)
|
||||||
regular_item_asset = build_item_asset(zones(:hat), body_id: 1)
|
regular_item_asset = build_item_asset(zones(:hat1), body_id: 1)
|
||||||
regular_item = build_item("Regular Pet Hat", swf_assets: [regular_item_asset])
|
regular_item = build_item("Regular Pet Hat", swf_assets: [regular_item_asset])
|
||||||
|
|
||||||
outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style)
|
outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style)
|
||||||
|
|
@ -463,7 +463,7 @@ RSpec.describe Outfit do
|
||||||
|
|
||||||
# Create a universal hat that restricts the hair zone
|
# Create a universal hat that restricts the hair zone
|
||||||
zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 (Hair Front) = 1
|
zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 (Hair Front) = 1
|
||||||
hat_asset = build_item_asset(zones(:hat), body_id: 0, zones_restrict: zones_restrict)
|
hat_asset = build_item_asset(zones(:hat1), body_id: 0, zones_restrict: zones_restrict)
|
||||||
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
|
hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset])
|
||||||
|
|
||||||
outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style)
|
outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style)
|
||||||
|
|
@ -526,7 +526,7 @@ RSpec.describe Outfit do
|
||||||
|
|
||||||
# Add universal items at various depths
|
# Add universal items at various depths
|
||||||
bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4
|
bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4
|
||||||
trinket = build_item_asset(zones(:righthanditem), body_id: 0) # depth 5
|
trinket = build_item_asset(zones(:righthanditem1), body_id: 0) # depth 46
|
||||||
|
|
||||||
bg = build_item("Background Item", swf_assets: [bg_item])
|
bg = build_item("Background Item", swf_assets: [bg_item])
|
||||||
trinket_item = build_item("Trinket", swf_assets: [trinket])
|
trinket_item = build_item("Trinket", swf_assets: [trinket])
|
||||||
|
|
@ -537,9 +537,9 @@ RSpec.describe Outfit do
|
||||||
layers = outfit.visible_layers
|
layers = outfit.visible_layers
|
||||||
|
|
||||||
# Expected order by depth:
|
# Expected order by depth:
|
||||||
# alt_background (3), bg_item (4), trinket (5), alt_body (18), alt_head (34)
|
# alt_background (3), bg_item (4), alt_body (18), alt_head (34), trinket (46)
|
||||||
expect(layers.map(&:depth)).to eq([3, 4, 5, 18, 34])
|
expect(layers.map(&:depth)).to eq([3, 4, 18, 34, 46])
|
||||||
expect(layers).to eq([alt_background, bg_item, trinket, alt_body, alt_head])
|
expect(layers).to eq([alt_background, bg_item, alt_body, alt_head, trinket])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue