Plugin
Decor
Create and use your own custom avatar decorations, or pick your favorite from the presets.
1
import "./ui/styles.css";2
3
import ErrorBoundary from "@components/ErrorBoundary";4
import { Devs } from "@utils/constants";5
import definePlugin from "@utils/types";6
import { UserStore } from "@webpack/common";7
8
import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants";9
import { useAuthorizationStore } from "./lib/stores/AuthorizationStore";10
import { useCurrentUserDecorationsStore } from "./lib/stores/CurrentUserDecorationsStore";11
import { useUserDecorAvatarDecoration, useUsersDecorationsStore } from "./lib/stores/UsersDecorationsStore";12
import { settings } from "./settings";13
import { setAvatarDecorationModalPreview, setDecorationGridDecoration, setDecorationGridItem } from "./ui/components";14
import DecorSection from "./ui/components/DecorSection";15
16
export interface AvatarDecoration {17
asset: string;18
skuId: string;19
}20
21
export default definePlugin({22
name: "Decor",23
description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.",24
tags: ["Appearance", "Customisation"],25
authors: [Devs.FieryFlames],26
patches: [27
// Patch MediaResolver to return correct URL for Decor avatar decorations28
{29
find: "getAvatarDecorationURL:",30
replacement: {31
match: /(?<=function \i\(\i\){)(?=let{avatarDecoration)/,32
replace: "const vcDecorDecoration=$self.getDecorAvatarDecorationURL(arguments[0]);if(vcDecorDecoration)return vcDecorDecoration;"33
}34
},35
// Patch profile customization settings to include Decor section36
{37
find: "DefaultCustomizationSections",38
replacement: {39
match: /(?<=#{intl::USER_SETTINGS_AVATAR_DECORATION}\)},"decoration"\),)/,40
replace: "$self.DecorSection(),"41
}42
},43
// Decoration modal module44
{45
find: "80,onlyAnimateOnHoverOrFocus:!",46
replacement: [47
{48
match: /(?<==)\i=>{let{children.{20,200}isSelected:\i.{0,5}\}=\i/,49
replace: "$self.DecorationGridItem=$&",50
},51
{52
match: /(?<==)\i=>{let{user:\i,avatarDecoration/,53
replace: "$self.DecorationGridDecoration=$&",54
},55
// Remove NEW label from decor avatar decorations56
{57
match: /(?<=\i\.PURCHASE)(?=,)(?<=avatarDecoration:(\i).+?)/,58
replace: "||$1.skuId===$self.SKU_ID"59
}60
]61
},62
{63
find: "isAvatarDecorationAnimating:",64
group: true,65
replacement: [66
// Add Decor avatar decoration hook to avatar decoration hook67
{68
match: /(?<=\.avatarDecoration,guildId:\i\}\)\),)(?<=user:(\i).+?)/,69
replace: "vcDecorAvatarDecoration=$self.useUserDecorAvatarDecoration($1),"70
},71
// Use added hook72
{73
match: /(?<={avatarDecoration:).{1,20}?(?=,)(?<=avatarDecorationOverride:(\i).+?)/,74
replace: "$1??vcDecorAvatarDecoration??($&)"75
},76
// Make memo depend on added hook77
{78
match: /(?<=size:\i}\),\[)/,79
replace: "vcDecorAvatarDecoration,"80
}81
]82
},83
// Current user area, at bottom of channels/dm list84
{85
find: ".DISPLAY_NAME_STYLES_COACHMARK)",86
replacement: [87
// Use Decor avatar decoration hook88
{89
match: /(?<=\i\)\({avatarDecoration:)\i(?=,)(?<=currentUser:(\i).+?)/,90
replace: "$self.useUserDecorAvatarDecoration($1)??$&"91
}92
]93
},94
...[95
"#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}", class="ts-cmt">// Messages96
"#{intl::COLLECTIBLES_NAMEPLATE_PREVIEW_A11Y}", class="ts-cmt">// Nameplate preview97
"#{intl::COLLECTIBLES_PROFILE_PREVIEW_A11Y}", class="ts-cmt">// Avatar preview98
].map(find => ({99
find,100
replacement: {101
match: /(?<=userValue:)((\i(?:\.author)?)\?\.avatarDecoration)/,102
replace: "$self.useUserDecorAvatarDecoration($2)??$1"103
}104
})),105
// Patch avatar decoration preview to display Decor avatar decorations as if they are purchased106
{107
find: "#{intl::PREMIUM_UPSELL_PROFILE_AVATAR_DECO_INLINE_UPSELL_DESCRIPTION}",108
replacement: [109
{110
match: /(?<==)\i=>{let{user:\i,guildId:\i,avatarDecoration:/,111
replace: "$self.AvatarDecorationModalPreview=$&"112
}113
]114
}115
],116
settings,117
118
flux: {119
CONNECTION_OPEN: () => {120
useAuthorizationStore.getState().init();121
useCurrentUserDecorationsStore.getState().clear();122
useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);123
},124
USER_PROFILE_MODAL_OPEN: data => {125
useUsersDecorationsStore.getState().fetch(data.userId, true);126
},127
},128
129
set DecorationGridItem(e: any) {130
setDecorationGridItem(e);131
},132
133
set DecorationGridDecoration(e: any) {134
setDecorationGridDecoration(e);135
},136
137
set AvatarDecorationModalPreview(e: any) {138
setAvatarDecorationModalPreview(e);139
},140
141
SKU_ID,142
RAW_SKU_ID,143
144
useUserDecorAvatarDecoration,145
146
async start() {147
useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);148
},149
150
getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {151
// Only Decor avatar decorations have this SKU ID152
if (avatarDecoration?.skuId === SKU_ID) {153
const parts = avatarDecoration.asset.split("_");154
// Remove a_ prefix if it's animated and animation is disabled155
if (avatarDecoration.asset.startsWith("a_") && !canAnimate) parts.shift();156
return `${CDN_URL}/${parts.join("_")}.png`;157
} else if (avatarDecoration?.skuId === RAW_SKU_ID) {158
return avatarDecoration.asset;159
}160
},161
162
DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })163
});164