mirror of
https://github.com/brues-code/TwitchEmotes.git
synced 2026-09-22 07:36:58 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f1e198e4e | |||
| 87147d61ac | |||
| 110d8c3002 | |||
| 9cd7911c73 | |||
| 0f20f91466 | |||
| b466f0b2a5 | |||
| 0470d5bbc6 | |||
| 3fe9ed86b8 | |||
| 6348b85740 | |||
| bea278e730 | |||
| ce8038ed03 | |||
| a9297cc58b |
@@ -0,0 +1,180 @@
|
||||
name: Mirror release
|
||||
|
||||
# Copy a GitHub release — its notes and the files attached to it — across to
|
||||
# the Gitea mirror at https://octowow.st/git/brues/TwitchEmotes.
|
||||
#
|
||||
# A Gitea pull mirror replicates commits and tags only. Releases are database
|
||||
# records with their own attachments, so they never cross: the mirror's
|
||||
# Releases page shows bare tags with nothing to download. This workflow pushes
|
||||
# them over through Gitea's API.
|
||||
#
|
||||
# ONE-TIME SETUP: on the mirror, go to Settings -> Applications -> Generate New
|
||||
# Token, tick `repository` write, and copy the token. On GitHub, add it under
|
||||
# Settings -> Secrets and variables -> Actions as `MIRROR_TOKEN`. The token
|
||||
# needs push rights on brues/TwitchEmotes.
|
||||
#
|
||||
# Manual only — nothing calls it, and a release does not trigger it. Run it from
|
||||
# the Actions UI with a `tag` to mirror one release, or with `tag` left blank to
|
||||
# backfill every release that exists on GitHub.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to mirror (e.g. v2.0.0). Blank = every release.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# No checkout: the release files come from the GitHub release itself, so
|
||||
# this reads the same bytes a user downloads rather than a rebuild.
|
||||
- name: Push release to the mirror
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
MIRROR_TOKEN: ${{ secrets.MIRROR_TOKEN }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${MIRROR_TOKEN:-}" ]; then
|
||||
echo "::error::MIRROR_TOKEN is not set - see the setup note at the top of .github/workflows/mirror-release.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API=https://octowow.st/git/api/v1
|
||||
MIRROR=brues/TwitchEmotes
|
||||
RESP=$(mktemp)
|
||||
|
||||
# Sends one API call. Prints the HTTP status; leaves the body in $RESP.
|
||||
req() {
|
||||
local method=$1 path=$2
|
||||
shift 2
|
||||
curl -sS -m 600 -o "$RESP" -w '%{http_code}' -X "$method" \
|
||||
-H "Authorization: token $MIRROR_TOKEN" \
|
||||
-H 'Accept: application/json' \
|
||||
"$API/$path" "$@"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "::error::$1"
|
||||
echo "the mirror replied: $(head -c 2000 "$RESP")"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# A release must hang off a tag that is already on the mirror. If the
|
||||
# tag is missing the mirror has not pulled yet, so ask it to and wait.
|
||||
# Attaching to a tag the mirror does not have would let its next sync
|
||||
# turn our release back into a draft.
|
||||
ensure_tag() {
|
||||
local tag=$1 i
|
||||
if [ "$(req GET "repos/$MIRROR/tags/$tag")" = 200 ]; then
|
||||
return 0
|
||||
fi
|
||||
echo " tag $tag is not on the mirror yet - asking it to sync"
|
||||
req POST "repos/$MIRROR/mirror-sync" > /dev/null || true
|
||||
for i in $(seq 1 30); do
|
||||
sleep 10
|
||||
if [ "$(req GET "repos/$MIRROR/tags/$tag")" = 200 ]; then
|
||||
echo " tag $tag arrived after $(( i * 10 ))s"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fail "tag $tag never reached the mirror. Sync it, then run this workflow again."
|
||||
}
|
||||
|
||||
mirror_one() {
|
||||
local tag=$1 work code id f name size have_id have_size enc
|
||||
work=$(mktemp -d)
|
||||
echo "=== $tag ==="
|
||||
|
||||
gh release view "$tag" --json tagName,name,body,isDraft,isPrerelease > "$work/gh.json"
|
||||
if [ "$(jq -r .isDraft "$work/gh.json")" = true ]; then
|
||||
echo " still a draft on GitHub - skipped"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ensure_tag "$tag"
|
||||
|
||||
jq '{tag_name: .tagName,
|
||||
name: (.name // .tagName),
|
||||
body: (.body // ""),
|
||||
draft: false,
|
||||
prerelease: .isPrerelease}' "$work/gh.json" > "$work/release.json"
|
||||
|
||||
# Gitea's tag sync leaves a bare tag record behind. Patching it
|
||||
# promotes that record to a real release instead of colliding.
|
||||
code=$(req GET "repos/$MIRROR/releases/tags/$tag")
|
||||
if [ "$code" = 200 ]; then
|
||||
id=$(jq -r .id "$RESP")
|
||||
code=$(req PATCH "repos/$MIRROR/releases/$id" \
|
||||
-H 'Content-Type: application/json' --data-binary @"$work/release.json")
|
||||
if [ "$code" != 200 ]; then
|
||||
fail "could not update release $tag on the mirror (HTTP $code)"
|
||||
fi
|
||||
echo " updated release #$id"
|
||||
else
|
||||
code=$(req POST "repos/$MIRROR/releases" \
|
||||
-H 'Content-Type: application/json' --data-binary @"$work/release.json")
|
||||
if [ "$code" != 201 ]; then
|
||||
fail "could not create release $tag on the mirror (HTTP $code)"
|
||||
fi
|
||||
id=$(jq -r .id "$RESP")
|
||||
echo " created release #$id"
|
||||
fi
|
||||
|
||||
mkdir -p "$work/assets"
|
||||
# Exits non-zero when the release carries no files, which is fine.
|
||||
gh release download "$tag" --dir "$work/assets" --clobber || true
|
||||
|
||||
code=$(req GET "repos/$MIRROR/releases/$id/assets")
|
||||
if [ "$code" != 200 ]; then
|
||||
fail "could not list the mirror's attachments for $tag (HTTP $code)"
|
||||
fi
|
||||
cp "$RESP" "$work/have.json"
|
||||
|
||||
for f in "$work"/assets/*; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f")
|
||||
size=$(stat -c %s "$f")
|
||||
have_id=$(jq -r --arg n "$name" 'map(select(.name == $n)) | .[0].id // ""' "$work/have.json")
|
||||
have_size=$(jq -r --arg n "$name" 'map(select(.name == $n)) | .[0].size // ""' "$work/have.json")
|
||||
|
||||
# Same name and same size: already mirrored. A size mismatch means
|
||||
# a re-cut release, so replace it - Gitea would otherwise keep both
|
||||
# and serve the stale one from the older link.
|
||||
if [ -n "$have_id" ] && [ "$have_size" = "$size" ]; then
|
||||
echo " $name is already there"
|
||||
continue
|
||||
fi
|
||||
if [ -n "$have_id" ]; then
|
||||
req DELETE "repos/$MIRROR/releases/$id/assets/$have_id" > /dev/null
|
||||
fi
|
||||
|
||||
enc=$(jq -rn --arg n "$name" '$n | @uri')
|
||||
code=$(req POST "repos/$MIRROR/releases/$id/assets?name=$enc" -F "attachment=@$f")
|
||||
if [ "$code" != 201 ]; then
|
||||
fail "could not upload $name (HTTP $code)"
|
||||
fi
|
||||
echo " uploaded $name ($size bytes)"
|
||||
done
|
||||
}
|
||||
|
||||
if [ -n "${TAG:-}" ]; then
|
||||
mirror_one "$TAG"
|
||||
else
|
||||
echo "no tag given - backfilling every release on GitHub"
|
||||
gh release list --limit 200 --json tagName --jq '.[].tagName' > /tmp/tags.txt
|
||||
# fd 3 keeps gh and curl from eating the tag list off stdin.
|
||||
while read -r t <&3; do
|
||||
if [ -n "$t" ]; then
|
||||
mirror_one "$t"
|
||||
fi
|
||||
done 3< /tmp/tags.txt
|
||||
fi
|
||||
@@ -1,4 +1,5 @@
|
||||
package-as: TwitchEmotes
|
||||
changelog-title: TwitchEmotes
|
||||
|
||||
ignore:
|
||||
- .luarc.json
|
||||
|
||||
+18
@@ -4270,6 +4270,12 @@ defaultpack={
|
||||
[":Awkward:"]="Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\Awkward.tga:28:28:0:0:128:1024:0:32:0:32",
|
||||
["catKISS"]="Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\catKISS.tga:28:28:0:0:64:1024:0:32:0:32",
|
||||
["Maaaaan"]="Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\Maaaaan.tga:28:28",
|
||||
["Tssk"]="Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\Tssk.tga:28:28:0:0:32:416:0:32:0:32",
|
||||
["TheVoices"]="Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\TheVoices.tga:28:28:0:0:128:1024:0:32:0:32",
|
||||
["SpeedLaugh"]="Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\SpeedLaugh.tga:28:32:0:0:36:928:0:36:0:32",
|
||||
["YouTried"]="Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\YouTried.tga:28:28:0:0:32:576:0:32:0:32",
|
||||
["DIESOFCRINGE"]="Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\DIESOFCRINGE.tga:28:28:0:0:128:896:0:32:0:32",
|
||||
["DisGonBGud"]="Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\DisGonBGud.tga:28:28:0:0:128:1024:0:32:0:32",
|
||||
|
||||
};
|
||||
emoticons={
|
||||
@@ -8950,6 +8956,12 @@ defaultpack={
|
||||
[":Awkward:"]=":Awkward:",
|
||||
["catKISS"]="catKISS",
|
||||
["Maaaaan"]="Maaaaan",
|
||||
["Tssk"]="Tssk",
|
||||
["TheVoices"]="TheVoices",
|
||||
["SpeedLaugh"]="SpeedLaugh",
|
||||
["YouTried"]="YouTried",
|
||||
["DIESOFCRINGE"]="DIESOFCRINGE",
|
||||
["DisGonBGud"]="DisGonBGud",
|
||||
|
||||
};
|
||||
|
||||
@@ -9076,4 +9088,10 @@ TwitchEmotes_animation_metadata = {
|
||||
["Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\DONOTCUM.tga"] = {["nFrames"] = 46, ["frameWidth"] = 32, ["frameHeight"] = 32, ["imageWidth"]=64, ["imageHeight"]=1024, ["framerate"] = 25},
|
||||
["Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\Awkward.tga"] = {["nFrames"] = 76, ["frameWidth"] = 32, ["frameHeight"] = 32, ["imageWidth"]=128, ["imageHeight"]=1024, ["framerate"] = 10},
|
||||
["Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\catKISS.tga"] = {["nFrames"] = 60, ["frameWidth"] = 32, ["frameHeight"] = 32, ["imageWidth"]=64, ["imageHeight"]=1024, ["framerate"] = 17},
|
||||
["Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\Tssk.tga"] = {["nFrames"] = 13, ["frameWidth"] = 32, ["frameHeight"] = 32, ["imageWidth"]=32, ["imageHeight"]=416, ["framerate"] = 30},
|
||||
["Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\TheVoices.tga"] = {["nFrames"] = 127, ["frameWidth"] = 32, ["frameHeight"] = 32, ["imageWidth"]=128, ["imageHeight"]=1024, ["framerate"] = 21},
|
||||
["Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\SpeedLaugh.tga"] = {["nFrames"] = 29, ["frameWidth"] = 36, ["frameHeight"] = 32, ["imageWidth"]=36, ["imageHeight"]=928, ["framerate"] = 6},
|
||||
["Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\YouTried.tga"] = {["nFrames"] = 18, ["frameWidth"] = 32, ["frameHeight"] = 32, ["imageWidth"]=32, ["imageHeight"]=576, ["framerate"] = 10},
|
||||
["Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\DIESOFCRINGE.tga"] = {["nFrames"] = 111, ["frameWidth"] = 32, ["frameHeight"] = 32, ["imageWidth"]=128, ["imageHeight"]=896, ["framerate"] = 30},
|
||||
["Interface\\AddOns\\TwitchEmotes\\Emotes\\Custom\\DisGonBGud.tga"] = {["nFrames"] = 125, ["frameWidth"] = 32, ["frameHeight"] = 32, ["imageWidth"]=128, ["imageHeight"]=1024, ["framerate"] = 25},
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 512 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 512 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,42 @@
|
||||
[](https://github.com/brues-code/ClassicAPI)
|
||||
|
||||
# TwitchEmotes
|
||||
|
||||
Twitch, BetterTTV, FrankerFaceZ and Discord emotes in World of Warcraft 1.12.1.
|
||||
|
||||
Type `Kappa`, `PepeLaugh` or `:skull:` in chat and it comes out as the emote — for you and for
|
||||
everyone else running the addon. Over 4,000 codes across 28 packs, 122 of them animated, rendered
|
||||
inline in chat, chat bubbles and mail.
|
||||
|
||||
Originally written for retail by **Ren**; backported to vanilla by **Brues**.
|
||||
|
||||
## Features
|
||||
|
||||
- **Emotes everywhere** — say, yell, guild, officer, whisper, party, raid, battleground, channels
|
||||
and in-game mail. Each channel can be toggled on its own.
|
||||
- **Animated emotes** at ~30fps, in chat, in the picker, and over chat bubbles.
|
||||
- **Autocomplete** — type `:` plus two characters for a ranked popup (prefix, then substring, then
|
||||
fuzzy). Tab cycles the suggestions, space or a click accepts one.
|
||||
- **Emote picker** on the minimap button, grouped by pack. Mark the packs you use as favourites and
|
||||
hide the rest.
|
||||
- **Clickable emotes** — hover an emote in chat to see its code, shift-click to put it in your
|
||||
edit box.
|
||||
- **Usage statistics** — how often you have sent and seen each emote (shift-click the minimap
|
||||
button).
|
||||
|
||||
Minimap button: click for the picker, shift-click for statistics, right-click for options.
|
||||
|
||||
## Requirements
|
||||
|
||||
[ClassicAPI](https://github.com/brues-code/ClassicAPI) is required. The 1.12 client cannot draw
|
||||
cropped inline textures on its own, which is what animated emotes and sprite sheets need. Without
|
||||
the DLL loaded the addon disables itself and points you at the download.
|
||||
|
||||
## Adding an emote
|
||||
|
||||
`tools/add_emote.py` takes a BetterTTV emote, converts it to a texture the 1.12 client can decode,
|
||||
and registers it:
|
||||
|
||||
```
|
||||
python tools/add_emote.py https://betterttv.com/emotes/<id> PepeCool
|
||||
```
|
||||
+1
-1
@@ -71,7 +71,7 @@ Emoticons_Settings={
|
||||
[1]= {"Asmongold","asmon1","asmon2","asmon3","asmon4","asmonBoi","asmonC","asmonCD","asmonD","asmonDad","asmonDaze","asmonDegen","asmonE","asmonE1","asmonE2","asmonE3","asmonE4","asmonFiend","asmonG","asmonGASM","asmonGet","asmonHide","asmonL","asmonLFR","asmonLong1","asmonLong2","asmonLong3","asmonLong4","asmonLove","asmonM","asmonOcean","asmonOrc","asmonP","asmonPower","asmonPray","asmonPrime","asmonR","asmonREE","asmonSad","asmonStare","asmonTar","asmonTiger","asmonUH","asmonW","asmonWHAT","asmonWHATR","asmonWOW"},
|
||||
[2]= {"Preachlfw","pgeBan","pgeBen","pgeBrian","pgeCheese","pgeChick","pgeClub","pgeCrisp","pgeDrama","pgeEdge","pgeEmma","pgeFish","pgeGhost","pgeHmm","pgeNem","pgeNoob","pgeOhno","pgeOhno2","pgePog","pgePug","pgeRay","pgeScience","pgeShame","pgeSherry"},
|
||||
[3]= {"BTTV+FFZ","4Head","ANELE","AngelThump","BabyRage","BBona","BibleThump","BlessRNG","BloodTrail","bUrself","cmonBrother","cmonBrug","cmonBruh","ConcernDoge","ConcernFroge","D:","DansGame","DatSheffy","DD:","DogeWitIt","EleGiggle","eShrug","FacePalm","FailFish","FrankerZ","GabeN","gachiBASS","gachiGASM","gachiHYPER","GivePLZ","haHAA","HandsUp","HeyGuys","HotPokket","HYPERBRUG","HYPERLUL","HYPERTHONK","Jebaited","Kapp","Kappa","KappaPride","Keepo","KKomrade","Kreygasm","LUL","LULE","LULW","MegaLUL","MingLee","MingLUL","MrDestructoid","NickyQ","NotLike","NotLikeThis","o_O","OhMyDog","OpieOP","PagMan","PartyTime","pokiBASS","PotFriend","PowerUpL","PowerUpR","RCool","ResidentSleeper","RFrown","RGasp","RHeart","RMeh","RPatch","RSmile","RSmiling","RTired","RTongue","RWink","RWinkTongue","SeemsGood","smileC","smileW","SMOrc","Squid1","Squid2","Squid3","Squid4","SwiftRage","TakeNRG","Thonk","tooDank","TriHard","weSmart","WideHard","WinnerWinner","WTFF","Wut","WutFace","ZULOL","ZULUL"},
|
||||
[4]= {"Custom","4Play","4WeirdBusiness",":alarm:",":alert:",":Aware:",":Awkward:",":Awoken:",":blinking:",":Cinema:",":Classic:",":Clueless:",":Concerned:",":cringe:",":doo:",":empty:",":ew:",":groove:",":hehe:",":hehesip:",":horror:",":jons:",":kissing:",":kms:",":mc:",":meow:",":misery:",":mo:",":noted:",":om:",":penguin:",":pls:",":queen:",":quest:",":sitch:",":soy:",":torment:",":waiting:",":WM:",":xdd:",":yo:","AbdulPls","AINTNOWAY","AlienPls","AMAZIN","ANEBruh","angrybear","arnoldHalt","AYAYA","bange","batBrah","BedgeMorgan","bidenBlast","bign","billyReady","bjornoVV","bjornoVVona","blobDance","BOGGED","bruggaroniNcheese","bufeY","businesscat","Catablepon","CatDance","catJAM","catKISS","CatMad","CatPop","channMies","channWeen","ChipiChipi","chompy","chupBro","chupDerp","chupHappy","ciaciuu","ciaciuWorried","Clap","Cluegi","cmon","confuseddoggo","cptfriHE","CUNGUS","deanWink","Del","Depresstiny","DestiSenpaii","Disgustiny","DocArrive","DocLeave","docmeat","DogeKek","DONOTCUM","DonoWall","ednasly","Eggroll","endANELE","endBomb","endDawg","endFrench","endHarambe","endKyori","endNotLikeThis","endRP","endTrump","evilcat","fentL","FlipThis","FLOOSHED","flushdoggo","foxsus","freddyCREEP","freddyFINGER","freddyLUL","freddyW","FrogPog","fruitBug","GAMING","GasmChamp","GigaChad","GODSTINY","goinginsane","HappyMerchant","headBang","hehecat","HiveDiver","HmmStiny","HmmTodayIWill","HowSleeper","HUH","HUHH","HyperAngryMerchant","HyperMpreg","HYPERYOGGERS","ICANT","intjCaught","intjdreaming","intjGlad","intjIdk","intjMad","intjReallyMad","intjSad","intjSalute","intjShake","intjSmile","intjSus","intjWoa","intjXD1","intjXD2","jerryWhat","jewdai","jillzoMallet","jinL","Jons","Joy2","Kekflap","KEKok","KEKWait","KermitScream","KKonaS","Klappa","KomodoHype","Krug","LaurGasm","lebronJAM","LeoHug","LeRuse","LETHIMCOOK","LETSGOOO","lockOmegatayys","LockStone","Lole","LOLW","Maaaaan","marcithDerp","marcithMath","mastahFloor","megaflushed","MikeSmug","modCheck","MoneySniff","monkeyS","MooCowGold","MooCowW","moWait","MrStark","NeckBeard","NoAnime","NoBitches","NOBULLY","NoTears","OKKona","oldmorDim","OMEGAKEKMAN","OMEGALOL","omgBruh","OverRustle","PatrickMad","PatrickPray","pedro","peepoVW","PEPE","PogCena","PogT","poki1","poki2","pokiW","politeCatWTF","popCat","Popoga","PunkChamp","RAGEY","ratJAM","REE","ricardoFlick","RIPBOZO","sameEnergy","selyihHEY","SNIFFA","StareBruh","Stonks","SuicideThinking","sunglassesflushed","SURPRISE","susfox","Sussy","SWEATSTINY","TeaTime","ThisIsFine","ThumbsUp","totekatze","TrollDespair","TrollLaugh","VoHiYo","WeirdAYAYA","worryChocolate","worryCool","worryExcite","worryFat","worryHug","worryHugged","worryLove","worryPopcorn","worryStick","worryWave","WrathChest","yacubgasm","YEE","yeshoney","YodaSip","YOGGERS","YoshiBlush","ZOINKS"},
|
||||
[4]= {"Custom","4Play","4WeirdBusiness",":alarm:",":alert:",":Aware:",":Awkward:",":Awoken:",":blinking:",":Cinema:",":Classic:",":Clueless:",":Concerned:",":cringe:",":doo:",":empty:",":ew:",":groove:",":hehe:",":hehesip:",":horror:",":jons:",":kissing:",":kms:",":mc:",":meow:",":misery:",":mo:",":noted:",":om:",":penguin:",":pls:",":queen:",":quest:",":sitch:",":soy:",":torment:",":waiting:",":WM:",":xdd:",":yo:","AbdulPls","AINTNOWAY","AlienPls","AMAZIN","ANEBruh","angrybear","arnoldHalt","AYAYA","bange","batBrah","BedgeMorgan","bidenBlast","bign","billyReady","bjornoVV","bjornoVVona","blobDance","BOGGED","bruggaroniNcheese","bufeY","businesscat","Catablepon","CatDance","catJAM","catKISS","CatMad","CatPop","channMies","channWeen","ChipiChipi","chompy","chupBro","chupDerp","chupHappy","ciaciuu","ciaciuWorried","Clap","Cluegi","cmon","confuseddoggo","cptfriHE","CUNGUS","deanWink","Del","Depresstiny","DestiSenpaii","DIESOFCRINGE","DisGonBGud","Disgustiny","DocArrive","DocLeave","docmeat","DogeKek","DONOTCUM","DonoWall","ednasly","Eggroll","endANELE","endBomb","endDawg","endFrench","endHarambe","endKyori","endNotLikeThis","endRP","endTrump","evilcat","fentL","FlipThis","FLOOSHED","flushdoggo","foxsus","freddyCREEP","freddyFINGER","freddyLUL","freddyW","FrogPog","fruitBug","GAMING","GasmChamp","GigaChad","GODSTINY","goinginsane","HappyMerchant","headBang","hehecat","HiveDiver","HmmStiny","HmmTodayIWill","HowSleeper","HUH","HUHH","HyperAngryMerchant","HyperMpreg","HYPERYOGGERS","ICANT","intjCaught","intjdreaming","intjGlad","intjIdk","intjMad","intjReallyMad","intjSad","intjSalute","intjShake","intjSmile","intjSus","intjWoa","intjXD1","intjXD2","jerryWhat","jewdai","jillzoMallet","jinL","Jons","Joy2","Kekflap","KEKok","KEKWait","KermitScream","KKonaS","Klappa","KomodoHype","Krug","LaurGasm","lebronJAM","LeoHug","LeRuse","LETHIMCOOK","LETSGOOO","lockOmegatayys","LockStone","Lole","LOLW","Maaaaan","marcithDerp","marcithMath","mastahFloor","megaflushed","MikeSmug","modCheck","MoneySniff","monkeyS","MooCowGold","MooCowW","moWait","MrStark","NeckBeard","NoAnime","NoBitches","NOBULLY","NoTears","OKKona","oldmorDim","OMEGAKEKMAN","OMEGALOL","omgBruh","OverRustle","PatrickMad","PatrickPray","pedro","peepoVW","PEPE","PogCena","PogT","poki1","poki2","pokiW","politeCatWTF","popCat","Popoga","PunkChamp","RAGEY","ratJAM","REE","ricardoFlick","RIPBOZO","sameEnergy","selyihHEY","SNIFFA","SpeedLaugh","StareBruh","Stonks","SuicideThinking","sunglassesflushed","SURPRISE","susfox","Sussy","SWEATSTINY","TeaTime","TheVoices","ThisIsFine","ThumbsUp","totekatze","TrollDespair","TrollLaugh","Tssk","VoHiYo","WeirdAYAYA","worryChocolate","worryCool","worryExcite","worryFat","worryHug","worryHugged","worryLove","worryPopcorn","worryStick","worryWave","WrathChest","yacubgasm","YEE","yeshoney","YodaSip","YOGGERS","YoshiBlush","YouTried","ZOINKS"},
|
||||
[5]= {"Greekgodx","greekA","greekBrow","greekDiet","greekGirl","greekGordo","greekGweek","greekHard","greekHYPERP","greekJoy","greekKek","greekM","greekMlady","greekOi","greekP","greekPVC","greekSad","greekSheep","greekSleeper","greekSquad","greekT","greekThink","greekTilt","greekWC","greekWhy","greekWtf","greekYikes"},
|
||||
[6]= {"nymn","nymn0","nymn1","nymn158","nymn2","nymn2x","nymn3","nymnA","nymnAww","nymnB","nymnBee","nymnBenis","nymnBiggus","nymnBridge","nymnC","nymnCaptain","nymnCC","nymnCD","nymnCozy","nymnCREB","nymnCringe","nymnCry","nymnDab","nymnDeer","nymnE","nymnElf","nymnEU","nymnEZ","nymnFEEDME","nymnFlag","nymnFlick","nymnFood","nymnG","nymnGasm","nymnGasp","nymnGnome","nymnGold","nymnGolden","nymnGun","nymnH","nymnHammer","nymnHmm","nymnHonk","nymnHydra","nymnJoy","nymnK","nymnKek","nymnKing","nymnKomrade","nymnL","nymnM","nymnNA","nymnNo","nymnNormie","nymnOkay","nymnP","nymnPains","nymnPog","nymnPuke","nymnR","nymnRaffle","nymnRupert","nymnS","nymnSad","nymnScuffed","nymnSleeper","nymnSmart","nymnSmol","nymnSmug","nymnSon","nymnSoy","nymnSpurdo","nymnStrong","nymnThink","nymnTransparent","nymnU","nymnV","nymnW","nymnWhy","nymnXD","nymnY","nymnZ"},
|
||||
[7]= {"Drainerx","drxBrain","drxCozy","drxCri","drxCS","drxD","drxDict","drxED","drxED2","drxEyes","drxFE","drxFE1","drxFEED","drxGlad","drxGod","drxHappy","drxHey","drxKEK","drxLewd","drxLit","drxLUL","drxMad","drxmonkaEYES","drxPog","drxR","drxSad","drxSmart","drxSmile","drxSpace","drxSSJ","drxThink","drxW","drxWeird","drxWink"},
|
||||
|
||||
@@ -21,11 +21,11 @@ local function GetCurrentFrameNum(animdata)
|
||||
return math.floor((TWITCHEMOTES_T * animdata.framerate) % animdata.nFrames)
|
||||
end
|
||||
|
||||
-- Frames run row-major across however many columns the sheet is wide. Two
|
||||
-- client limits force that: no texture side may exceed 1024, and a texture
|
||||
-- skinnier than 16:1 doesn't draw at all -- a 32x1024 strip renders nothing,
|
||||
-- while the same frames as 64x512 render fine. So a run longer than 16 frames
|
||||
-- is packed in columns rather than as one tall strip.
|
||||
-- Frames run row-major across however many columns the sheet is wide, since a
|
||||
-- run longer than 32 frames outgrows the 1024px decode scratch as one strip.
|
||||
-- Older sheets are packed more densely than that: before ClassicAPI's texture
|
||||
-- dimension gate a sheet skinnier than 16:1 drew nothing, so runs wrapped at 16
|
||||
-- frames. Deriving the column count from the sheet handles either.
|
||||
local function GetFrameRect(animdata, framenum)
|
||||
local cols = math.floor(animdata.imageWidth / animdata.frameWidth)
|
||||
if cols < 1 then cols = 1 end
|
||||
|
||||
+16
-8
@@ -49,8 +49,13 @@ local WIN_H = 516
|
||||
|
||||
local FALLBACK_TEX = "Interface\\AddOns\\TwitchEmotes\\Emotes\\1337.tga"
|
||||
|
||||
local BORDER_TEX = "Interface\\ItemSocketingFrame\\UI-EngineeringSockets"
|
||||
local BORDER = { left = 0.015625, right = 0.6875, top = 0.41210938, bottom = 0.49609375 }
|
||||
-- Same panel styling as the autocomplete popup.
|
||||
local FEATURED_BACKDROP = {
|
||||
bgFile = "Interface\\Buttons\\WHITE8X8",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true, tileSize = 8, edgeSize = 8,
|
||||
insets = { left = 3, right = 3, top = 3, bottom = 3 },
|
||||
}
|
||||
|
||||
local sentKeys = {}
|
||||
local seenKeys = {}
|
||||
@@ -184,15 +189,18 @@ local function makeFeatured(f, label, centerX)
|
||||
title:SetPoint("CENTER", f, "TOPLEFT", centerX, -52)
|
||||
title:SetText(label)
|
||||
|
||||
local border = f:CreateTexture(nil, "ARTWORK")
|
||||
border:SetTexture(BORDER_TEX)
|
||||
-- a frame rather than a texture, so the emote can sit on its OVERLAY layer
|
||||
-- above the backdrop
|
||||
local border = CreateFrame("Frame", nil, f)
|
||||
border:SetSize(86, 86)
|
||||
border:SetTexCoord(BORDER.left, BORDER.right, BORDER.top, BORDER.bottom)
|
||||
border:SetPoint("CENTER", f, "TOPLEFT", centerX, -108)
|
||||
border:SetBackdrop(FEATURED_BACKDROP)
|
||||
border:SetBackdropColor(0.05, 0.05, 0.05, 0.95)
|
||||
border:SetBackdropBorderColor(1, 1, 1, 1)
|
||||
|
||||
local tex = f:CreateTexture(nil, "OVERLAY")
|
||||
local tex = border:CreateTexture(nil, "OVERLAY")
|
||||
tex:SetSize(70, 70)
|
||||
tex:SetPoint("CENTER", f, "TOPLEFT", centerX, -108)
|
||||
tex:SetPoint("CENTER", border, "CENTER", 0, 0)
|
||||
|
||||
local cap = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
||||
cap:SetPoint("CENTER", f, "TOPLEFT", centerX, -160)
|
||||
@@ -212,7 +220,7 @@ local function buildWindow()
|
||||
f:SetScript("OnDragStop", f.StopMovingOrSizing)
|
||||
f:SetFrameStrata("HIGH")
|
||||
f:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
|
||||
+32
-24
@@ -7,12 +7,12 @@
|
||||
Downloads the emote, converts it to a texture the 1.12 client can decode, and
|
||||
registers it in Emotes.lua (and in the minimap dropdown's pack list).
|
||||
|
||||
Two client constraints drive the conversion:
|
||||
Two constraints drive the conversion:
|
||||
|
||||
* Textures are capped at 1024px per side and power-of-two, so an animation
|
||||
longer than 32 frames is packed row-major across several 32px columns
|
||||
rather than one over-tall strip. TwitchEmotesAnimator derives the column
|
||||
count from imageWidth / frameWidth.
|
||||
* A sheet may not exceed 1024px per side, so an animation longer than 32
|
||||
frames is packed row-major across several 32px columns rather than one
|
||||
over-tall strip. TwitchEmotesAnimator derives the column count from
|
||||
imageWidth / frameWidth.
|
||||
* The animator plays frames at one constant rate off a ~30fps ticker, but a
|
||||
GIF holds each frame for as long as it likes. The source timeline is
|
||||
resampled at a constant rate instead: a long hold repeats, and a source
|
||||
@@ -32,8 +32,7 @@ from PIL import Image, ImageSequence
|
||||
CDN = 'https://cdn.betterttv.net/emote/%s/%s'
|
||||
FRAME = 32 # cell height in the sheet; a wide emote's cell is wider
|
||||
DISPLAY = 28 # rendered height in a chat line
|
||||
MAX_TEXTURE = 1024 # client cap, per side
|
||||
MAX_ASPECT = 16 # see layout(): skinnier than this and nothing draws
|
||||
MAX_TEXTURE = 1024 # side of the decode scratch, per side
|
||||
MAX_COLS = 4 # 4 * 32 = 128px wide, 128 frames at most
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@@ -127,20 +126,30 @@ def resample(durations, budget):
|
||||
return fps, indices
|
||||
|
||||
|
||||
def pot(n):
|
||||
v = 1
|
||||
while v < n:
|
||||
v *= 2
|
||||
return v
|
||||
def max_frames(cell):
|
||||
"""Most frames `layout` can pack for this cell shape within MAX_TEXTURE.
|
||||
|
||||
A non-square cell stays single-column (see `layout`), so its budget is
|
||||
shallower than a square cell's, which can spread across MAX_COLS.
|
||||
"""
|
||||
cw, ch = cell
|
||||
cols = MAX_COLS if cw == FRAME else 1
|
||||
return cols * (MAX_TEXTURE // ch)
|
||||
|
||||
|
||||
def layout(count, cell):
|
||||
"""Column count and texture size the client can actually sample.
|
||||
"""Column count and texture size for `count` frames.
|
||||
|
||||
Two limits, both found the hard way: no side may exceed 1024, and the sheet
|
||||
may not be skinnier than 16:1 - a 32x1024 strip (32:1) draws nothing at all,
|
||||
while the same frames as 64x512 draw fine. So a run longer than 16 frames
|
||||
goes to two columns rather than growing the strip.
|
||||
ClassicAPI's dimension gate lifts 1.12's power-of-two rule and grows the
|
||||
decode scratch to fit, so the sheet is sized to its frames exactly. One
|
||||
limit is left - the scratch's own side, 1024 - which a run longer than 32
|
||||
frames outgrows, so it wraps into further 32px columns.
|
||||
|
||||
(A sheet skinnier than 16:1 used to draw nothing, which forced columns much
|
||||
sooner. That was never a client rule: VanillaHelpers grew the texture
|
||||
recycle pool to 6x6 but left the index stride at 5, so a 32x1024 strip
|
||||
collided with a 64x32 bucket and got handed back the wrong texture. The gate
|
||||
rewrites both index sites to stride 6.)
|
||||
|
||||
A non-square cell stays single-column: the animator derives its column count
|
||||
as imageWidth / frameWidth, which only holds when the cell tiles the texture
|
||||
@@ -148,14 +157,13 @@ def layout(count, cell):
|
||||
"""
|
||||
cw, ch = cell
|
||||
for cols in ((1,) if cw != FRAME else (1, 2, 4)):
|
||||
width = pot(cols * cw)
|
||||
width = cols * cw
|
||||
rows = -(-count // cols)
|
||||
height = pot(rows * ch)
|
||||
if (max(width, height) <= MAX_TEXTURE and
|
||||
height <= width * MAX_ASPECT and width <= height * MAX_ASPECT):
|
||||
height = rows * ch
|
||||
if max(width, height) <= MAX_TEXTURE:
|
||||
return cols, width, height
|
||||
sys.exit('%d frames of %dx%d do not fit a sheet within %dpx and %d:1'
|
||||
% (count, cw, ch, MAX_TEXTURE, MAX_ASPECT))
|
||||
sys.exit('%d frames of %dx%d do not fit a sheet within %dpx'
|
||||
% (count, cw, ch, MAX_TEXTURE))
|
||||
|
||||
|
||||
def write_tga(im, path):
|
||||
@@ -289,7 +297,7 @@ def main():
|
||||
info = None
|
||||
print('%s: static, %s in a %dx%d texture' % (args.name, shape, tw, th))
|
||||
else:
|
||||
fps, indices = resample(durations, MAX_COLS * (MAX_TEXTURE // FRAME))
|
||||
fps, indices = resample(durations, max_frames(cell))
|
||||
sheet, nframes, cols, tw, th = build_sheet(frames, indices, cell)
|
||||
info = (nframes, fps)
|
||||
print('%s: %d source frames (%.2fs) -> %d frames at %dfps (%.2fs, '
|
||||
|
||||
Reference in New Issue
Block a user