# MSP Local v0.9

Lokalny klient MSP z 2016 roku z backendem PHP i dołączonym Ruffle. Projekt jest
zainstalowany w `C:\xampp\htdocs`, a Apache obsługuje `http://localhost:1600/`.
Dane gry trzyma baza MySQL/MariaDB `msp` (opis niżej, w części „Baza danych
MySQL”); bez niej serwer umie pracować na plikach JSON. Osobna instalacja Flash
nie jest potrzebna.

## Uruchomienie

1. Uruchom Apache w XAMPP.
2. Otwórz **http://localhost:1600/index-local.html**.
3. Zaloguj się: **DemoMSP**, hasło **Demo123!**.

Przycisk „Graj teraz” otwiera kreator. Wybierz postać, zmień jej wygląd i przejdź
do formularza konta. W nowym haśle użyj liter i cyfr, np. `MojeHaslo123` — stary
formularz rejestracji nie dopuszcza znaków specjalnych. Logowanie obsługuje
podane wyżej hasło demonstracyjne ze znakiem wykrzyknika.

Konta oraz wybrane elementy są zapisywane w bazie `msp` (albo w
`local_server/data/users.json`, gdy `local_server/config.php` wskazuje pliki).
Hasła są haszowane. Skrypt `C:\xampp\php\php.exe tools\seed_demo.php` tworzy
konto demonstracyjne tylko wtedy, gdy go jeszcze nie ma; zachowuje istniejące konta.

## Baza danych MySQL

Serwer zapisuje dane gry w bazie wskazanej w `local_server/config.php`
(wzór z opisem: `local_server/config.example.php`). Lokalnie to baza `msp`
w MariaDB z XAMPP, użytkownik `root` bez hasła. Katalog `local_server` jest
zablokowany dla przeglądarki, więc dane logowania do bazy nie wyciekną przez WWW.

W bazie są trzy tabele:

- `msp_players` — konta: jedno konto w wierszu, każde pole w osobnej kolumnie,
  tak jak w dawnym `users.json`. Każda kolumna ma w phpMyAdmin opis po polsku.
  Listy (szafa, znajomi, pokój, zwierzaki, Magazyn Fame…) są w kolumnach JSON,
  a pola, których tabela nie zna, w kolumnie `extra`. Pixi Star to wiersz
  z kluczem `#anchor-3`;
- `msp_rows` — reszta danych w wierszach: bilety sesji, wiadomości, tematy
  i posty forum, prezenty, filmy, looki, ubrania i animacje ze sklepu, newsy
  i kody podarunkowe — każdy to osobny wiersz z treścią w JSON (kolumna
  `data`). Kolumna `doc` mówi, z czego wiersz pochodzi: `state` (stan gry),
  `catalog` (sklep: ubrania i animacje), `news`, `redeem-codes`, `anchors`
  (Pixi Star), `frontpage`, `backgrounds`, `cloth-kinds` (pamięć rodzajów ubrań);
- `msp_locks` — wiersz, na którym każdy zapis bierze blokadę, żeby dwa
  żądania nie nadpisały sobie danych, i znacznik wersji tabeli kont.

W `msp_players` można zmieniać wartości wprost w phpMyAdmin (Przeglądaj →
Edytuj). Gra czyta tabelę przy każdym żądaniu, więc zmiana działa od razu:

- `moderator` — 0 gracz, 1 administrator, 2 moderator, 3 ukryty administrator;
- `judge`, `celeb` — plakietki, 1 tak, 0 nie;
- `money`, `diamonds` — StarCoins i diamenty; `fame` — Fame. Poziom (`level`)
  liczy się sam z Fame przy każdym odczycie, więc żeby zmienić poziom, zmień
  Fame (progi są w `local_server/lib/AccountState.php`);
- `vipTier` (0 VIP, 1 Super VIP, 2 Elite VIP, 3 Star VIP) i `vipUntil` — VIP
  działa do tej daty, np. `2030-01-01T23:59:59+00:00`; `NULL` to brak VIP-a;
- `lockedUntil` z `lockedText` blokują konto, `mutedUntil` wycisza czat.

Gra zapisuje w wierszu tylko kolumny, które sama zmieniła, więc nie cofa
wartości wpisanych w phpMyAdmin w innych polach. Wartość spoza zakresu
(np. ujemne monety) poprawia się sama przy odczycie. Kolumn JSON i
`passwordHash` lepiej nie ruszać ręcznie.

Dalsze części README podają nazwy plików JSON. Przy bazie chodzi o te same
dane: konta z `users.json` są w `msp_players`, a reszta w wierszach `msp_rows`
o danym `doc`: `users.json` → `state`,
`custom-items.json` → `catalog`, `news.json` → `news`, `redeem-codes.json` →
`redeem-codes`, `anchors.json` → `anchors`, `frontpage.json` → `frontpage`,
`backgrounds.json` → `backgrounds`, `cloth-kinds.json` → `cloth-kinds`.

Zapis idzie w transakcji: przerwane żądanie niczego nie zostawia w połowie,
a baza zmienia tylko te wiersze, które się zmieniły. Rozbicie na wiersze jest
potrzebne, bo MySQL ogranicza rozmiar jednego zapytania (w XAMPP 1 MB), a sam
katalog sklepu ma ponad pół megabajta. Na pustej bazie tabele zakładają się
same przy pierwszym żądaniu.

Plikami zostają: SWF-y, obrazy, bloby looków (`local_server/data/blobs`),
miniaturki (`local_server/data/snapshots`), obrazy newsów i dzienniki.
`gateway.log` i `gateway-error.log` po przekroczeniu 5 MB przechodzą do `.1`.

Przeniesienie danych z plików JSON robi:

```
C:\xampp\php\php.exe tools\migrate-to-mysql.php
```

Bez parametrów przenosi katalog sklepu (323 ubrania, 965 animacji), newsy,
kody podarunkowe, ustawienia Pixi i strony głównej, tła i pamięć rodzajów
ubrań. Konta graczy zostają w plikach — w bazie gra startuje od zera, ale
z Pixi Star (wygląd, pokój, zwierzak) i z licznikami numerów, żeby nowe konta,
looki i filmy nie dostały numerów starych i nie przejęły ich miniaturek
z dysku. `--with-accounts` przenosi też konta, `--force` nadpisuje dane, które
już są w bazie. Pliki JSON zostają nietknięte jako kopia.

Bilety sesji nie wygasały, więc każde logowanie dokładało wpis na zawsze.
Teraz konto trzyma 10 najnowszych biletów — starsze sesje tego konta się
wylogowują.

Na hosting: załóż bazę w panelu hostingu, w phpMyAdmin w XAMPP wyeksportuj
bazę `msp` (Eksport → SQL) i zaimportuj ją w phpMyAdmin hostingu, a potem
wpisz dane bazy w `local_server/config.php`. Pliki `local_server/data/*.json`
nie są wtedy potrzebne. Z dostępem SSH można zamiast eksportu wgrać te pliki
i uruchomić `php tools/migrate-to-mysql.php` na serwerze.

`'storage' => 'files'` w `config.php` wraca do plików JSON. Na plikach
pracują też testy (`tools/test-*.php`), a `tools/test-mysql-storage.php`
sprawdza bazę na osobnych tabelach `msp_test_*`, które na końcu usuwa —
także to, że zmiany z phpMyAdmin docierają do gry i nie cofa ich zapis gry.

Gdy konto dostanie nowe pole, dopisz je do `PLAYER_COLUMNS`
w `local_server/lib/Storage.php` i podbij tam `SCHEMA_VERSION` — kolumna
dopisze się do tabeli sama przy następnym żądaniu. Do tego czasu nowe pole
leży w kolumnie `extra`, więc nic nie ginie.

## Logowanie przez Discorda

Przed grą stoi bramka: strona pokazuje przycisk **Zaloguj przez Discorda**,
a dalej przechodzi tylko konto, którego ID jest na liście zaufanych
(panel admina → **ID Discord**). Obce konto widzi ekran „Brak dostępu” ze swoim
ID do przekazania administracji i nie wejdzie do gry.

Bramka to nie jest sam ekran. Każde żądanie gry — `Gateway.php` (AMF),
`message-api.php`, miniaturki i bloby looków — sprawdza po stronie serwera
podpisane ciasteczko `bettermsp_gate` (HMAC-SHA256, `HttpOnly`, `SameSite=Lax`,
domyślnie 30 dni). Bez niego dostają 403, więc samo otwarcie `main_local.swf`
z palca niczego nie daje. Ciasteczko niesie tylko ID Discorda i termin ważności;
przy każdym żądaniu sprawdzana jest też lista zaufanych, dlatego usunięcie ID
odcina gracza natychmiast, nawet w trakcie gry.

Co ustawić raz, na starcie:

1. Na `discord.com/developers/applications` utwórz aplikację (**New Application**).
2. W zakładce **OAuth2 → Redirects** dodaj dokładnie ten adres:
   `http://localhost:1600/discord-login.php` (na hostingu ten sam adres z Twoją
   domeną). Panel pokazuje gotowy adres do skopiowania.
3. Skopiuj stamtąd **Client ID** i **Client Secret** i wklej je w
   `local_server/config.php`, w sekcji `discord`.
4. Wejdź do `admin.php` (panel ma własne logowanie i nie przechodzi przez
   bramkę), zakładka **ID Discord**, i dodaj swoje ID — inaczej nikt nie wejdzie,
   łącznie z Tobą. ID można wpisać ręcznie albo kliknąć **Wpuść** przy koncie,
   które właśnie próbowało wejść.

**Aplikacja na komputer (Electron).** Jej stary silnik nie radzi sobie ze
stroną Discorda, więc aplikacja otwiera ją w zwykłej przeglądarce i sama
odbiera powrót na `http://localhost:53215/discord-callback`. Potem wczytuje
`discord-login.php?code=…` w oknie gry, bo tam leży ciasteczko stanu. Serwer
wybiera ten adres powrotu tylko wtedy, gdy User-Agent zawiera `BetterMSPApp/`.
Aplikacja dopisuje to w `main.js`. Dlatego w **OAuth2 → Redirects** muszą być oba
adresy: serwerowy i ten z `localhost:53215`. Inny adres aplikacji ustawisz
w `config.php` polem `app_redirect_uri`, a pusty tekst wyłącza tę drogę.
W aplikacji `GAME_DISCORD_CALLBACK_URL` musi wskazywać `discord-login.php` na tym
samym serwerze co `GAME_URL`. Jeśli Discord wraca prosto na serwer, powrót ląduje
w przeglądarce bez ciasteczka i kończy się błędem sesji.

`'enabled' => false` w sekcji `discord` wyłącza bramkę i gra jest znowu otwarta
dla wszystkich. Discord podaje nam tylko ID, nazwę i awatar (zakres `identify`);
żaden token nie jest zapisywany. Przycisk **Wyloguj wszystkich** w panelu losuje
nowy sekret podpisu, czyli unieważnia wszystkie wydane ciasteczka naraz —
przydaje się, gdy ktoś mógł przejąć czyjeś ciasteczko. Zaufane ID, ostatnie
odrzucone próby i ten sekret leżą w dokumencie `discord` (tabela `msp_rows`
albo `local_server/data/discord.json`).

## Panel admina (admin.php)

**http://localhost:1600/admin.php** (link w prawym dolnym rogu gry). Logujesz się
nickiem i hasłem postaci z MSP, która ma w grze rolę moderatora albo
administratora — rola jest sprawdzana przy każdym żądaniu, więc odebranie jej
od razu zamyka panel. Pięć nieudanych prób z jednego adresu włącza przerwę,
która za każdym razem się podwaja.

W menu (hamburger) są zakładki:

- **Pulpit** — liczby z gry: gracze, obecni, nowi, VIP-y, waluty, Fame, sklep,
  kody, ostatnie działania ekipy i najnowsze konta;
- **Użytkownicy** — kafelki graczy po 10 na stronę, wyszukiwarka w pasku górnym,
  a po kliknięciu okno z postacią i czterema zakładkami: **Info** (wszystkie dane
  konta), **Funkcje moderacyjne** (blokada na stałe i czasowa, ostrzeżenie,
  wyciszenie, wyzerowanie statusu, reset hasła z gotowym hasłem do przekazania,
  zmiana nicku, wymuszenie nowego nicku, usunięcie i przywrócenie konta),
  **Edycja** (StarCoiny, diamenty, Fame, kręcenia kołem, VIP, plakietki
  Judge i Celeb, rola — rolę zmienia tylko administrator i nie sobie) oraz
  **Adresy IP** (opis niżej);
- **Zgłoszenia** — wszystko, co gracze zgłosili w grze przyciskiem „Zgłoś”
  (opis niżej). Liczba czekających świeci przy zakładce, a na przycisku menu
  pojawia się czerwona kropka;
- **Zdjęcia** — zdjęcia wgrane przez graczy w Movie Town. Nowe czekają tu na
  decyzję ekipy i dopiero po zatwierdzeniu widzą je inni (opis w „Zdjęcia”);
- **Forum** — wszystkie tematy z gry (filtr działu, wyszukiwarka po tytule,
  nicku i numerze). Kliknięcie tematu pokazuje posty i wyniki ankiety;
  usunąć można pojedynczy post albo cały temat (opis w „Forum”);
- **ID Discord** — lista kont, które wejdą do gry (opis wyżej);
- **Ogłoszenie** — jedna wiadomość od ekipy, którą gra pokazuje w okienku
  wszystkim, którzy są w środku (opis w „Ogłoszenia ekipy”);
- **Wiadomości** — newsy ze strony startowej; dodanie to samo zdjęcie,
  z opcją ukrycia poprzednich;
- **Sklep** — **Ciuchy** i **Animacje**: wgrywasz plik SWF, a potem ustawiasz
  nazwę, kategorię, płeć, cenę w StarCoinach albo diamentach, dostęp tylko dla
  VIP-ów i widoczność. Do czasu zapisu pozycja jest ukrytym szkicem. Przycisk
  **Masowe dodawanie** przyjmuje wiele plików naraz (opis niżej). **Rzeczy**:
  przedmioty do trzymania i do pokoju — kategorię panel rozpoznaje sam
  (opis w „Sklep z rzeczami”). **Tła**: wgrywasz obraz (opis w „Sklep z tłami”);
- **Kody** — jednorazowe kody na VIP-a, waluty i ubrania, **Zestawy**, czyli
  gotowe nagrody (np. „Rok Star VIP + 5000 SC” albo same buty i sukienka), oraz
  **Pakiet Diamond** — strój z plików SWF + 185 000 SC i 2 100 diamentów, osobno
  kobiecy i męski, widoczny w grze w oknie pakietów (opis w „Kody podarunkowe”);
- **Strona główna** — kto stoi na ekranie startowym i jakie animacje odgrywa;
- **Pixi Star** — jej statusy, odstęp między nimi i kopiowanie wyglądu z konta gracza.

Dawny `accounts-local.php` przekierowuje do panelu (jego kod leży w
`local_server/legacy/`, poza zasięgiem przeglądarki). `health.php`
i `prepare-client.php` otwierają się teraz po zalogowaniu w panelu;
przebudowę klienta można też uruchomić bez logowania z wiersza poleceń:
`C:\xampp\php\php.exe prepare-client.php`.

### Nick, usuwanie kont i adresy IP

**Zmień nick** nadaje nowy nick od razu (3–30 znaków, bez `#`; wielkość liter
też można poprawić). Konto dostaje w danych nowy klucz, a wszystko inne jest
związane z numerem postaci, więc znajomi, rzeczy i historia zostają. Gracz
zobaczy nowy nick po ponownym zalogowaniu. Nick członka ekipy zmienia tylko
administrator.

**Wymuś zmianę nicku** wylogowuje gracza, a przy następnym logowaniu serwer
odpowiada statusem `ForceEntityChange` — klient zamiast gry otwiera wtedy
oryginalne okno „Zmień swoją nazwę” (`ForceNameChange`). Okno wysyłało nowy nick
starą usługą SOAP, więc łatka `tools/client-patches/CommonService.as` przesyła
`NewForcedEntityName` przez AMF. Serwer sprawdza nick jak przy rejestracji
(długość, `#`, zablokowane nazwy, zajętość), zmienia go, zdejmuje wymuszenie
i zapisuje w historii konta „Nowy nick wybrany przez gracza”; klient pokazuje
nowy nick i wylogowuje, a gracz wchodzi już pod nową nazwą. Wymuszenie można
cofnąć w panelu.

**Usuń konto** (tylko administrator, nie dla kont z rolą) działa jak w MSP: na
konto nie da się zalogować (odpowiedź jak przy złym haśle), nie da się go
zaprosić do znajomych ani przyjąć jego zaproszenia, a czekające zaproszenia
i prośby o relację znikają. Filmy, looki, ArtBooki, zdjęcia, ubrania
i znajomości zostają. Z opcją **zwolnij nick** konto nazywa się „Deleted User”
(w danych leży pod kluczem `#deleted-<numer>`), znika z wyszukiwarki w grze,
a dawny nick może zająć ktoś inny. Bez tej opcji nick zostaje przy koncie
i konto da się wyszukać. „Deleted User” jest zarezerwowany — nikt nie założy
konta o tej nazwie. **Przywróć konto** cofa usunięcie; przy zwolnionym nicku
podajesz, pod jakim ma wrócić. Usunięte konta są na liście graczy w panelu
z plakietką „USUNIĘTE” (szuka się ich także po dawnym nicku), ale nie liczą
się w statystykach.

W grze serwer oznacza usunięte konto polem `IsExtra = 1`, a klient traktuje je
wtedy jak usunięte (`Actor.isDeleted`). Mini profil nie pokazuje przycisków
znajomych, najlepszego przyjaciela ani relacji. Akcje z sekcji „Do zrobienia”
są wyłączone, a przycisk zgłoszenia znika. Nagłówek „Jesteście przyjaciółmi”
chowa łatka `tools/client-patches/CharacterPopUpDeletedActor.pcode`. W obsadzie
filmu kliknięcie usuniętego aktora nie otwiera jego profilu.

**Adresy IP** zapisują się przy rejestracji i każdym logowaniu: adres
z rejestracji, adres ostatniego logowania i lista do 30 różnych adresów
z pierwszym i ostatnim użyciem oraz liczbą logowań (`registrationIp`,
`lastLoginIp`, `loginIps`; w MySQL w kolumnie `extra`). Zakładka **Adresy IP**
pokazuje je razem z innymi kontami, które logowały się z tych samych adresów.
Adres to `REMOTE_ADDR` — nagłówkowi `X-Forwarded-For` nie ufamy, bo klient
wpisze tam, co chce. Konta sprzed tej zmiany mają listę pustą do pierwszego
logowania.

### Zgłoszenia i blokowanie graczy

Okno „Zgłoś” w grze (gracz, film, look, ArtBook, wpis w księdze gości…) i okno
„Zgłoś lub Zablokuj” wysyłały dane starą usługą SOAP, której lokalny serwer
nie ma: zgłoszenie wisiało bez odpowiedzi, a „Zablokuj” nic nie robiło. Łatka
`ActorService.as` przesyła `NewReport`, `BlockActor`, `UnblockActor`
i `BlockedActors` przez AMF (`MovieStarPlanet.WebService.Local`).

**Zgłoszenie** zapisuje się od razu z nazwami obu graczy, powodem, miejscem,
opisem, tytułem lub treścią zgłoszonej rzeczy i ostatnimi (do 12, z tygodnia)
prywatnymi wiadomościami między zgłaszającym a zgłoszonym — dowody zostają,
nawet gdy ktoś potem coś usunie. Ta sama rzecz zgłoszona ponownie, zanim ktoś
ją rozpatrzy, uzupełnia czekające zgłoszenie („zgłoszone 2 razy”). Jeden gracz
może wysłać do 20 zgłoszeń na godzinę; historia trzyma 2000 rozpatrzonych.

W zakładce **Zgłoszenia** panelu są filtry Czekające / Rozpatrzone / Wszystkie,
wyszukiwarka (nick, ID, treść), kliknięcie gracza otwiera jego profil,
a „Kary i profil” — od razu zakładkę Funkcje moderacyjne. „Rozpatrzone” pyta
o nieobowiązkową notatkę, która zostaje przy zgłoszeniu i w historii gracza;
„Przywróć” cofa zgłoszenie do czekających, a administrator może je usunąć.
Liczba czekających jest też na Pulpicie.

**Blokada** zapisuje się na koncie blokującego (`blockedActors`) i zrywa
w obie strony znajomość, zaproszenia, serduszko i najlepszą przyjaźń.
Zablokowany nie wyśle blokującemu zaproszenia ani prośby o relację, nie
napisze w jego księdze gości, a powiadomienia na żywo między nimi nie
dochodzą (poza samą wiadomością o blokadzie). Wiadomości czatu od
zablokowanych ukrywa klient — dostaje teraz prawdziwe listy
`LoadBlockedAndBlockingActors` (także w pakiecie po zalogowaniu). Blokadę
zdejmuje „Usuń Blokadę” w tym samym okienku albo lista „Zablokowani”
w ustawieniach.

### Wartości konta

Poziom wynika z Fame; wpisanie innego poziomu dostosowuje Fame do jego progu.
Poziomów jest 200 (`MspAccountState::MAX_LEVEL`), progi są w
`MspAccountState::LEVEL_FAME`: poziom 1 od 3 Fame, 2 od 39, 10 od 48 283,
20 od 1 340 450, 50 od 122 326 967, 101 od 2 738 879 270, 150 od
9 313 762 097, 200 od 18 000 000 000. Do 60 poziomu to progi z MSP tańsze
o ok. 10%; wyżej przyrost na poziom rośnie coraz wolniej (ok. 97 mln Fame przy
101, ok. 182 mln przy 200), więc dawne poziomy 70–101 są tańsze mocniej
(90.: −28%, 101.: −37%) — inaczej 200. poziom kosztowałby dziesiątki
miliardów. Nazwy poziomów 102–200 (`FAMESYS_V2_LEVEL_102`…`200`, we
wszystkich 11 językach — brak klucza dawał w grze pusty napis albo błąd) to
serie po dziesięć z numerami: Wieczna Movie Star II–X, Kosmiczna,
Galaktyczna, Hollywoodzka, Nieśmiertelna, Mityczna, Niebiańska, Boska,
Supernowa, Absolutna Movie Star, a 200. to „Legenda MovieStarPlanet”.
Poniżej 3 Fame jest poziom 0 — klient ma na niego własny tytuł
(`LEVEL0` = „Nieznany”), więc świeże konto zaczyna jako Nieznany, tak jak w MSP.
Po obniżeniu progów konto od razu dostaje brakujące awanse: jedno okno
gratulacji z 3000 SC za każdy nowy poziom (np. dawny 101. poziom z
4 335 320 220 Fame to teraz 115. i 42 000 SC).
Poziom każdego konta przelicza się z Fame przy każdym odczycie, więc zmiana
tabeli od razu obniżyła poziomy kont, które miały je ze starej, taniej krzywej.
Klient pobiera progi raz, przy starcie — po zmianie trzeba odświeżyć grę.

Fame ma własny sufit, 99 999 999 999 (`MAX_FAME`), bo próg poziomu 200
(18 mld) nie mieści się w int; ponad próg 200 Fame dalej rośnie do rankingów. StarCoiny i diamenty zostają przy 2 000 000 000, bo klient
trzyma je w polach int. Z Fame jest podobnie tylko w `DBML.Actor` (postać innego
gracza) — tam wysyłamy najwyżej 2 147 483 647, żeby liczba nie zawinęła się na
ujemną. Fame zalogowanego gracza (`ActorDetails.Fame`) i suma kontrolna
logowania działają na pełnej wartości.
Na poziomie 200 pasek na górze jest pełny, a dymek mówi, że to maksymalny
poziom (`LevelBarMaxLevel.pcode`). Okno „Zdobywaj Fame” (klik w pasek) też
zna 200 poziomów: 200. pokazuje jako ostatni, a pasek numerów przesuwa się aż
do 175–200 (łatka EarnFameView w `tools/build-client.ps1`).
VIP wygasa z końcem wybranego dnia UTC; pusta data usuwa VIP-a. Role konta:
0 = gracz, 2 = moderator, 1 = administrator, 3 = ukryty administrator.
Zmiana tych danych zachowuje hasło i wygląd. Stare konta automatycznie otrzymują
brakujące pola, bez kasowania danych.

## Forum

Ikona „Forum” na górnej belce otwiera oryginalny moduł forum. Sześć działów
odpowiada kartom klienta: Przyjaciele i zabawa, Filmy, Gry, Pomoc, VIP oraz
MovieStarPlanet. Zakładki u góry filtrują tematy: najnowsze, moje, znajomych
i ankiety.

Ikoną z ołówkiem zakładasz temat albo ankietę, a pole na dole tematu służy do
pisania postów — wysyła je Enter. Można wybrać kolor tekstu i wstawić emotikonę.
Swój post kasujesz krzyżykiem przy dymku. Ankieta przyjmuje jeden głos na konto
i pokazuje wyniki po zagłosowaniu.

Tematy, posty i głosy trafiają do `local_server/data/users.json` razem z resztą
kont. Limit to pięć tematów na dobę; zablokowane i wyciszone konta nie piszą.
Moderator dodatkowo przypina i kasuje cudze tematy oraz posty.

W panelu zakładka **Forum** pokazuje wszystkie tematy tak jak gra: przypięte
na górze, dalej od ostatniego posta, z autorem, działem, liczbą postów
i początkiem pierwszego wpisu. Okno tematu ma posty od najstarszego (po 30 na
stronę) i wyniki ankiety. **Usuń post** chowa jeden post, **Usuń cały temat**
— temat ze wszystkimi postami i głosami. Oba działania trafiają do historii
autora i na Pulpit.

Klient wpuszcza na forum tylko konta z potwierdzonym adresem e-mail i od
3 poziomu (VIP bez ograniczenia poziomu). Lokalne konta nie mają poczty do
potwierdzania, więc logowanie zgłasza adres jako potwierdzony — to odblokowuje
też komentarze i oceny filmów.

## Filmy

„Miasto Filmów” → „Filmy” otwiera przeglądarkę i edytor. Sam edytor siedzi
w `moviemodule.swf`, który leżał w projekcie, ale nie był wpisany w
`GetAppSettings` — bez klucza `MovieModule` klient nie miał skąd go wziąć,
tak samo jak wcześniej było z forum.

Treść filmu — sceny, klatki, rekwizyty i ubrania aktorów — serwer trzyma
jako zakodowaną wartość AMF, dokładnie taką, jaką przysłał edytor, i oddaje
ją bez zmian. Dzięki temu nie modelujemy każdego z tych typów, a nic po
drodze nie ginie. W JSON-ie osobno leżą tylko pola, po których szukamy
i sortujemy: tytuł, autor, widoczność, obejrzenia i oceny. Filmy siedzą
w `users.json` pod kluczem `movies`.

Widoczność wybiera gracz w oknie zapisu i klient zna dokładnie cztery
wartości: `-1` usunięty, `0` prywatny, `100` publiczny, `300` zgłoszony do
konkursu. Listy w przeglądarce mają własne numery typów: `1` moje
nieopublikowane, `2` moje opublikowane, `3` filmy z udziałem znajomych,
`4` filmy znajomych. Trójkę traktujemy jak czwórkę — rozróżnienie wymagałoby
zaglądania w obsadę, a ta siedzi w treści, której nie rozbieramy.

Obejrzenie liczy się **raz na konto**, tak jak w MSP: kolejne seanse tego
samego widza nie podbijają licznika ani nie płacą drugi raz (klient dostaje
wtedy `returnType 2`). Za każde policzone wyświetlenie reżyser dostaje
**50 starcoinów** wprost na konto oraz sławę do odebrania w gazetce.
`StarCoinsEarned` na filmie to suma, ile ten film do tej pory przyniósł.
Ocena i komentarz idą tą samą metodą i liczą się raz na widza — kolejna
zastępuje poprzednią. Za pierwszą ocenę cudzego filmu widz dostaje 10 sławy
i 10 starcoinów (tyle samo, co w oryginale). Odpowiedź musi nieść
`rateMovieId`, `awardedFame` i `awardedStarCoins` — dopiero callback klienta
zamyka okno oceny, więc przy innym kształcie zostaje ono otwarte i martwe.
Lista recenzji (`PagedMovieReviewList`) trzyma wpisy w polu `items`, a nie
`list` jak lista filmów; pod złą nazwą panel recenzji wisi na „Wczytywanie”.

Ranking „Top” to dwie kolumny — znajomi i wszyscy — więc `GetHighscoreMovie`
dostaje `[actorId, forFriends, lastWeek, orderBy, pageIndex, pageSize]`
i stronicowanie siedzi na czwartej oraz piątej pozycji. Kafelek rankingu
czyta `WatchedActorCount` (nie `WatchedTotalCount`) i `StarCoinsEarned`,
a `Watched` i `Rated` decydują o oznaczeniu obejrzanego filmu.
Sortowanie: `TOTALWATCHED`, `ACTORSWATCHED`, `RATING`.

### Tła, muzyka i animacje w studiu

Trzy rzeczy, bez których film wyglądał na zepsuty, choć zapis działał:

- **Tła.** Oryginał trzymał je jako gotowe pliki w `backgrounds/`, którego
  w tej kopii nie było — scena zostawała pusta. `tools/build-backgrounds.ps1`
  renderuje je z plansz lokacji (`graphics/*.swf`) i kadruje do proporcji
  sceny 960×480, a przy okazji zapisuje katalog do
  `local_server/data/backgrounds.json`, z którego serwer odpowiada na
  `GetMyBackgrounds`. Sześć teł (`rooftop.jpg`, `hallway_day.jpg`,
  `fairytale.jpg`, `front_roomV5.jpg`, `tropical_beach.jpg`, `Nightclub.jpg`)
  ma identyfikatory i nazwy plików zaszyte w kliencie — ich nie wolno zmieniać
  i serwer ich nie odsyła, bo klient dokłada je sam.
- **Muzyka.** To była przyczyna „film stoi na klatce 0:00”: przy odtwarzaniu
  `MovieStudio.markCurrentScene` robi `new Sound(...)` na pliku z `music/`,
  a potem `playScene` woła na nim `play()`. Gdy pliku nie ma, Flash rzuca
  IOError i `playScene` przerywa się **zanim** utworzy timer klatek — scena
  i postacie były już załadowane, więc wyglądało to na zamrożony film.
  `tools/build-movie-music.php` dorabia brakujące pliki jako poprawne, ciche
  MP3 (prawdziwych nagrań nie nadpisuje). Serwer wystawia całą zawartość
  `music/` przez `GetMyMusic`, więc w studiu widać pełną bibliotekę,
  pogrupowaną po katalogach i członie `FilmEdge_<Kategoria>_`.
- **Animacje.** Klient pokazuje 23 zaszyte u siebie animacje, a w zasobach
  było 10. Każda brakująca kończyła się 404, `PlayAnimation` dostawał `null`
  i postać zostawała zamrożona — najgorsza była `walk`, bo studio przypisuje
  ją automatycznie przy każdym przesunięciu aktora.
  `tools/build-animation-stand-ins.ps1` podstawia zamienniki z tego, co jest
  (siedzenie i leżenie wygląda jak stanie). Prawdziwe pliki wrzucone do
  `swf/animations/` mają pierwszeństwo — skrypt pomija istniejące.

### Sklep z tłami

Sklep z tłami (Tła wśród sklepów albo „Kup więcej” w wyborze tła w studiu
filmowym) był pusty, bo serwer nie znał jego metod. Teraz odpowiada na
`GetBackgroundsForShopNew`, `…Category`, `…Tag`, `GetAllBackgroundsForModerators`,
`GetBackgroundTags` i `BuyBackground`. W sklepie są tła dodane w panelu
(**Sklep → Tła**) — wbudowane tła z `backgrounds.json` każdy ma i tak.

Dodawanie: wybierasz obraz (JPG, PNG, GIF, WebP), przesuwasz kadr do
proporcji sceny, a przeglądarka rysuje trzy pliki: scenę 960×480
(`backgrounds/shop_<numer>_<losowe>.jpg`), kafelek sklepu 237×100
(`backgrounds/snapshots/<numer>.png` — sklep szuka go po numerze tła)
i miniaturkę 192×96 jak przy wbudowanych tłach. Serwer nie potrzebuje więc GD.
Tło trafia jako ukryty szkic; w edytorze ustawiasz nazwę, kategorię (dziesięć
zakładek sklepu: Natura, Sport, Dom, Święta, Pory roku, Miasto, Pozostałe,
Szkoła, Tła, Fantastyka), podkategorię z listy w Świętach, Porach roku
i Fantastyce, cenę w StarCoinach albo diamentach, VIP-a i widoczność. Numery
teł ze sklepu zaczynają się od 5000, a wpisy leżą w katalogu sklepu
(`custom-items.json` / dokument `catalog`, gałąź `backgrounds`).

Kupione tło zapisuje się na koncie (`backgroundsOwned`) i od razu jest
w wyborze tła w studiu filmowym, w grupie swojej kategorii. Szał zakupów
obniża cenę w StarCoinach o połowę, jak liczy klient; tło VIP kupi tylko VIP.
Obrazu nie da się podmienić, a zapisanego tła nie usuwa się — ukryte znika ze
sklepu, ale kto je kupił, dalej ma je w filmach. Przez 14 dni tło ma gwiazdkę
„Nowe”. `GetBackgroundTags` zwraca też po jednym zastępczym tagu dla zakładek
bez podkategorii — bez tego `DropDownMenu` dostawało `null` przy zmianie zakładki.

Miniaturki filmów klient pobiera z `movie-snapshots/<n>/<id>.jpg`, a pokoi
i ArtBooków z `entity-snapshots/<typ>/<n>/<id>.jpg`; oba adresy `.htaccess`
kieruje do `snapshot-local.php`, który czyta to, co klient sam wcześniej
wysłał przez `SaveSnapshot`.

Treść filmu wraca do klienta z kolekcjami zapisanymi jako zwykły obiekt
z polem `source`, a nie jako „externalizable” ArrayCollection — tak samo jak
wszystkie inne listy (`amf_array_collection`). Bez tego Ruffle nie potrafi
odczytać ani odpowiedzi, ani zawartości `MovieData`, i okno filmu zostaje
czarne. Natywny Flash czyta obie postacie tak samo.

Autozapis (`GetAutoSavedMovieId`) zwraca zero. Zwykły zapis i autozapis
przychodzą tą samą metodą, więc nie odróżnimy niedokończonego szkicu od
gotowego filmu prywatnego — oddawanie tu identyfikatora kazałoby edytorowi
wciąż otwierać stary film zamiast nowego.

Konkursy filmowe (`MovieCompetition`) nie są zrobione.

## Mój pokój i pokoje graczy

„Mój Pokój” na górnej belce otwiera pokój, a ikona domku w mini profilu
prowadzi do pokoju innej osoby. Widok jest ten sam co w chatroomach, ale
oryginalny klient wchodził tam przez `RoomRequester.joinRoom` — czyli RTMP,
którego lokalny serwer nie ma. Callback nigdy nie wracał i przycisk nic nie
robił. `main_local.swf` woła teraz ten callback wprost, tak jak zrobiliśmy
to wcześniej dla chatroomów. Postacie specjalne (Pixi Star) miały w tej samej
metodzie osobną gałąź przez `RoomRequester.requestRoom`, też po RTMP — ta
gałąź jest wyłączona i ich pokój otwiera się tak samo.

Zdjęcie pokoju powstaje dopiero przy zapisie. Pokój, którego nikt jeszcze nie
zapisał, dostaje z `snapshot-local.php` pusty pokój z domyślną tapetą
i podłogą (`img/room_default_196/392/980.jpg`) zamiast szarej ramki.

Serwer obsługuje `loadActorRoom`, `LoadRoomItems`, `SaveRoom`, `LikeAdd`
i `SaveNewAndOldPetsPositionsInMyRoom`. Ta ostatnia wygląda pobocznie, ale
bez niej nie dało się zapisać pokoju: klient woła ją **przed** `SaveRoom`
i dopiero w jej callbacku — czytając z odpowiedzi `fameEarned` — wysyła
właściwy zapis. Brak metody oznaczał zwykłe `true`, callback wywracał się
na nieistniejącym polu i zmiany nigdy nie docierały do serwera.

Panel „Artykuły” w edytorze bierze zawartość z `LoadActorItems` i **musi
dostać pozycje rzeczy**: po nich edytor poznaje, co stoi już w pokoju.
Klik na pozycji z pozycją zdejmuje rzecz z pokoju, a bez pozycji dokłada ją,
więc lista bez pozycji sprawiłaby, że każdy klik dokłada kolejną kopię.
Dlatego `LoadActorItems` i `LoadRoomItems` zwracają tę samą listę. Bez tej
metody panel był pusty i nie było czego postawić, choć sam zapis działał.
Strój przysłany przez klienta dostawał wcześniej zastępcze, ujemne numery
relacji — takich rzeczy nie dało się umieścić w pokoju, bo zapis rozpoznaje
je właśnie po tym numerze. Teraz każda posiadana rzecz dostaje prawdziwy.

Rzeczy w pokoju to rzeczy z szafy z ustawioną pozycją — serwer trzyma ją
przy każdej relacji ubrania, więc pokój nie ma własnej listy. `LoadRoomItems`
zwraca zwykłą tablicę `ActorClothesRel` (nie kolekcję), a `SaveRoom` przysyła
`ActorClothesRelId` oraz małe `x` i `y`. Rzecz spoza listy wraca na pozycję
zerową, czyli znika z pokoju i zostaje w szafie.

Tapeta i podłoga to lista kafli sklejona dwukropkiem — po jednym na sekcję,
np. `wall/big flower wall.png:garden_wall02.jpg:kitchen/...:party/...`.
Wcześniej walidator przyjmował tylko jedną nazwę i odrzucał całą listę,
przez co zapis tapety cicho wracał do wartości domyślnej.
Pokój siedzi przy koncie w `users.json` pod kluczem `room`: tapeta, podłoga,
lista ustawionych rzeczy z pozycją i skalą oraz kto polubił. Nazwy tapet
i podłóg zawierają podkatalog i rozszerzenie (`wall/big flower wall.png`),
bo klient dokleja do nich tylko `img/wallpapers/` — nazwa wychodząca poza
ten katalog jest odrzucana i wraca wartość domyślna.

Pokój ma cztery sekcje: Pokój Dzienny, Ogród, Kuchnia i Pokój Imprez.
Tapetę i podłogę z serwera dostaje tylko Pokój Dzienny — nazwy tł trzech
pozostałych są zaszyte w kliencie (`garden_wall02.jpg`,
`kitchen/KitchenSandMosaic.jpg`, `party/diamondsPurpleSunRise.jpg` i ich
podłogi). Tych plików nie było w projekcie, więc sekcje zostawały czarne.
Złożyłem je z kafli, które już leżą w `img/wallpapers`, pod nazwami, których
klient szuka — to zamienniki, nie oryginalne grafiki MSP, więc jeśli masz
prawdziwe, podmień pliki pod tymi samymi nazwami. Rozszerzenie `.jpg` przy
danych PNG nie przeszkadza: Flash rozpoznaje format po zawartości.

Wejście do cudzego pokoju podbija licznik odwiedzin właściciela, a „LOVE IT!”
liczy się raz na odwiedzającego; własnego pokoju polubić nie można. Licznik
polubień na dole pokoju pokazuje wartość z chwili wejścia, więc świeże
polubienie widać po ponownym wejściu.

Łatka wejścia (`PlayerRoomLocalEnter.pcode`) tworzy dwa domknięcia, a ich
numery metod różnią się między kompilacjami klienta. Plik jest więc szablonem:
`build-client.ps1` wstawia w `%AFTER_JOINED%` i `%ANCHOR_JOINED%` numery
odczytane z oryginalnej metody (`FindClosures.java`). Po przebudowie klienta
pod hosting stały numer trafiał na setter `chatRoomId` i `displayMyRoom`
i wejście do pokoju zawieszało grę. `ListClosureRefs.java` wypisuje wszystkie
odwołania `newfunction` — porównanie przed i po łatce pokazuje, czy któreś
domknięcie nie zmieniło numeru.

Pozycje zwierzaków w pokoju przychodzą jako `PetLocation` z polem
`actorClickItemRelId` (z małej litery). Serwer szukał `ActorClickItemRelId`,
więc postawiony zwierzak nigdy nie zostawał w pokoju — teraz zostaje.

### Zapis pokoju z prawdziwego klienta

Natywny Flash wysyła listę rzeczy do `SaveRoom` jako „externalizable”
`ArrayCollection`, a dekoder oddaje ją jako `AmfExternalizable` z tablicą w polu
`value`. `collectionItems` czytał tylko tablicę i obiekt z polem `source` (tak
wysyłał Ruffle), więc zapis widział pustą listę: **żadna rzecz postawiona w pokoju nie
zapisywała się**, a przy okazji każdy zapis zdejmował z pokoju to, co już w nim stało.
Teraz `collectionItems` rozpakowuje `AmfExternalizable` (`tools/test-rooms.php` ma
przypadek w postaci, w jakiej wysyła ją klient). `SaveRoom` zwraca teraz 0 — klient
czyta wynik jako zdobyte Fame i dopisywał je sobie do licznika (wcześniej serwer oddawał
liczbę postawionych rzeczy, więc po zapisie pojawiała się nagroda, której nie było).

### Tapety i podłogi do wyboru

Edytor pyta o listy usługę SOAP `WebService/Service.asmx` metodą `GetWallpapers(type,
roomtype)` (`type` 1 to ściany, 2 podłogi; `roomtype` 0 Pokój Dzienny, 1 Ogród,
2 Kuchnia, 3 Pokój Imprez). Bez odpowiedzi listy były puste, więc nie dało się niczego
wybrać. Odpowiada `soap-local.php` (reguła w `.htaccess`; adres w kliencie ma podwójny
ukośnik, `//WebService/Service.asmx`, reguła przyjmuje oba warianty), a listy składa
`local_server/lib/Wallpapers.php` z plików w `img/wallpapers`: Pokój Dzienny to `wall/*`
i `floor/*`, Ogród to dwa tła z klienta (`garden_wall02.jpg`, `garden_carpet00.jpg`)
i zestaw zimowy z `garden/`, Kuchnia i Pokój Imprez to `kitchen/*` i `party/*` (nazwa z
„floor” to podłoga). Nowa tapeta albo podłoga pojawia się w edytorze po wrzuceniu pliku do
właściwego katalogu — pliki o nazwach, których zapis pokoju by nie przyjął, są pomijane.
Kształt odpowiedzi wynika z opisu usługi wbudowanego w `main.swf`
(`Service__WSDL_CLASS`, do obejrzenia przez `ffdec-cli -export binaryData`). Test:
`tools/test-wallpapers.php`.

### Wejście do pokoju po zapisie (łatki klienta)

Po zapisaniu rzeczy w edytorze pokój pokazywał stary stan (do ponownego zalogowania),
pierwsze kliknięcie X nie zamykało go, a kolejne wejście bywało zablokowane. Przyczyny
leżały w oryginalnym kodzie, który lokalnie, bez RTMP, nie robi tego, co robił na serwerze
pokojów; `build-client.ps1` ma trzy łatki w etapie zaraz po `PlayerRoomLocalEnter`:

- `ChatRoom.Enter`: przy pustym `roomId` nie robił `Exit()` poprzedniego wejścia, więc po
  edytorze stary `PublicProfile` zostawał w widoku nad nowym. Teraz, gdy poprzedni
  `PublicProfile` wciąż wisi w widoku, `Enter()` najpierw woła `Exit()`. Do tego
  `Enter()` zwalnia zawieszoną flagę `isExiting`, gdy nie ma SharedObjectu pokoju (bez
  RTMP nikt jej nie zdejmie po `Exit()`, a `Enter()` czekałoby na koniec wyjścia bez końca,
  więc pokój nie chciałby się już otworzyć);
- `EditMyRoom.btnExitClicked`: edytor jest singletonem, który przy wejściu subskrybuje
  `CHATROOM_CLOSED`, a wypisuje się dopiero w `exit()`, którego zapis nie woła. Zamykany
  potem pokój wołał `exit()` edytora, ten uruchamiał minutnik otwierający pokój i pokój
  wracał po X (trzeba było zamykać go dwa razy). Teraz `btnExitClicked` wypisuje się
  z tego zdarzenia na początku;
- `showPublicProfile` (domknięcie `afterJoined`): każde wejście zawijało ten sam
  `ChatRoom` w nowy `WindowStackableContainer`, a przy dwóch wejściach naraz drugi zabierał
  pokój pierwszemu i sam odpadał na zablokowanym stosie okien. Jeśli `ChatRoom` już stoi
  w stosie, ponowne pokazywanie jest pomijane i woła się tylko `Enter()`.

## Boonies i PetPets

Są dwie rodziny zwierzaków. Klasyczne to ClickItemy z lat 2011–2013: jajko (etap 0)
i trzy albo cztery etapy wzrostu, grafiki w `swf/clickitems/<plik>_<etap>.swf`;
katalog, ceny i zasady są w `local_server/lib/Pets.php` (`MspPetCatalog`).
Nowsze to **Boonies z DragonBones** (animowane, z lat 2014–2017) z
`swf/bonsters/boonies`; obsługuje je `local_server/lib/Bonsters.php` (opis niżej).
Prawdziwych Bonsterów (psy, koty, lwy z `swf/bonsters/fourleggedcreatures`) nadal
nie da się pokazać — w archiwum brakuje ich `skeleton.json` i `texture.json`. Przełącznik
sklepu Bonsterów w kliencie jest wyłączony (`BonsterShopSwitchOff.pcode`), a Świat →
Zwierzaki → Boonies i Bonsters otwiera od teraz nowy sklep (opis niżej).

### Nowy sklep zwierząt

Świat → Zwierzaki → „Boonies” i „Bonsters” otwierają moduł `PetShop` z grafiką
`swf/shops/pet/PetShopScreen.swf` (pokój z łapkami, półki, zakładki Nowe / Top /
Bonsters / Boonies, po prawej skrzynia z sylwetką i przyciskiem „Kup”). Sklep sprzedaje
obie rodziny: „Boonies” to najpierw 60 Boonies z DragonBones, a po nich klasyczne Boonies;
„Bonsters” to klasyczne PetPety (jajka z `swf/clickitems`). Wszystkie pozycje stoją
w sklepie cały czas (rotacja trzech jajek niżej dotyczy starego sklepu); „Nowe” to
najwyższe numery, „Top” układa po liczbie zwierzaków w grze, zakładka „Znajomi” jest
schowana.

Jak to działa:

- serwer obsługuje `AMFBonsterShopService` (`bonsterShopService` w `Bonsters.php`):
  każdy zwierzak to `MovieStarPlanet.DBML.Bonster`. Boonie z DragonBones mają
  `BonsterTemplateId` 0 i w `ArmaturePath` nazwę szkieletu bez numeru etapu; klasyczne
  mają −1 i nazwę pliku z `clickitems` (kafelek i podgląd rysują wtedy jajko, w `Scale`
  jest skala ze starego sklepu);
- zakup (`BuyBonster`) zwraca kod (0 udało się, −1 błąd albo limit 50 zwierzaków obu
  rodzin razem, −3 brak StarCoins) i od razu stawia zwierzaka w pokoju kupującego na wolnym
  miejscu; po „OK” w komunikacie „Znajdź to w swoim Pokoju!” gra otwiera edytor pokoju;
- klient: trzy klasy modułu są przekompilowane z `tools/client-patches/petshop`
  (`BonsterRenderer` i `PreviewView` rysują Boonie z DragonBones jak w oryginale, a
  klasycznego zwierzaka jako jajko; `BrowserView` daje „Nowym” zwykły pierwszy kafel),
  moduł to `shoppingmoduleexternal_local_v9.swf`; w `main.swf` `PetElement.as` i łatka
  `SelectPetBase.buyClicked` prowadzą oba wejścia wprost do nowego sklepu, a przełącznik
  `BonsterShopSwitch` zostaje wyłączony (steruje też paskiem poziomu, oknem wylogowania
  i wyborem zwierzaka w statusie).

**Pułapka kompilacji:** klasa kompilowana z `.as`, której nadklasa leży w innym SWF-ie
(tu `BonsterRenderer` po `AbstractListItemRenderer` z `main.swf`), gasi klienta — FFDec
nie zna łańcucha klas bazowych, więc inicjalizator skryptu nie dokłada ich zakresów
przed `newclass`, a metody dostają głębokość zakresu 3 zamiast 10/11. Stage w
`build-client.ps1` przywraca inicjalizator skryptu z oryginału (`DumpScriptInit.java`)
i głębokość zakresu (`FixScopeDepth.java`; `DumpClassInfo.java` pokazuje różnicę).
Klasy z nadklasą w tym samym module albo wbudowaną (`PreviewView`, `BrowserView`) tego
nie potrzebują.

### Boonies z DragonBones

60 Boonies leży w `swf/bonsters/boonies/<nazwa><etap>/` (`skeleton.json`,
`texture.json`, `texture.dbswf` — atlas jako SWF). Mamy tylko etap 1 (nazwa folderu
kończy się na `1`), więc kupiony zwierzak od razu jest wyklutym etapem 1 i nie ewoluuje;
zamiast tego **rośnie z poziomem** (wielkość w pokoju od 0,65 na poziomie 1 do 1 na
32., `ScaleWeb`). Lista, nazwy (do 20 znaków, tyle mieści tytuł okna zwierzaka) i ceny
(500 StarCoins) to `MspBonsterCatalog::ITEMS`; numery od 1001 w kolejności folderów, a
nazwa szkieletu w drugiej kolumnie musi być zapisana tak samo jak folder (Linux rozróżnia
wielkość liter). W 9 szkieletach pierwsza animacja (poza spoczynku, 2 klatki) miała w
paczce przypadkową nazwę („…_3”, „swf”, „test_20”) — w kopii na serwerze jest to
`Body_Static`, której szuka klient.

Serwer (`MspBonsters`, usługa `AMFBonsterService`):

- `GetBonsterTemplateList` — szablon 0 „Boonie” z `SkeletonPath` `boonies/` i animacjami:
  stany z `BonsterConstants` (`PetActionAnim00–12`) oraz sztuczki z okna zwierzaka (13–24;
  kategorie: sztuczki, świętowanie, słodkie). `AnimationData` to „Body_X;Face_X;Mouth_X;Eyes_X”
  — nazw, których dany szkielet nie ma (np. mycie to `Wash` albo `Washing`), klient po
  cichu pomija. `GetBonsterCandyPrices` musi zwracać listę, inaczej okno zwierzaka wywraca
  się na cenie cukierka;
- stan w polu konta `bonsters` (w MySQL jedzie w kolumnie `extra`): numer relacji od
  200000000, imię (na start nazwa z katalogu), doświadczenie, punkty głodu/brudu/zabawy
  (0–100, każdy punkt znika po 15 minutach — liczy to klient z daty ostatniej opieki),
  miejsce w pokoju, hotel, ostatnie użycie sztuczek;
- pokój i edytor (`GetBonsterListByActor`, `SaveNewAndOldPetsPositionsInMyRoom` zapisuje
  też `BonsterLocation`), pogłaskanie cudzego zwierzaka (+10 StarCoins, raz dziennie na
  zwierzaka, do 20 dziennie), zwierzak w statusie (ruch ust `BONSTER<numer>`);
- opieka (okno zwierzaka, moduł `petmodule.swf`, ustawienie `PetModule`): jedzenie 1–10
  za 3 StarCoins daje +25 punktów głodu i 5 doświadczenia (ulubione według charakteru
  +10 i +5), lekarstwo za 20 StarCoins leczy zwierzaka po 3 dniach bez jedzenia (chory
  odmawia zwykłego jedzenia, kod −5), cukierek za diamenty (cena wg poziomu) podnosi
  poziom, mycie i zabawa dodają punkty z minigry i 5 doświadczenia. Poziom `n` zaczyna
  się od `5·(n−1)·n` doświadczenia, ostatni to 32;
- hotel zwierzaków jak przy klasycznych (7/14/28 dni za 100/200/300 StarCoins, 28 dni tylko
  VIP); pobyt wstrzymuje ubywanie punktów.

Testy serwera: `tools/test-bonsters.php` i `tools/test-bonster-shop.php`.

### Stary sklep z gniazdami

Stary `ClickItemShop` (Świat → Zwierzaki już do niego nie prowadzi) miał **trzy jajka na
gniazdach z tła** — tak jak w oryginale — i co tydzień były to inne zwierzaki. Katalog, który serwer wysyła klientowi, ma
wszystkie pozycje (z niego biorą się grafiki także dla zwierzaków, które gracz
już ma), ale numer działu w polu `ClickItemVO.Discount` (1 Boonies, 2 PetPets,
0 ukryty) dostają tylko trzy pozycje na dany tydzień, a łatka
`ClickItemShopGrid` pokazuje wyłącznie je — na miejscach 56, 240 i 424 przy
y = 185 (oryginał miał tam 56, 238 i 424). Lista działu przesuwa się co poniedziałek o `SHOP_SLOTS` (3) pozycje,
więc dwanaście Boonies obraca się w cztery tygodnie, a siedem PetPetów w trzy.
Tydzień liczy `MspPetCatalog::weekNumber()` i zmienia się w poniedziałek
o 00:00 UTC.

Skalę jajka ×100 serwer wysyła w `isNew`; klient nie czyta tych dwóch pól przy
zwierzakach nigdzie indziej. Każdy zwierzak kosztuje 500 StarCoins; sprawdzanie
VIP dla numerów 3 i 7 jest w kliencie wyłączone. Wcześniej łatka układała
wszystkie jajka w siatce sześciu kolumn — wychodziły poza tło sklepu i zasłaniały
napisy, a lista dostawała pasek przewijania.

Numery mają znaczenie w kliencie: 1–5 i 7 losują wygląd (`Pet`), 19 to
`Puppy`, reszta ma stały wygląd (`XMasBoonie`), więc zwierzaki tematyczne
mają numery od 9. Wymiana za diamenty losuje w grupach [1, 2, 3] i [4, 5, 7],
dlatego 5 i 7 istnieją jako ukryty Furball. Kupione jajka dostają numery
relacji od 100000000, darmowy Boonie konta ma numer aktora ×10 + 1.

Po zakupie gra przechodzi do edytora pokoju: jajko stawia się w pokoju,
a klik na nim i „Wykluj się” wysyła `HatchPet` z wylosowanym wyglądem.
Karmienie kosztuje 10 StarCoins i daje punkt jedzenia (VIP 2); dziewięć punktów
to kolejny etap. Klient liczy 8 godzin przerwy od `LastFeedTime`, a serwer
oddaje tę datę cofniętą tak, żeby przerwa wynosiła `FEED_INTERVAL`
(30 minut). Głód zjada punkt dziennie, a przy −2 zwierzak choruje
(lekarstwo 20 StarCoins). Mycie brudnego zwierzaka, zabawa piłką, imię,
głaskanie cudzych zwierzaków (StarCoins raz dziennie na zwierzaka) i hotel
też są obsłużone. Zwierzak wybrany w statusie (`Mood.MouthAnimation` to jego
numer) stoi obok postaci w oknie gracza (`ActorSpecialSummary.PetClickItem`).

## Świnka-skarbonka

Przy każdym zdobytym Fame klient pokazuje wpadające do świnki 25% tej
wartości (`AwardVisualizationController.spawnAwards`), a po zamknięciu
zwykłego koła gracz bez VIP-a widzi dodatkowe 10–75 StarCoins. Klient tylko
animuje — przy odświeżeniu konta pobiera stan świnki z `GetPiggyBank`, więc
wcześniej wracała do zera. Serwer dopisuje teraz bonus sam: po każdej zmianie
stanu, w której urosło Fame zalogowanego gracza, ćwierć przyrostu trafia do
`piggyFame` (`addPiggyFameBonus` w `runOnState`), a zwykłe koło dokłada losowe
10–75 StarCoins do `piggyMoney`. Bez VIP-a świnka mieści 5000 Fame i 2500
StarCoins (tyle, ile `PiggyBankAmountManager` w kliencie); VIP nie ma limitu
i może ją rozbić. Rozbicie (`DestroyPiggyBank`) nie liczy bonusu od
odebranego Fame.

## Fame za aktywność

Oryginalny backend dawał Fame za zakupy, status, filmy i resztę twórczości,
a lokalny nie dawał nic, więc poziomu prawie nie dało się wbić. Serwer
przyznaje je teraz sam (stałe na początku `MspServer.php`):

| Za co | Fame | Przed spamem chroni |
|---|---|---|
| wydane StarCoins (ubrania, przedmioty, animacje, klinika, prezenty, zwierzaki) | 10 za każde pełne 100 | reszta czeka na kolejny zakup |
| nowy tekst statusu (dymek albo wpis na własnej ścianie) | 100 | przerwa 5 minut |
| nowy film, artbook albo look | 200 | raz za każdy, przerwa 10 minut osobno dla filmów, artbooków i looków |
| zmieniona biografia w profilu | 50 | przerwa 30 minut, tylko przy zmianie treści |

- Film płaci dopiero przy zapisie z oknem nazwy: autozapis studia nazywa film
  „Autosaved” i nic nie daje. Film musi mieć w scenach choć jedną postać
  (`Complexity`), a filmy sprzed tej zasady nic nie dają.
- Poprawka istniejącego artbooka, looka czy filmu nic nie daje. Sama zmiana
  koloru statusu albo animacji też nie.
- Czasy ostatnich nagród i reszta ze StarCoins siedzą na koncie
  (`activityFame`), więc przerw nie omija przeładowanie gry.
- Fame Booster podwaja te nagrody jak każde inne Fame, a ćwierć trafia do
  świnki.
- Status, film, look i zakup ubrań pokazują „+Fame” jak w oryginale (klient
  czyta wynik `SetMood`/`PostToWall` albo pole `awardedFame`). Po artbooku,
  biografii i pozostałych zakupach licznik Fame odświeża się sam w ciągu
  kilku sekund.

## Odświeżanie bez przelogowania

Klient sam odświeża zalogowaną postać przez `LoadActorDetailsExtended`, ale
oryginalnie robił to co cztery minuty — dlatego nowe zaproszenia, autografy
i sława pojawiały się dopiero po ponownym zalogowaniu. `main_local.swf`
przestawia `ActorReload.INTERVAL_RELOAD` na dziesięć sekund. To jedno
wywołanie AMF, znacznie lżejsze niż lista aktywności, którą klient odpytuje
i tak co dwie sekundy.

To odświeżanie, nie push: zmiany widać do dziesięciu sekund, a nie
natychmiast. Powiadomienia od innych graczy (zaproszenia, prośby, autografy)
nie czekają na nie — przychodzą osobną drogą, opisaną niżej.

Gazetka Fame pokazuje wyłącznie to, co jest jeszcze do odebrania. Wcześniej
trzymały ją na liście sumy historyczne (`count`, `fame`, `starcoins`), więc
odebrana kategoria zostawała na widoku, choć licznik powiadomień — liczony
po samym bonusie — pokazywał już zero.

## Ogłoszenia ekipy

Panel → **Ogłoszenie**. Wpisujesz tytuł (do 40 znaków) i treść (do 300),
wybierasz rodzaj i klikasz **Wyślij do wszystkich** — każdy, kto jest w grze,
dostaje w ciągu kilku sekund okienko z komunikatem. Obok formularza jest
podgląd, a pod nim historia ostatnich ogłoszeń z liczbą graczy, do których
dotarły, i nickiem osoby, która je wysłała.

Rodzaje to trzy okna, które gra ma u siebie: **Dobra wiadomość**
(`Prompt.showFriendly`, wesołe), **Zwykłe ogłoszenie** (`Prompt.show`)
i **Ważne ostrzeżenie** (`Prompt.showEvil`). Pole „Pokazuj wchodzącym jeszcze
przez” decyduje, jak długo ogłoszenie czeka na tych, którzy dopiero wejdą do
gry — zero znaczy „tylko ci, którzy grają teraz”. Przycisk **Odwołaj** zdejmuje
je wcześniej; komu już wyskoczyło, temu okno zostaje.

Droga jest ta sama co przy powiadomieniach na żywo: klient co trzy sekundy pyta
o `GetChatUpdates`, a serwer dokłada do odpowiedzi wpis `announce` z tytułem,
treścią i rodzajem; łatka `FriendList` otwiera na nim okno komunikatu. Każde
konto dostaje ogłoszenie raz — pamięta jego numer w polu `announcementSeen`,
a licznik „dotarło do…” podbija się przy odbiorze. Treść leży
w `local_server/data/announcements.json` (przy MySQL w tabeli wierszy),
a kod w `local_server/lib/Announcements.php`.

## Eventy

Zakładka **Eventy** w panelu (uruchamia tylko administrator, reszta ekipy
może podejrzeć). Wszystko działa od razu, bez ponownego logowania graczy:

- **Specjalne Pozdrowienie** od Pixi Star albo Zac Sky do każdego, kto był
  w grze w ciągu podanej liczby dni (0 = do wszystkich kont). To zwykłe zadanie
  „Specjalne Pozdrowienie” z animacją i nagrodą (`SPECIAL_GREETING_STARCOINS`
  i `SPECIAL_GREETING_FAME`, z boosterem więcej). Graczom w grze kanał
  `GetChatUpdates` daje sygnał `greeting` i animacja wyskakuje w ciągu kilku
  sekund, reszta zobaczy ją po wejściu. Konto Zac Sky (numer 4,
  `AnchorCharacters.MALE_ANCHOR`) zakłada się przy pierwszym pozdrowieniu od
  niego. Wygląd bierze ze zwykłego konta „Zac Sky”, jeśli takie istnieje, a bez
  niego ubiera się w strój chłopaka z kreatora.
- **Booster Fame**: mnożnik 2–10 do podanej chwili. Działa wszędzie tam, gdzie
  Fame Booster za diamenty (`boostedFame`). Z nim się nie sumuje, bo liczy się
  większy mnożnik.
- **Szalone zakupy**: te same co za diamenty, tylko dla wszystkich. Wszystko za
  StarCoiny kosztuje połowę. Gra sama pokazuje wtedy ceny o połowę niższe
  i wszędzie pisze o 50%, więc innej obniżki nie da się ustawić. Serwer pobiera
  połowę ceny wszędzie tam, gdzie klient ją pokazuje (`shoppingSpreeActive`):
  ubrania i rzeczy, tła, animacje, klinika urody (połowa całej sumy),
  zwierzaki i hotel, zakup z listy życzeń. Ceny w diamentach się nie zmieniają.

Booster i Szalone zakupy liczą się w grze jak kupione. Klient dostaje je na
liście aktywnych dodatków (`activeSpecialsForUser`, do końca wydarzenia). Na
górnym pasku widać zegar i baner, a sklep diamentowy nie sprzeda drugiego
(przycisk jest wyłączony, serwer odpowiada `ALREADY_ACTIVE`). Każde włączenie
i zakończenie podbija numer wersji w dokumencie `events`. GetChatUpdates daje
wtedy graczom w grze jednorazowy sygnał `specials` (pole konta `eventsSeen`),
a łatka `FriendList` pobiera listę dodatków na nowo. Bez tego klient zna je
tylko z logowania. Oba wydarzenia mogą też od razu wysłać ogłoszenie
(„Booster Fame x3!”), widoczne dla wchodzących do końca wydarzenia (najwyżej
12 godzin). Stan i historia leżą w dokumencie `events`, a kod
w `local_server/lib/Events.php`.

**Zresetuj wygląd** (Użytkownicy → gracz → Funkcje moderacyjne) pomaga, gdy
zepsuta rzecz albo część twarzy nie daje graczowi wejść do gry. Zdejmuje
wszystkie ubrania, zakłada startowy strój z kreatora dla płci postaci
(RegNewUser 1 albo 2) i przywraca domyślną twarz i pozę. Ubrania zostają
w szafie, kolor skóry się nie zmienia. Wpis trafia do historii konta.

## Gry wieloosobowe

Gry z zakładki **Gry** (Quiz, Casting, Ubierz się, Wybieg, Zwariowane Karty)
działały w MSP na osobnym serwerze czasu rzeczywistego (Flash Media Server):
klient łączy się przez RTMP z aplikacją gry (`rtmp://<host>/theQuiz`), a stan
pokoju — gracze i przebieg gry — idzie przez zdalny SharedObject. Apache i PHP
tego nie potrafią, dlatego gry prowadzi osobny program: **`games_server/server.js`**
(Node.js 18+, bez dodatkowych bibliotek), który mówi tym samym protokołem
(RTMP, AMF0/AMF3, Shared Objects). Arkada to co innego — osobny moduł z grami
jednoosobowymi, których pliki były na serwerach MSP; tych nie mamy.

Wspólne dla wszystkich gier: gra rusza, gdy przy stole siedzi co najmniej dwóch
graczy (po 15 s), a gdy w trakcie zostaje jeden — przerywa się i czeka na
następnych. Fame na koniec: zwycięzca 125, pozostali 50 razy liczba graczy przy
stole (przy dwóch 250 i 100) — tyle pokazuje tablica „Gra zakończona!”;
z Boosterem Fame odpowiednio więcej. Wyniki liczy serwer gier, a wypłaca PHP.

- **Quiz** (`theQuiz`, do 4 graczy) — 5 pytań po 15 s (runda kończy się
  wcześniej, gdy wszyscy odpowiedzą), za dobrą odpowiedź 2 punkty. Wygrywa
  najwięcej punktów, przy remisie szybsze dobre odpowiedzi. Pytania są
  w `games_server/quiz-questions.json`: `{"q": "pytanie", "a": ["dobra", "zła",
  "zła"]}` — pierwsza odpowiedź jest poprawna, serwer tasuje kolejność.
  Odpowiedzi najlepiej do ok. 16 znaków (przyciski są wąskie).
- **Casting** (`theStage`, do 4 graczy) — reżyser (postać nr 3, czyli Pixi Star)
  pokazuje na scenie trzy ruchy, a gracze powtarzają je w tej samej kolejności
  przyciskami. Pierwszy poprawny dostaje 2 punkty, kolejni po 1; 5 rund.
- **Ubierz się** (`theDressup`, do 5 graczy) i **Wybieg** (`theExclusiveDressup`,
  od 6. poziomu) — w każdej rundzie jeden gracz jest jurorem, reszta ma 60 s na
  strój do hasła rundy (np. „Bal maturalny”). W „Ubierz się” ubrania losuje
  serwer (PHP: `getRandomClothesByType` bierze rzeczy ze sklepu), na Wybiegu
  gracz ubiera się we własne. Potem juror wybiera najlepszy strój — punkt dla
  zwycięzcy rundy. Rund jest tyle, ilu graczy: każdy raz sędziuje.
- **Zwariowane Karty** (`theWordRelation`, do 5 graczy) — jak wyżej, tylko
  zamiast stroju gracze wykładają kartę ze słowem do hasła rundy (30 s),
  a juror wybiera najzabawniejszą. Hasła i karty są w `games_server/words.json`
  (razem z motywami „Ubierz się”); plik czyta się na nowo, gdy się zmieni.
- **Gra tylko dla Przyjaciół** — ta sama gra, ale w pokoju prywatnym: kto ją
  zakłada, wybiera rodzaj gry, a znajomi dostają zaproszenie na dolnym pasku
  (idzie tą samą drogą co inne powiadomienia, `SendLocalNotification`).
  Do pokoju prywatnego nie trafi nikt z losowania.

**Jak to się łączy z PHP** (`local_server/lib/Games.php`):

- logowanie zwraca `amsHash` — podpis gracza (HMAC wspólnym sekretem
  `local_server/data/games-secret.txt`, tworzonym przy pierwszym logowaniu);
  klient podaje go w `requestRoom`, serwer gier sprawdza go bez bazy;
- ustawienie `chatGameFMSServer` to host serwera gier — domyślnie host strony,
  inny można wpisać w `local_server/config.php`: `'games' => array('host' => '…')`;
- po grze serwer gier zgłasza wyniki do `games-local.php` (tylko z 127.0.0.1,
  z podpisem), a klient odbiera Fame przez `GiveMultiplayerEarnings` — raz
  za grę, wynik ważny 30 minut;
- „Ubierz się” prosi o losowe ubrania do przymierzenia (`getRandomClothesByType`
  w `local_server/lib/Games.php`) — bierze rzeczy ze sklepu, nic nie daje graczowi.

**Port 80 (RTMPT).** Hosting przepuszcza tylko porty 22 i 80 — połączenia na
1935 giną, zanim dotrą do VPS-a. Dlatego klient łączy się przez `rtmpt://`:
RTMP opakowany w żądania HTTP (`/open/1`, `/idle/…`, `/send/…`, `/close/…`),
które Apache przekazuje serwerowi gier (`.htaccess` → `127.0.0.1:1936`, tylko
lokalnie). Łatka klienta (`tools/build-client.ps1`) ustawia na starcie
`ComServerConnection.curProtocol` na `rtmpt://`. Działa to też u graczy, których
sieć blokuje nietypowe porty. Flash Player przyjmuje w odpowiedziach RTMPT tylko
bajt opóźnienia 0x01, więc tempo zapytań ustala serwer: puste `/idle` czeka
do 250 ms albo do pierwszych danych. Serwer nadal słucha też zwykłego RTMP na 1935.

**Instalacja na VPS-ie** (raz, przez PuTTY jako root):

```
apt install -y nodejs
cp /var/www/html/games_server/bettermsp-games.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now bettermsp-games
a2enmod proxy proxy_http
systemctl restart apache2
```

Moduły `proxy` i `proxy_http` są potrzebne regule RTMPT w `.htaccess`.
Stan: `systemctl status bettermsp-games`, log: `journalctl -u bettermsp-games -f`
albo `local_server/data/games-server.log`. Po wgraniu nowego `server.js`:
`systemctl restart bettermsp-games`. Katalog `games_server` jest zablokowany
dla przeglądarki (`.htaccess`). Szczegółowy log protokołu: zmienna
`MSP_GAMES_DEBUG=1` w pliku usługi.

## Zwykłe pokoje czatu (plaża, kawiarnia, centrum handlowe…)

Plaża, kawiarnia, centrum handlowe, skatepark, park, klub VIP, kino, szkoła
i pokój tematyczny — dziewięć miejsc z `ChatElement`/`ChatRoomType` — używały
tej samej łatki co pokój własny: bez serwera pokojów, więc każdy gracz wchodził
do pustego pokoju i nie widział nikogo innego, nawet gdy ktoś inny stał w tym
samym miejscu. Gry z zakładki **Gry** (opis wyżej) już wtedy łączyły się
naprawdę — poprawka rozszerza to na te dziewięć pokoi, korzystając z tego
samego serwera (`games_server/server.js`) i tego samego mechanizmu SharedObject.

**Jak to działa.** `ChatRoom.connectTextSo` (łatka `ChatRoomLocalEnter.pcode`)
łączy się naprawdę, gdy `isInMinigame` (gra) **albo** `roomType != null`
(jeden z tych dziewięciu pokoi — `ChatRoomType.getChatType` rozpoznaje go po
`roomId`). Tylko pokój własny albo znajomego (`isInMyRoom`, `roomType == null`
i `isInMinigame == false`) zostaje przy starej, lokalnej ścieżce — to osobna
sprawa, którą trzeba by rozwiązać inaczej (pokój ma właściciela i prywatność
do pilnowania), więc na razie bez zmian. Reszta łańcucha (`MakeRoomReadyToUse`,
`Exit`) już wcześniej sprawdzała obecność `text_so`, a nie `isInMinigame`
wprost, więc nie trzeba było ich ruszać — same zaczynają łączyć się naprawdę,
gdy `connectTextSo` utworzy `text_so`.

**Druga, ukryta połowa łatki: `ChatElement.enter()`.** Sam gate w
`connectTextSo` nie wystarczył — osobna, starsza łatka (`ChatElement.as`,
klik w ikonę plaży/kawiarni itd. na mapie) w ogóle nie pytała serwera pokojów
o przydział instancji (`RoomRequester.requestRoom`), tylko wchodziła prosto
z samą nazwą działu (`"theCafe"` zamiast prawdziwej instancji `"theCafe_1"`).
Poza tym, że pokój był więc bez sensownej nazwy, `requestRoom` to jedyne
miejsce, które w ogóle **łączy** `RoomRequester.roomCon` (statyczne
`NetConnection`) z serwerem gier — bez tego wywołania `text_so.connect(roomCon)`
podłącza się do niepołączonego połączenia i nic się nie synchronizuje: ani
obecność, ani chodzenie (kliknięcie działa dopiero po echu własnego wpisu
„ENTER” z powrotem przez SharedObject — `ChatRoom.onSyncActor`, `sendPosition`
ustawia `myRoomReadyToGo`), ani pisanie (ta sama droga co obecność —
`CommunicationHandler.sendStatus`), ani dołączanie do pokoju znajomego przez
kulkę w mini profilu (serwer trzyma pokoje pod kluczem `<typ>_<numer>`, więc
szukanie po samej nazwie działu kończy się „ta gra już nie istnieje”).
Łatka przywraca oryginalne wywołanie `RoomRequester.requestRoom(ident, ...)` —
serwer już zna te dziewięć typów (`SocialRoom` niżej), więc działa jak
w oryginale.

Po stronie serwera gier nowa, lekka klasa `SocialRoom` (bez rund, punktów ani
zegara — sama obecność) jest zarejestrowana pod dziewięcioma identami klienta
(`theBeach`, `thePark`, `theCafe`, `theSkatePark`, `theMall`, `theClub`,
`videoTop`, `themeRoom`, `theSchool`), limit 40 osób na pokój. Gracze sami
rozgłaszają swój `ActorCommunicationVO` (pozycja, animacja, wiadomość, strój)
przez `SO_REQUEST_CHANGE` na własny klucz (numer aktora) — generyczny
mechanizm `SharedObject.setRaw` w `server.js` (już używany przez gry) sam
przekazuje to wszystkim innym w pokoju, więc `SocialRoom` nie publikuje
niczego samodzielnie, tylko trzyma listę obecnych i sprząta klucze po wyjściu.

**Flaga „trwały” SharedObjectu — dlaczego nie dało się chodzić ani pisać.**
Pokój czatu otwiera swój SharedObject jako trwały
(`SharedObject.getRemote(roomId, uri, !isInMinigame)` — dla zwykłego pokoju
`true`), a gra jako nietrwały. Klient wysyła tę flagę (2) w każdej wiadomości
SharedObject i dopasowuje odpowiedź serwera po nazwie **i** tej fladze — serwer
odpowiadał zawsze flagą 0, więc Flash po cichu wyrzucał odpowiedź: pokój nigdy
nie dostawał pierwszej synchronizacji, nie wysyłał wejścia (`ENTER`), nie
ustawiał `myRoomReadyToGo` (chodzenie) i nie pokazywał dymków ani innych osób.
W grach flaga to 0 i tam wszystko działało. Teraz `server.js` zapamiętuje flagę,
z którą dane połączenie otworzyło obiekt, i odsyła dokładnie tę samą.

**Lista pokoi („Kawiarnia Flora”).** W lewym górnym rogu pokoju jest lista
(`ChatRoomSelector`): klient pyta serwer gier o `getRoomNameList` i dostaje
pokoje tego samego rodzaju z nazwą i liczbą osób. Każdy rodzaj ma stałe numery
pokoi (`theCafe_1`, `theCafe_2`…) i nazwy: 1 = Flora, 2 = Luna, 3 = Stella,
potem Aurora, Bella, Gloria, Fiesta, Riviera, Laguna, Bonita, Viva, Siesta.
Lista zawsze pokazuje trzy pierwsze pokoje (także puste), a więcej, gdy są
zajęte. Wybranie pokoju z listy przenosi do niego (`joinRoom` po numerze; pusty
pokój powstaje od nowa). Wejście z mapy trafia do pokoju, w którym już ktoś
jest, a gdy jest pełny (40 osób) — do następnego. Łatka
`ChatRoomSelectorLabel.pcode` odwraca kolejność w etykiecie (oryginał pisał
„Flora Kawiarnia”) i po wypełnieniu odznacza listę — bez tego Flex sam
zaznaczał pierwszy dodany pokój i zamiast bieżącego było widać np.
„Kawiarnia Stella (0)”. Stara łatka, która wyłączała tę listę
(`ChatRoomSelectorLocal`), jest usunięta.

**Strój widziany przez innych.** Klient wysyła innym swój wygląd z
`Actor.createActorMinimalFromActor`, biorąc wszystkie `ActorClothesRels` —
w prawdziwym MSP były to tylko założone rzeczy, a nasz serwer oddaje tam całą
szafę (z `IsWearing`). Inni widzieli więc postać we wszystkim naraz (np. białe
spodnie na dżinsach i kokardę). Łatka `ActorMinimalWornClothes.pcode` bierze
tylko rzeczy z `IsWearing`. Przebranie się w trakcie pobytu w pokoju
(`postOutfit`) serwer przekazuje obecnym jako `ActorClothes<numer>` i kasuje po
5 s — klient przy każdej synchronizacji ubiera postać od nowa z każdego takiego
wpisu, więc stały wpis powodowałby miganie.

**Pokój tematyczny.** Szósty kafelek w menu czatu (`ChatRoomType.THEMEROOM`)
bierze nazwę i tło z `GetWorldThemeInfo`; serwer zwracał tam `null`, więc
kafelek miał napis „localeschool”. Teraz to „Szkoła” (`RoomName` to gotowy
napis — klient go nie tłumaczy — tło `graphics/school_yard.swf`).

**Zaproszenie do pokoju.** Przycisk z ludzikiem obok listy pokoi wysyła
znajomemu zaproszenie z numerem bieżącego pokoju (np. `theCafe_2`); zaproszony
klika ✓ w okienku nad paskiem znajomych albo kulkę przy miniaturce i wchodzi
przez `joinRoom` do tego samego pokoju. Okienko samo znika po kilku sekundach
bez ruchu myszy — wtedy zostaje kulka przy miniaturce.

Test logiki serwera (bez Flasha): `node tools/test-social-rooms.js` —
przydział pokoi, przekazywanie zmian, sprzątanie po wyjściu, lista pokoi,
przejście do innego pokoju, strój i flaga „trwały”.

Ustawienie `chatFMSServer` (obok istniejącego `chatGameFMSServer`) wskazuje
klientowi ten sam host serwera gier dla zwykłych pokoi czatu —
`RoomRequester` pyta o nie osobno, ale to ten sam proces Node.js, więc host
jest identyczny; osobnej instalacji ani nowego portu nie trzeba.

**Po wgraniu na VPS koniecznie `systemctl restart bettermsp-games`** — sam
nowy `server.js` nie wystarczy, dopóki usługa nie zostanie zrestartowana
(patrz wyżej).

## Kluby

Przycisk „Kluby” w Mieście Filmów otwiera moduł `clubsmodule_v1.swf` (adres
z ustawienia `ClubsModule`). Obsługę po stronie serwera ma usługa
`MovieStarPlanet.WebService.Clubs.AMFClubService` w `local_server/lib/Clubs.php`.

Co działa:

- **Zakładanie klubu** — tylko VIP i tylko jeden klub (tak jak w MSP; klient
  pokazuje niebieskie okno „tylko VIP”, a przy drugim klubie komunikat
  o limicie). Klub ma nazwę, kategorię (Inne, Muzyka, Filmy, Zwierzaki, Sport,
  Moda) i rodzaj: publiczny albo prywatny.
- **Logo i tło z opisem** to dwa ArtBooki zakładane przez serwer przy tworzeniu
  klubu. Właściciel edytuje je ołówkami w oknie klubu (zwykły edytor ArtBooków).
  Nie pokazują się na listach ArtBooków gracza — mają rodzaj 9 i numer klubu.
- **Lista klubów** z filtrem (wszystkie, moje, znajomych, założone przeze mnie),
  kategorią, sortowaniem (liczba członków, alfabetycznie, ostatnia aktywność,
  najnowsze) i wyszukiwarką.
- **Członkowie** — klub publiczny przyjmuje od razu, prywatny zapisuje
  zgłoszenie, które właściciel zatwierdza albo odrzuca. Właściciel może też
  wyrzucić członka; gracz sam wychodzi z klubu. Limit: pięć klubów na konto.
  Ukryci administratorzy nie pokazują się na liście członków.
- **Treści klubu** — looki, filmy i ArtBooki przypinane przez członków
  (zakładka „Aktywność”). Usuwa je właściciel, ekipa albo ten, kto je dodał.
  Filmów z YouTube nie ma, bo serwer nie prowadzi listy filmów zewnętrznych.
- **Komentarze** pod klubem (zakładka „Komentarze”) — pisać mogą tylko
  członkowie, kasuje autor, właściciel klubu albo ekipa. Limit 200 znaków.
- **Ekipa** widzi w oknie klubu przycisk kasowania klubu (`Deleted = 2`)
  i może wyczyścić logo albo tło (`ResetClub`).

### Edytor logo i tła klubu — okno zostawało białe na zawsze

`createClubScrapBlog()` zakładał puste ArtBooki logo/tła klubu z polem
`background` ustawionym wprost na `null` w AMF3. Prawdziwy klient nigdy
tak nie zapisuje — `ScrapBlogUtils.backgroundToData()` przy braku tła zawsze
zwraca puste `Object`, nigdy `null`. Klientowy `ScrapBlogUtils.dataToBackground()`
woła `param1.hasOwnProperty(...)` bez sprawdzenia `null`, więc przy wczytaniu
takiego ArtBooka `loadBackground()` rzucał `TypeError #1009` w środku
`scrapBlogLoaded()` — zanim zdążyło się pokazać cokolwiek. Okno edytora
zostawało białe i puste na stałe, bo cały dalszy ciąg wczytywania
(`initLoadedState`, pokazanie przeglądarki klipartów) nigdy nie ruszał.

Poprawka w `Clubs.php` (`createClubScrapBlog`, `resetClub`) i `MspServer.php`
(`scrapBlogObject`) zapisuje puste `background` jako `array()` zamiast `null`
— to samo, co robi prawdziwy klient. Kluby założone przed poprawką mają stare,
zepsute dane zapisane już w `users.json`; nowe kluby (i logo/tło wyczyszczone
przez ekipę przez „Reset”) dostają poprawny format od razu.

Osobno: sama przeglądarka klipartów (`ClubLogoItemBrowser`/
`ClubBackgroundItemBrowser`) po otwarciu edytora nie pokazuje jeszcze ikon
kategorii po prawej — mimo że reszta okna (tytuł, Zapisz, Podgląd) działa
poprawnie. To osobny, nierozwiązany na razie problem w warstwie Flex/AS3
(zwykły edytor ArtBooków tej samej klasy bazowej pokazuje ikony bez
problemu), więc wybór nowego obrazka na logo/tło klubu na razie nie działa.

Dane siedzą w `users.json` (albo w tabeli `state` przy MySQL) w kluczach
`clubs`, `clubMembers`, `clubEntities` i `clubComments`. Limity (jeden klub dla
VIP-a, pięć członkostw) to stałe `CLUB_LIMIT_VIP`, `CLUB_LIMIT_REGULAR`
i `CLUB_MEMBERSHIP_LIMIT` na górze `local_server/lib/Clubs.php`.

## Powiadomienia na żywo

Zaproszenie do znajomych, prośba o najlepszą przyjaźń albo o serduszko,
przyjęcie i odmowa, autograf, prezent, polubienie pokoju czy nowy poziom:
klient nadawcy wysyła takie powiadomienie wprost do drugiej osoby. W oryginale
szło przez XMPP (`FriendNotificationChannel`), którego lokalny serwer nie ma,
więc nic nie dochodziło — zaproszenie nie wyskakiwało na dolnym pasku
z przyciskami, a przy ikonie przyjaciół nie pojawiała się jedynka.

Teraz trzy metody wysyłające tego kanału w `main_local.swf` przekazują
powiadomienie do `FriendList.relayLocalNotification`, a ta do serwera
(`SendLocalNotification`). Treść to `NotificationObject` zapisany tak samo
jak w wiadomości XMPP (`SerializeUtils.serializeToString`); serwer jej nie
rozpakowuje, tylko odkłada w kolejce `liveNotifications`. Klient odbiorcy
zabiera ją przy odpytywaniu `GetChatUpdates` (wersja 3, co trzy sekundy) i
podaje dalej tą samą drogą co XMPP. Dolny pasek sam pokazuje wtedy prośbę
z przyciskami, a ikona przyjaciół podbija licznik.

- Nadawcę wpisuje serwer z sesji, więc nie da się podszyć pod innego gracza.
- Cel 0 to wszyscy znajomi online (aktywności typu awans czy nowy pokój).
- Osoba spoza znajomych dostaje tylko prośby o relację, odpowiedzi na nie,
  blokady, autograf i wymianę — jak w oryginalnym filtrze XMPP.
- Czat, status i wygląd nie idą tą drogą: mają już `GetChatUpdates`
  i `GetFriendActivityFeed`, więc dymki by się dublowały. Czatu grupowego
  lokalnie nie ma, a „pisze…” spóźniałoby się za samą wiadomością.
- Powiadomienie jest ulotne: czeka najwyżej dwie minuty, od jednego nadawcy
  wisi ich najwyżej 40 i tyle samo u jednego odbiorcy. Kto jest offline,
  i tak znajdzie prośby na liście „Do zrobienia”.

## Autografy

Ikona pióra w mini profilu („Daj Autograf”) podpisuje się drugiej osobie.
Autograf wart jest `10 × poziom podpisującego` sławy. Podpisujący dostaje ją
od ręki, bo klient pokazuje mu animację nagrody — bez tego licznik w grze
rozjechałby się ze stanem konta. Odbiorca dostaje autograf **do odebrania
w Magazynie Fame**: tam czeka jako bonus, a sława dolicza się dopiero po
kliknięciu „Odbierz”. Tak działa gazetka i bez tego wpisu była pusta.

Licznik nowych rzeczy w gazetce przychodzi tylko w danych logowania
(`PostLoginData.NumberOfFameNotifications`), więc sama liczba na belce
odświeża się dopiero po ponownym zalogowaniu. Zawartość gazetki jest
pobierana przy każdym otwarciu, więc autograf widać od razu.

Przerwa między autografami zależy od VIP-a i jest liczona po obu stronach:
konto bez VIP-a podpisuje raz na godzinę, a VIP odpowiednio 4, 8, 15 albo 30
razy na godzinę zależnie od poziomu. Klient sam pokazuje, ile zostało do
następnego, ale serwer pilnuje tego samego — inaczej wystarczyłoby
przeładować grę, żeby przerwę ominąć.

Dane aktora niosą `TimeOfLastAutographGiven` i `TimeOfLastAutographGivenStr`
(format `RRRR-MM-DD GG:MM:SSZ`). Wcześniej szło tam `null`, a klient robi na
tym `getTime()` — przycisk wywalał się na nullu, zanim cokolwiek wysłał.

## Serduszko i najlepsi przyjaciele

W mini profilu drugiej osoby działają dwa przyciski obok „Jesteście
przyjaciółmi”: gwiazdka prosi o najlepszą przyjaźń, serduszko o związek.
Każdy otwiera okno z pytaniem, a po „Tak” druga strona dostaje prośbę
i widzi ją u siebie. Przyjęcie wiąże obie strony i od razu robi z nich
zwykłych znajomych; odmowa zostawia pytającemu informację o odmowie.

Serduszko jest jedno, najlepszych przyjaciół najwyżej trzech. Zajęte
miejsce blokuje kolejną prośbę, chyba że klient poda, z kim zrywamy —
wtedy stara relacja znika. Dwie prośby w obie strony zamykają się od razu
w parę: klient czyta odpowiedź serwera jako „czy już jesteśmy razem”,
więc zwykła prośba musi wrócić jako `false`, inaczej gra pokazałaby
związek zamiast czekania na odpowiedź.

Prośba trafia na listę „Do zrobienia” odbiorcy, a jeśli jest online, wyskakuje
mu też od razu na dolnym pasku z przyciskami (zob. „Powiadomienia na żywo”).
Typ 5 i 6 oznacza chłopaka i dziewczynę, 9 najlepszego przyjaciela; po tych
numerach klient poznaje, którą metodę wywołać. Wpis znika po odpowiedzi.

Nieodebrana prośba nie blokuje kolejnej: serduszko jest jedno, więc nowa
prośba zastępuje poprzednią. Miejsce zajmują wyłącznie potwierdzone relacje.
Bez tego klient milczał, bo nie ma jak pokazać odmowy serwera — wysyła
prośbę bez informacji, z kim zerwać.

Po wysłaniu prośby klient próbował powiadomić drugą osobę przez XMPP,
którego lokalny serwer nie ma. Wyjątek na nullu ubijał resztę callbacku,
więc przycisk nie pokazywał, że pytanie poszło, mimo że dane były zapisane.
`FriendNotificationChannel.sendNotificationToFriend` w `main_local.swf` nie
dotyka już połączenia XMPP — przekazuje powiadomienie serwerowi.

Potwierdzone relacje wracają miniaturkami w mini profilu, a zalogowany
aktor niesie je w `ActorRelationships`. Stan siedzi przy koncie w
`local_server/data/users.json` pod kluczem `relationships`.

Okna z pytaniem („Poproś o zostanie najlepszymi przyjaciółmi”, „Bądź moją
dziewczyną” i wszystkie inne `showFriendly`) pytały serwer o ustawienie
`ChristmasStartDate`, tylko po to, by wybrać zwykły albo świąteczny popup,
a potem robiły `split("-")` na odpowiedzi. Sesja, której cache ustawień
powstał przed dodaniem tej daty, dostawała null i wyjątek: okno w ogóle się
nie pokazywało, więc żadna prośba nie wychodziła. `main_local.swf` woła
teraz `doShowFriendly` wprost, z domyślnym `PositivMessagePopUp.swf`.
Kosztem jest świąteczna wersja okienka, która nigdy się nie pokaże.

## Kody podarunkowe: VIP i pakiety

Zakładka **Kody** w panelu generuje kody, które wpisuje się w grze
w **Ustawieniach → Kod Karty Podarunkowej** (ten sam formularz otwiera
przycisk „mam kod” w oknie VIP-a). Nic nie trzeba kupować ani wyceniać —
panel jest jedynym miejscem, w którym kody powstają.

Kod ma **dokładnie dziesięć znaków**, bo tyle wymaga oryginalny klient:
krótszy albo dłuższy odrzuca sam, nie pytając nawet serwera. Alfabet pomija
znaki, które mylą się przy przepisywaniu (0/O, 1/I/L, 5/S, 8/B).

Kod na VIP-a nadaje wybrany rodzaj i liczbę dni. Dni **dokładają się** do
VIP-a, który jeszcze trwa, a rodzaj podnosi się tylko w górę — słabszy kod
nie zdegraduje Star VIP-a. Kod na pakiet dopisuje StarCoins, diamenty
i Fame. Jeden kod działa raz, na jedno konto; zajmujemy go jeszcze pod tą
samą blokadą pliku, więc dwa równoległe wywołania nie odbiorą go dwa razy.
Kod można też unieważnić — gra pokazuje wtedy inny komunikat niż przy
zwykłej literówce. Kody leżą w `local_server/data/redeem-codes.json`.

Klient czyta z odpowiedzi pole `Code`: `5` przyjęty, `6` już użyty,
`9` unieważniony, cokolwiek innego to nieprawidłowy kod. Dopiero po piątce
zamyka okno i sam woła `ActorReload.requestReload()` — dlatego **nie trzeba
się przelogowywać**: waluty pojawiają się na belce od razu.

### Okno z podsumowaniem i oferty VIP-a

Po odebraniu kodu wyskakuje oryginalne okno „Gratulacje! Jesteś teraz
Elite VIP!” z odznaką, długością i przyznanymi walutami — dokładnie to,
co MSP pokazywało po zakupie. Działa to tak: odebranie kodu zapisuje na
koncie `lastTransactionId`, klient widzi go przy odświeżeniu postaci
(`PaymentActorReloadListener`), pobiera `GetTransactionPurchaseInfo`
i rysuje `PaymentSuccessPopup`. Identyfikator zerujemy przy odczycie,
inaczej okno wracałoby co dziesięć sekund. `UpgradeVIPTier` musi być `-1`
— tylko wtedy okno pokazuje czas VIP-a i waluty, a nie samą odznakę.
`ContentId` wybiera grafikę i nagłówek: końcówki `10`–`40` to rangi
VIP-a przy danej długości, a `42001` to pakiet samych walut.

Na natywnym Flashu to okno długo się nie pokazywało, choć w Ruffle działało.
`PurchaseTypeDomain` ma pola `Items` i `Visuals` typu `ArrayCollection`, a serwer
wysyłał tam zwykłe tablice AMF. Ruffle to przepuszcza, natywny Flash rzuca przy
odczycie `TypeError #1034` i cała odpowiedź `GetTransactionPurchaseInfo` (tak samo
`GetCurrentPaymentPossibilities`) przepadała. Teraz obie listy idą jako kolekcje
obiektów `PurchaseItemDomain` i `PurchaseVisualDomain`.

Oryginalny `PaymentActorReloadListener` pokazywał okno tylko kontom z **aktywnym
VIP-em** (pakiety walut kupowali w MSP wyłącznie VIP-owie). Łatka
`tools/client-patches/PaymentActorReloadListener.as` w `main_local.swf` zdejmuje
ten warunek, bo kody z panelu dają pakiety także graczom bez VIP-a. Zapamiętuje też
numer pokazanej transakcji — odświeżenie postaci co kilka sekund nie otworzy
drugiego okna, zanim moduł płatności się wczyta. Okno wyskakuje zaraz po
wpisaniu kodu (klient sam od razu odświeża postać).

### Pakiet Diamond

Zakładka **Kody → Pakiet Diamond** w panelu to lista pakietów. Pakiet ma nazwę,
płeć (kobiecy albo męski — działają osobno) i strój: **włosy, top, spodnie i buty**
(wymagane) oraz **najwyżej jeden dodatek**. Każde ubranie wybiera się z plików SWF
w `swf/<kategoria>/`, z miniaturką i podglądem stroju. Plik, którego nie ma
jeszcze w katalogu gry, dostaje ukrytą pozycję spoza sklepu (`rewardOnly`), bo
szafa gracza zapisuje ubrania po numerze z katalogu. Oprócz stroju pakiet daje
zawsze **185 000 StarCoins i 2 100 diamentów** (`DIAMOND_PACK_STARCOINS`,
`DIAMOND_PACK_DIAMONDS`). Pakiety leżą w dokumencie `diamond-packs`
(`local_server/data/diamond-packs.json` albo MySQL).

Przełącznik **Aktywny** pokazuje pakiet w grze: w oknie pakietów (kliknięcie
diamentów w prawym górnym rogu → Kup) czwarty pasek, „Diamond”
(`ContentId 42001`, grafika `Addon_Diamond_02` z polem `ItemBox`), zamiast
zwykłego pakietu walut ma ubrania, nazwę i waluty aktywnego pakietu płci
oglądającego. Aktywny może być jeden pakiet kobiecy i jeden męski — włączenie
wyłącza poprzedni tej płci. Okno pakietów widzą VIP-owie, tak jak w MSP (konto bez
VIP-a dostaje w tym miejscu oferty VIP-a). Gracz, który ma już otwarte okno ofert
w tej sesji, zobaczy zmianę po ponownym zalogowaniu.

**Generuj kod** przy pakiecie (albo „Pakiet Diamond” w oknie generowania) zakłada
kod z ubraniami i walutami pakietu z chwili generowania — późniejsza zmiana
pakietu nie zmienia już wydanych kodów.

### Ubrania w zestawach i zmiana koloru

Zestawy nagród (i kody ręczne) mogą dawać ubrania — np. same buty albo sukienkę,
najwyżej 10 (`REWARD_MAX_ITEMS`), wybrane tak samo z plików SWF. Po wpisaniu kodu
ubrania trafiają do szafy (jeszcze nie założone), a transakcja niesie ich listę,
więc okno „Gratulacje!” pokazuje je w polu `ItemBox`. Przy ubraniach z paletą
kolorów okno ma **przycisk zmiany koloru**: gracz wybiera kolor, a po zamknięciu
okna klient wysyła `SetColorOnActorItem` — serwer zapisuje kolor w najnowszym
egzemplarzu tego ubrania w szafie.

Okno chowało pole `ItemBox` razem z walutami, gdy nagroda nie miała StarCoins ani
diamentów. `tools/client-patches/PaymentSuccessItemBox.pcode`
(`PaymentSuccessPopup.loaderComplete` w `paymentmodule_v1.swf`) chowa dalej same
waluty, a `ItemBox` tylko wtedy, gdy nie ma ubrań.

Kliknięcie w VIP-a albo pakiety pokazuje, co która oferta daje — cztery
paczki VIP (tydzień, miesiąc, trzy miesiące, rok) i cztery pakiety walut,
z czasem, StarCoins i diamentami. Lokalnie nic nie kosztuje i nie da się
nic kupić; kody z panelu są jedyną drogą. Żeby to okno w ogóle się
otworzyło, musiały zejść się cztery rzeczy:

- `PaymentModule` nie był wpisany w ustawieniach aplikacji, a
  `getModuleVersion` oddawał numer wersji zamiast nazwy pliku SWF-a —
  menedżery modułów budują adres wprost z tej odpowiedzi, więc moduł
  płatności nigdy się nie wczytywał. Teraz obie ścieżki czytają z tej
  samej mapy ustawień;
- `PurchaseFlow` musi mieć wartość **2** (`USE_OLD_VIP`). Nowa ścieżka (1)
  idzie przez `paymentmoduleexternal` i katalog cen z prawdziwego backendu,
  a przy każdej innej wartości `purchaseFlowSwitchFetched` nie robi nic;
- `GetCurrentPaymentPossibilities` musi oddać `Data` jako kolekcję,
  a każdą ofertę z aliasem `MovieStarPlanet.WebService.PurchaseTypeDomain` —
  klient robi na surowej odpowiedzi `Data.source` i rzutuje pozycje na tę
  klasę, więc bez aliasu dostaje null i callback ginie;
- każdy pasek oferty czeka na `GetBokuPricePoints`, zanim uzna się za
  wczytany. Oddajemy pustą walutę i zerowe ceny — bez odpowiedzi okno
  zostaje puste.

`PurchaseFlow = 2` psuje jednak nagłówek okna z gratulacjami (klient
podmienia go wtedy na ogólne „Gratulacje! Otrzymałeś”), dlatego
`tools/client-patches/PaymentSuccessHeadline.pcode` wycina z tego warunku
człon o `PurchaseFlow`. Pakiety samych walut dalej dostają swój nagłówek.
Ta łatka musi być pełnym blokiem metody (`method … param int … end ; method`):
wcześniej był w niej sam kod i FFDec zapisał metodę bez parametru. `loaderComplete`
woła `getResourceId(vipTier)` z jednym argumentem, więc natywny Flash rzucał
`ArgumentError #1063` i okno „Gratulacje!” w ogóle się nie pokazywało (Ruffle
nadmiarowe argumenty ignoruje). To samo dotyczy każdej łatki P-code na metodę
z parametrami — plik bez nagłówka gubi parametry.
Oryginał modułu leży w `paymentmodule_original.swf`, a łatkę nakłada
`tools/build-client.ps1`.

### Baner z diamentem i znaczek VIP w rogu ekranu

Obok postaci gracza (pod świnką) siedzi baner reklamowy: gdy VIP jest
aktywny, pokazuje diamenty ze StarCoinami („Diamond”), a bez VIP-a — sam
znaczek VIP. Kliknięcie w niego (i w kilka innych miejsc: „mam kod” przy
prezencie, okno specjalnej oferty, „Podnieś VIP-a”) szło przez zupełnie
inny menedżer modułów niż przycisk VIP w mini profilu —
`PaymentModuleExternalManager`, moduł `PaymentModuleExternal`. Tego
ustawienia aplikacji w ogóle nie było, więc `AbstractModuleManager.loadModule`
dostawał z `GetModuleVersion` nasze domyślne „local-1” (bez `?v=`), liczył
`slice(0, "local-1".indexOf("?v="))`, czyli `slice(0, -1)` — ucinał ostatni
znak adresu modułu i ładował go spod urwanego adresu. Menedżer więc cicho
padał, zanim cokolwiek się pokazało: baner i znaczek nie otwierały nic.

Naprawiono to dwoma krokami:

- ustawienie `PaymentModuleExternal` wskazuje teraz na `paymentmoduleexternal.swf`
  (oryginał z 2016), więc `loadModule` przynajmniej nie pada — ale sam ten
  moduł to prawdziwe płatności (Boku, karta) z prawdziwego backendu, którego
  nie mamy, więc **realnie nic ciekawego by nie pokazał**;
- łatka `tools/client-patches/PaymentModuleExternalManager.as` zamienia cztery
  wejścia (`openPurchaseVip`, `openPurchaseGiftCertificate`,
  `openSpecialOfferView`, `openUpgradeVip`) tak, żeby zamiast ładować ten
  moduł, otwierały to samo okno pakietów co przycisk VIP w mini profilu
  (`PaymentManager.openOffersView` — jedyna ścieżka zakupowa, która działa
  lokalnie, opisana wyżej).

## Looki

„Miasto Filmów” → **Looks** otwiera przeglądarkę looków, a zapisać looka można
z przymierzalni. Zapis idzie **dwoma wywołaniami**: `SaveLookWithClothes` oddaje
`lookId`, a klient dopiero tym numerem dosyła sam wygląd przez `SaveLookData`.
Bez numeru w odpowiedzi drugie wywołanie nie ma czego użyć i look zostaje pusty.

Metadane — kto zrobił, na kim, ile polubień — leżą w `users.json` pod kluczem
`looks`. Wygląd (`LookData`) trafia osobno do `local_server/data/blobs/look/`,
bo klient **nie czyta go przez AMF**, tylko zwykłym GET-em pod adresem, który
skleja sobie `BlobLoader`:

```
<SnapshotServerUrl>MSP_<kraj>_blob_<typ>data_<id rozbity na trójki>
http://localhost:1600/MSP_pl_blob_lookdata_000_000_000_007
```

Bez ustawienia `SnapshotServerUrl` prefiks wychodził dosłownie „null” i nic się
nie wczytywało — to ten sam mechanizm, który wcześniej wywalał odtwarzanie
filmów. Adres obsługuje `blob-local.php` przez regułę w `.htaccess`.

Cztery listy klienta różnią się tylko tym, czyje looki pokazują, i wszystkie
dostają `[actorId, viewerId, orderBy, pageIndex, pageSize]`:
`GetLooksForActor` (założone na tej postaci), `GetLooksCreatedBy` (zrobione
przez nią), `GetLooksByOthers` (cudze na niej) i `GetLooksForOthers` (jej looki
na kimś innym). Sortowanie po `LIKES` albo po dacie.

Look poprawia się tym samym numerem, kasuje go twórca albo osoba, na której
siedzi, a polubienie liczy się raz na osobę i dokłada sławę do gazetki twórcy —
tak samo jak przy pokoju. Kasowanie looka usuwa też jego blob.

Numery looków zaczynają się od **100001**, i to nie jest kaprys: klient rysuje
kafelek z zapisanej miniaturki dopiero powyżej `MyLooks.usingSnapShotsSinceId`,
czyli 100000. Niżej próbuje złożyć postać na żywo z `LookData` i miniaturka nie
pojawia się ani na liście, ani w szafie, ani na profilu.

Samą miniaturkę klient wysyła sam zaraz po zapisie (`SaveSnapshot` dla typów
`look` i `fullSizeLook`), a czyta ją spod adresu
`MSP_<kraj>_<typ>_<id rozbity na trójki>.jpg`. Uwaga: ustawienie
`SnapshotServerUrl` przełącza na tę postać **wszystkie** miniaturki w grze —
także pokoi, filmów i postaci — więc reguła w `.htaccess` musi przepuszczać
dowolny typ, a nie tylko wybrane.

### Dlaczego kafelek looka pokazywał „MyLook Name”

Kafelek na liście looków trzymał teksty wprost z projektu — „MyLook Name”,
„Friend Name”, „3. okt 2010” — i nie miał miniaturki, chociaż ten sam look na
profilu wyświetlał się poprawnie (profil rysuje go innym komponentem).

Winna była data. `LookListItemComponent.Update()` zaczyna się od

```as
this.dateLabel.text = DateUtils.ToDateString(this._look.Created);
```

a `DateUtils.ToDateString` składał ją przez `mx.formatters.DateFormatter`
z wzorcem `EEE MMM D YYYY`. Skróty dni i miesięcy Flex bierze z pakietu
zasobów `SharedResources`, a ten siedzi **wyłącznie** w
`swf/locales/<locale>_resourcemodule.swf`:

```
main_local.swf              — brak klas mx.formatters.* i brak SharedResources
sdk/framework_4.6.0.23201.* — DateFormatter i DateBase są, pakietu nie ma
swf/locales/pl_pl_...swf    — pl_PL$SharedResources_properties, dayNamesShort
```

Klient nigdy o ten plik nie pyta (`grep -c "swf/locales" msp-local-access.log`
daje 0, a lista RSL-i w SWF-ie wymienia tylko `sdk/*.swz`). `dayNamesShort`
było więc `null`, `format()` wywalało się błędem typu i wyjątek ucinał
`Update()` na **pierwszej instrukcji** — stąd wszystkie napisy z projektu naraz
i brak miniaturki.

Łatka `tools/client-patches/DateUtilsToDateString.pcode` przepisuje
`ToDateString` na `flash.globalization.DateTimeFormatter` („pl-PL”, wzorzec
`d MMM yyyy`) — ten sam formatter, którego używa już `ToDateStringNoFlex`.
Nazwy miesięcy daje system, więc nic się nie doczytuje i nie ma wyścigu przy
starcie; `null` oddaje pusty napis zamiast wywalać wywołującego. Naprawia to
przy okazji wszystkich pozostałych wywołujących: komentarze pod lookiem i pod
profilem, wpisy na tablicy, kafelki filmów, log wymian i przegląd konkursu.

Dwie metody z tym samym wzorcem `EEE` zostały nietknięte, bo używają ich tylko
ekrany moderatora: `ToDateTimeString2` (log haseł) i `ToServerDateTimeString`
(historia logowań).

## ArtBooki: „Love it” i komentarze

**Polubienie.** Klient sam ocenia, czy oglądający już polubił ArtBooka:
`ScrapBlog.hasActorLikedIt()` to po prostu „lista `ScrapBlogActorLikes` jest
niepusta”, a przycisk „Loved” nie ma obsługi kliknięcia. Serwer musi więc
odesłać w `LoadScrapBlog` (drugi argument to oglądający) tylko polubienie
*oglądającego*, jako obiekt `ScrapBlogActorLike`. Dawniej szła tam lista
numerów wszystkich, którzy polubili, więc po pierwszym polubieniu przycisk
był zablokowany każdemu innemu. `LikeScrapBlog` odpowiada `{liked, fameEarned,
likes}` — dopiero po polu `liked` klient podbija licznik i zmienia ikonę — i
niczego nie przełącza: polubienie liczy się raz na osobę i nie da się go cofnąć.
Nie polubisz własnego ani cudzego prywatnego ArtBooka, a zablokowany i
blokujący nie mają do siebie dostępu. Każde nowe polubienie dokłada autorowi
10 Fame do Magazynu Fame (kategoria „Artbooki”), jak przy looku.

**Komentarze.** Okno komentarzy pod ArtBookiem woła `GetEntityComments`
(`scrapblog`), `CommentEntity` (typ 7) i `EntityCommentDelete`; wszystko trzyma
`scrapBlogComments` w stanie (w MySQL jeden komentarz to jeden wiersz, nowe
klucze stanu nie wymagają rejestracji). Numer dostaje się od `100000000`, bo
`EntityCommentDelete` nie mówi, pod czym komentarz leży, a numery zdjęć i klubów
zaczynają się od 1 — bez tego skasowanie komentarza ArtBooka mogło trafić w
zdjęcie. Komentować może każdy, kto widzi ArtBooka (prywatny — tylko autor) i nie
jest wyciszony ani zablokowany; pod jednym ArtBookiem zostaje 300 najnowszych.
Kasuje autor komentarza, autor ArtBooka albo ekipa. Licznik komentarzy na
kafelkach i pod ArtBookiem liczy się z tej listy (pole `commentsCount` w
zapisie ArtBooka nie jest już czytane), a zgłoszony komentarz niesie do panelu
treść i tytuł ArtBooka. Pasek pod ArtBookiem odświeża licznik komentarzy dopiero
po ponownym otwarciu — tak działa oryginalny klient.

Test: `tools/test-artbooks.php`.

## Wybór koloru

Kolorowanie ubrań (sklep, szafa, klinika piękna — wszędzie, gdzie dymek z
kolorami otwiera paletę) używa nowego okna z `swf/colorpicker/New_ColorPicker_web_simple.swf`,
obsługiwanego przez łatkę `tools/client-patches/ColorPickerView.as`:

- trzy zakładki: **Kolory** (312 odcieni: 24 barwy od najjaśniejszych do
  najciemniejszych, przygaszone i szarości), **Modne** (pastele, neony, odcienie
  skóry, włosów i klasyki mody) i **Klasyczne** (dawna paleta 228 kolorów w starym
  układzie);
- najechanie próbki od razu pokazuje kolor na ubraniu, jak dawniej;
- gracz bez VIP-a wybiera kolor jednym kliknięciem; panel po prawej zasłania
  reklama VIP (klik otwiera zakup VIP-a), a pole HEX tylko pokazuje kod próbki;
- VIP klika próbkę, żeby przenieść ją do panelu, i może ją zmienić: tęcza
  w lewym górnym kwadracie ustawia odcień, duży kwadrat — nasycenie i jasność,
  a w polu HEX można wpisać dowolny kod. **OK** albo Enter w polu HEX wybiera kolor;
  dwuklik w próbkę wybiera ją od razu. Okno pamięta ostatnią zakładkę i ostatni
  kolor VIP-a do końca sesji.

Napisy są w tłumaczeniach (`COLORPICKER_TAB_*`, `COLORPICKER_VIP_TEXT`). Font
napisu w reklamie VIP ma tylko znaki ASCII (i ä, æ, é, ı), więc tłumaczenia
`COLORPICKER_VIP_TEXT` są bez polskich liter. Gdy grafika się nie wczyta, okno
pokazuje dawną siatkę kolorów.

## Sklep z ubraniami

Panel → **Sklep** → **Ciuchy**. Pojedyncze ubranie dodajesz przyciskiem
**Dodaj ubranie**: wgrany plik czeka jako ukryty szkic, a w oknie obok podglądu
ustawiasz nazwę, kategorię, płeć, cenę i widoczność.

### Kategorie i miejsca na postaci

Kategorie panelu mają te same numery co w kliencie, bo z numeru kategorii gra
wylicza katalog pliku, a z numeru miejsca (slotu) — co czego nie da się nosić
naraz. Rzeczy o różnych miejscach zakładają się jedna na drugą, dokładnie jak
w MSP: okulary, kolczyki, ozdoba włosów, szalik, tatuaż, czapka i zwykły dodatek
(torebka, skrzydła) to osobne miejsca, więc postać może mieć je wszystkie razem.

| Kategoria w panelu | Numer | Miejsce | Katalog pliku |
| --- | --- | --- | --- |
| Włosy | 1 | 1 | `swf/hair` |
| Góra / bluzka | 2 | 2 | `swf/tops` |
| Spodnie / spódnica | 3 | 3 | `swf/bottoms` |
| Sukienka / komplet | 6 | 2 (jak góra) | `swf/tops` |
| Buty | 10 | 5 | `swf/footwear` |
| Czapka / kapelusz | 13 | 6 | `swf/headwear` |
| Okulary | 27 | 7 | `swf/accessories` |
| Kolczyki | 26 | 9 | `swf/accessories` |
| Ozdoba włosów | 29 | 11 | `swf/accessories` |
| Ozdoba włosów (druga) | 30 | 12 | `swf/accessories` |
| Szalik | 45 | 27 | `swf/accessories` |
| Tatuaż | 31 | 13 | `swf/accessories` |
| Dodatek (torebka, skrzydła…) | 25 | 8 | `swf/accessories` |

Wcześniej wszystkie dodatki miały numer 7, a sukienki 4. Numer 7 to w kliencie
koszulka z katalogu `swf/tops`, więc gra szukała dodatku w złym miejscu i nic nie
pokazywała; dla numeru 4 klient w ogóle nie zna katalogu i przerywa błędem.
Panel poprawia to sam: przy pierwszym wejściu na zakładkę Ciuchy zapisuje nowe
numery, a plik, który leży w złym katalogu (sukienki z `swf/dresses`), przenosi
tam, gdzie szuka go gra. Rodzaj dodatku zgadujemy z nazwy pliku (okulary,
kokarda, kolczyk, szalik, tatuaż); resztę można poprawić w oknie ubrania —
zmiana kategorii przenosi plik do właściwego katalogu.

### Masowe dodawanie

Przycisk **Masowe dodawanie** nad siatką otwiera okno, do którego przeciągasz od
razu tyle plików SWF, ile chcesz (można też kliknąć i zaznaczyć wiele w okienku
wyboru). U góry ustawiasz kategorię, płeć i do czterech cen — wszystkie wgrane
pliki biorą te ustawienia, a każdy dostaje losową cenę z podanych.

Nazwa powstaje z nazwy pliku tak samo jak przy nagrodach: bez `custom_123_`, bez
roku i inicjałów autora, bez końcówek płci (`academy_2023_hairg_fj` → „Academy
Hair”), każde słowo z wielkiej litery. Plik ląduje w katalogu kategorii pod nazwą
`custom_<numer>_<nazwa pliku>` i od razu trafia do sklepu — bez szkicu.

Pliki idą paczkami po kilka (najwyżej 6 plików albo 12 MB na żądanie), żeby nie
przekroczyć limitów wgrywania PHP na hostingu; w polu upuszczania widać licznik.
Plik o tej samej nazwie, który jest już w tej kategorii, jest pomijany, a nie
dublowany — okno mówi na końcu, ile doszło i ile pominięto.

W `swf/accessories` siedzi kilka kategorii naraz, więc rodzaj dodatku rozpoznajemy
z nazwy pliku (okulary, kolczyk, kokarda, szalik, tatuaż), a reszta dostaje
kategorię wybraną w oknie. Wszystko można potem poprawić w oknie ubrania.

Jeśli w katalogu kategorii leżą pliki wgrane wcześniej inaczej (np. FTP-em),
których nie ma w sklepie, okno pokazuje ich liczbę i przycisk **Dodaj też te** —
działa tak samo, tylko bez wgrywania. Jedno kliknięcie dodaje najwyżej
`BULK_CLOTHING_LIMIT` (400) pozycji, a pliki zastępcze projektu (`local_dummy`,
`local_v4_*`) są pomijane.

## Sklep z rzeczami

Panel → **Sklep** → **Rzeczy**. To przedmioty, które postać trzyma albo stawia
w pokoju: instrumenty, jedzenie, pojazdy, gadżety, meble, zwierzęta i rekwizyty.
W danych gry rzecz to zwykłe ubranie (Cloth) o numerze kategorii z zakresu
19–46 i miejscu nr 8, a jej plik leży w `swf/stuff`, więc kupuje się ją tak samo
jak ubranie. Osobny jest tylko sklep w grze (metody `GetItems*`) i ta zakładka.

| Kategoria w panelu | Numer | Katalog pliku | Zakładka sklepu w grze |
| --- | --- | --- | --- |
| Instrumenty | 22 | `swf/stuff/instruments` | 1 |
| Jedzenie | 21 | `swf/stuff/food` | 2 |
| Pojazdy | 19 | `swf/stuff/vehicles` | 3 |
| Gadżety | 23 | `swf/stuff/gadgets` | 4 |
| Meble | 33 | `swf/stuff/furniture` | 5 |
| Rekwizyty | 24 | `swf/stuff` | 6 |
| Rekwizyty animowane | 46 | `swf/stuff/animated` | 6 |
| Zwierzęta | 20 | `swf/stuff/animals` | 7 |

Wszystkie rzeczy zajmują to samo miejsce nr 8, więc postać trzyma jedną naraz —
tak samo jak w MSP. Kliknięcie kafelka otwiera okno rzeczy: nazwa, kategoria,
cena (StarCoiny albo diamenty), dostęp dla VIP-ów i ukrycie w sklepie. Zmiana
kategorii przenosi plik do jej katalogu.

### Masowe dodawanie rzeczy

Przycisk **Masowe dodawanie** nad siatką działa jak przy ciuchach — przeciągasz
pliki SWF — ale **kategorii się nie wybiera**: panel rozpoznaje rzecz sam.

1. Jeśli plik o tej nazwie leży już w `swf/stuff/<kategoria>`, bierze tamtą kategorię.
2. Inaczej liczy, do której kategorii najbardziej pasują słowa z nazwy — liczy się
   nie to, ile razy słowo występuje, tylko jak bardzo należy do jednej kategorii
   („sofa” jest tylko w meblach, a „blue” wszędzie, więc kolory i podobne
   wypełniacze są pomijane).
3. Na koniec sprawdza słownik słów kluczowych (guitar, pizza, car, sofa, dog…).
4. Bez żadnej wskazówki rzecz trafia do rekwizytów.

Okno pokazuje po wgraniu, ile rzeczy trafiło do której kategorii, a każdą można
poprawić w oknie rzeczy. Plik o nazwie, która już jest w sklepie, jest pomijany;
plik, który leży już rozpakowany na serwerze, nie jest kopiowany drugi raz —
dostaje tylko wpis w sklepie.

Jeśli w `swf/stuff` czekają pliki spoza sklepu (a w archiwum gry jest ich ponad
tysiąc), okno pokazuje ich liczbę z podziałem na kategorie i przycisk **Dodaj też
te** — jedno kliknięcie wstawia je do sklepu z losową ceną z podanych czterech,
najwyżej `BULK_CLOTHING_LIMIT` (400) naraz.

## Sklep z animacjami

Zakładka **Animacje** w panelu dodaje pozycje do sklepu z animacjami. Wybierz
animację z listy plików SWF leżących w `swf/animations` albo wgraj własny plik —
jego nazwa staje się nazwą klatki, którą odtwarza postać. Podaj nazwę widoczną
w sklepie, kategorię, walutę (StarCoiny albo diamenty), cenę i ewentualnie
zaznacz „Tylko dla VIP-ów”.

Dodana animacja pojawia się na półce sklepu w dziale Animacje: cena renderuje
się monetą albo diamentem, a pozycje tylko dla VIP-ów dostają plakietkę. Zakup
schodzi z właściwego portfela, blokuje się przy braku środków i przy braku VIP-a,
a kupioną animację można ustawić na swojej postaci.

### Hurtowy import

Przy kilkuset plikach dodawanie po jednym przez panel nie ma sensu, więc jest
`tools/import-animations.php`. Bierze z `swf/animations` wszystko, czego nie ma
jeszcze w katalogu sklepu, i sam układa wpis:

- **nazwa** powstaje z nazwy pliku: leci rok, inicjały autora z końcówki
  (`_mf`, `_dg`, `_lsz`) i znaczniki techniczne (`_anim`, `_noskin`), a sklejone
  słowa rozbija zachłanny podział po słowniku, więc `zombiewestern_2012_duel_mf`
  czyta się jako „Zombie Western Duel”. Numer wariantu zostaje, żeby `cards1`
  i `cards2` nie miały tej samej nazwy;
- **kategoria** po słowach kluczowych — walka, taniec, pozy, zabawa, a reszta
  do podstawowych;
- **cena** losowa 250–750 SC, mniej więcej co szósta pozycja za 4–10 diamentów,
  co piąta tylko dla VIP-ów. Losowanie jest zasiane nazwą pliku, więc powtórne
  uruchomienie nie przecenia tego, co już jest w sklepie.

Bez `--apply` skrypt tylko pokazuje, co by zrobił. Pomija animacje zaszyte
w kliencie i zamienniki podstawione przez `build-animation-stand-ins.ps1` —
sprzedawanie kopii stania nie miałoby sensu. Zapis idzie jednym
`importCustomAnimations`, czyli jedną blokadą pliku zamiast kilkuset.

Ikon nie trzeba dodawać w żadnym z tych trybów: kafelek sklepu rysuje się
na żywo, pokazując Twoją postać w danej animacji.

Ikon nie trzeba nigdzie dodawać. Oryginalny klient wczytywał do kafelków
gotowe pliki `swf/animations/f_<klatka>.png`, a bez takiego pliku kafelek
zostawał pusty i połykał kliknięcie. Picker animacji ma jednak wbudowany
drugi tryb, więc `main_local.swf` przestawia jego stałą `useBitmaps` na
`false`: każdy kafelek rysuje teraz na żywo Twoją postać w danej animacji.
Działa to dla wszystkich animacji, także tych bez pliku PNG.

Ukrycie animacji zdejmuje ją z półki, ale zostaje u osób, które ją kupiły.
Wbudowane animacje za diamenty działają jak wcześniej i nie ma ich na liście
panelu. Katalog leży w `local_server/data/custom-items.json`.

Na start jest jedna przykładowa pozycja: **Przywitanie** za 250 StarCoinów.

## Klinika piękna

Oczy, nos, usta i cienie do powiek. Katalog siedzi w `MspServer.php`
(`dragonBoneEyeCatalog`, `dragonBoneNoseCatalog`, `dragonBoneMouthCatalog`,
`eyeShadowCatalog`), a każdy wpis opisują — poza nazwą i ceną — dwa pola:

| pole | znaczenie |
| --- | --- |
| `skin` | dla kogo: `1` kobiety (domyślnie), `2` mężczyźni, `0` dla obu |
| `db` | `true` (domyślnie) zestaw DragonBones, `false` zwykły plik SWF |

Co jest w sklepie:

| kategoria | kobiety | mężczyźni | skąd pliki |
| --- | --- | --- | --- |
| oczy | 26 | 33 | `swf/dragonbone_faceparts/eyes/<nazwa>/` |
| nosy | 22 | 5 | damskie DragonBones, męskie `swf/faceparts/noses/<nazwa>.swf` |
| usta | 20 | 6 | jak wyżej, `swf/faceparts/mouths/` |
| cienie | 66 | — | `swf/dragonbone_faceparts/eyeshadow/<nazwa>/` |

Płeć sprawdzają dwie strony. Klient sam filtruje listę po `SkinId`
(`BeautyClinicModel.getGenderSpecificItems`), a serwer przy zakupie odrzuca
niepasującą część z `WRONG_GENDER` — teraz osobno dla każdej wybranej rzeczy,
a nie hurtem dla całego męskiego konta jak wcześniej.

**Męskie oczy tylko z DragonBones.** W tym samym filtrze klienta wyjątek od
wymogu DragonBones dostają wyłącznie nosy i usta:

```as
if(!((Boolean(_loc3_.DragonBone) || _loc3_ is Nose || _loc3_ is Mouth) && ...))
   continue;
```

Zwykłe SWF-y z oczami (`swf/faceparts/eyes/male_*.swf` i podobne) wypadają
z listy zanim cokolwiek się narysuje, więc do kliniki idą tylko zestawy
z `swf/dragonbone_faceparts/eyes`. Nosy i usta mogą być zwykłymi SWF-ami i tak
właśnie robiło to MSP — moduł sklepu ma wpisane kolory dokładnie dla tych
nazw: `male_mouth_1`, `male_mouth_2`, `male_mouth_3_2009`, `male_mouth_4`,
`male_mouth_beard`, `poolparty_2011_mouthexpressions4`.

### Lusterko: raz kupiona rzecz wraca za darmo

W oryginalnym MSP każda wizyta w klinice kosztuje od nowa, nawet powrót do
kształtu sprzed chwili — tu nie. Konto trzyma listy tego, co kiedykolwiek
miało założone (`ownedEyeIds`, `ownedNoseIds`, `ownedMouthIds`,
`ownedEyeShadowIds`, `ownedSkinColors` w `MspAccountState::normalize`, razem
z dawnym kontem po prostu jako „to, co ma teraz założone”, więc stare konta
nic nie muszą dokupować) i klinika liczy cenę tylko za to, czego jeszcze nie
było — tak jak szafa z ubraniami, a nie sklep. Dotyczy to kształtu oczu, nosa,
ust, cienia do powiek i odcienia skóry; kolory (już wcześniej darmowe) bez
zmian.

`LoadDataForBeautyClinic` woła klient bez biletu sesji (`BeautyClinicService`
zostawia `addTicketHeader=false`), więc po stronie serwera jedynym sposobem
poznania konta jest — tak jak w kilku innych anonimowych wywołaniach w tym
pliku — ostatnio zalogowane konto (`beautyClinicOwnedFaceParts()`). Katalog,
który klinika pokazuje temu graczowi, ma więc ceny z jego punktu widzenia:
rzecz, którą już posiada, wraca jako `Price = 0` — i tak samo pokazuje ją
kafelek w kliencie (fioletowa plakietka „0” zamiast zwykłej ceny; `ShopPriceTag`
umie to narysować bez żadnej łatki klienta). Zakup też sam liczy `Price = 0`
dla takiej rzeczy, więc ponowny wybór przechodzi nawet przy zerowym koncie.
Odcień skóry serwer porównuje po wartości (`MspAccountState::skinColorKey`):
ten sam kolor bywa zapisany dziesiętnie („14654312”, tak wysyła go klient) albo
szesnastkowo („0xDF9B68”), a porównanie tekstów liczyło wtedy drugi raz 300.
To samo okno bez cen jest też w szafie jako lusterko (opis w „Status, mini
profil i szafa”).

Test: `tools/test-beauty-clinic-mirror.php` (osobny plik, bo
`tools/test-beauty-clinic.php` sprawdza jeszcze komplet assetów i kolorów
DragonBones całego katalogu, co akurat teraz ściga niezależny, wcześniejszy
błąd w paru wpisach `dragonBoneEyeColors`).

### Kolory oczu

Oczy z DragonBones klient maluje po kolei jednolitymi kolorami: pierwszy kolor
z listy na kości `eyeball1`, drugi na `eyeball2` itd., aż do pierwszego numeru,
którego szkielet nie ma (przód i bok osobno). Numery nie znaczą wszędzie tego
samego — w starych oczach 1 to tęczówka, a 2 brwi, ale w Cutie, Mega Anime,
Easter czy Dual Eyes 3 to rzęsy, a w Chain, Diamond, Dollz, Anime i Rare brwi.
Serwer dawał wszystkim oczom te same `DefaultColors`
(`26163,0x000000,skincolor`), więc rzęsy albo brwi malowały się kolorem skóry,
a czwarty i piąty kolor w ogóle nie dochodził.

Teraz każde oko ma własną listę (`dragonBoneEyeColors` w `MspServer.php`):
dla każdej kości barwę, która zajmuje najwięcej miejsca w jej grafice
w `texture.dbswf`. Brwi i rzęsy są więc czarne albo ciemnobrązowe, a tam, gdzie
projektant nadał im kolor (Candyland, Cutie), zostaje jego wybór. Dla nowych
oczu listę wylicza `php tools\facepart-default-colors.php eyes` (Java + FFDec).

Zapisane kolory kont naprawia `eyeColorString` przy odczycie, zakupie
i zakładaniu konta. Trzecia pozycja dawnych kolorów bierze domyślny kolor
oka — także wtedy, gdy klient zapisał ją już jako liczbę koloru skóry (tak
robi `MovieStar.LoadFacePart`, dlatego rzęsy nie zmieniały się razem ze
skórą). Brakujące pozycje dostają kolory domyślne, a kolory wybrane przez
gracza w klinice zostają.

### Kolory ust

Usta z DragonBones maluje się tak samo, tylko kodem `color`: `color1` to kolor
skóry (klient dokleja go sam), a `color2`, `color3`… to części do pomalowania.
Klinika pokazuje tyle kolorów do wyboru, ile wpisów ma `DefaultColors`, a serwer
dawał wszystkim ustom jeden (`13382502`). W Classic Lips 4 `color2` to kolczyk,
a `color3` usta, w Heart Lips `color3` to serduszko — do tego aparat (Braces),
grille, usta w paski (Candyland), dwukolorowe i Dress Up. Zmieniał się więc
tylko pierwszy kolor.

Teraz każde usta mają własną listę (`dragonBoneMouthColors` w `MspServer.php`,
wylicza ją `php tools\facepart-default-colors.php mouths`): po jednym kolorze na
każdą kość od `color2`, wzięty z grafiki. Kość pasuje po początku nazwy, więc
`color20` (kółko kolczyka) maluje się kolorem numer 2, jak w kliencie. Usta
ze zwykłych SWF-ów (męskie i dawne z kreatora) dostają listę ze swojego
`colorscheme` — Male Mouth 4 ma usta i kolczyk, Zarost kolor brody.

Zapisane kolory kont uzupełnia `mouthColorString` przy odczycie, zakupie
i zakładaniu konta: brakujące pozycje dostają kolor domyślny, a kolory wybrane
przez gracza zostają, więc usta kupione wcześniej wyglądają tak samo, tylko da
się już zmienić każdą ich część.

### Cienie do powiek

Klient dostaje je osobną listą (`eyeShadows`) i ładuje spod `eyeshadow/<nazwa>`.
Konto trzyma `eyeShadowId` i `eyeShadowColors` w `users.json`, a `LoadMovieStarFlat`
oddaje dodatkowo **cały obiekt** `Actor.EyeShadow` — `MovieStar.loadActorEyeShadow`
czyta go zamiast numeru i bez niego po prostu zdejmuje cień. Zdjęcie cienia
klient wysyła osobną flagą (`removeEyeShadow`), bez numeru i za darmo.

**Suma kontrolna postaci.** Odpowiedź `LoadMovieStarFlat` niesie `Seq` — md5
z numeru i imienia postaci oraz numerów nosa, ust, oczu, **cienia** i ubrań
(`MovieStarService.createHashString`). Klient liczy ją sam i przy niezgodności
odrzuca postać: po zakupie pierwszego cienia serwer liczył sumę bez niego, więc
logowanie stawało na „Logging out…” (i okienku o nieaktualnej wersji), a profil
się nie otwierał. Teraz sumę składa `movieStarHashString()` dokładnie tak jak
klient.

**Kolory cienia.** Klinika maluje `color1` na czarno (kreska przy oku) i zapisuje
`0x000000,` + kolory z suwaków, a suwaków daje tyle, ile wpisów ma
`DefaultColors`. Serwer liczy je z kości szkieletu — po jednym na `color2`,
`color3`… — i uzupełnia puste wpisy kolorem domyślnym (tak naprawił się też
zapis `0x000000,` z pierwszych zakupów).

### Pliki .jsonc

Część paczek DragonBones przychodzi tylko jako `skeleton.jsonc` i
`texture.jsonc`. To nie jest JSON z komentarzami, tylko **spakowany zlibem
obiekt AMF3**. Klient umie to czytać, ale dopiero po włączeniu ustawienia
`usejsonc`, które przestawia na ten wariant *wszystkie* zestawy — a te już
wgrane mają wyłącznie zwykłe `.json`. Zamiast przestawiać cały klient,
dokładamy brakujące `.json` obok:

```bash
php tools\expand-dragonbone-jsonc.php           # podgląd
php tools\expand-dragonbone-jsonc.php --apply   # zapis
```

Zgodność sprawdzona na dwóch zestawach, które przyszły w obu wariantach
(`noses/ess_2023_noseg_mmj` i `eyeshadow/eyeshadow_female_freckles_2020_mmj`):
rozpakowany `.jsonc` wychodzi identyczny z dołączonym `.json`.

Test `tools/test-beauty-clinic.php` sprawdza nie tylko ceny i płeć, ale też
czy każda klatka wołana przez szkielet naprawdę jest w `texture.dbswf` —
brakująca zostawia dziurę na twarzy.

## Nagrody (Awards)

Profil → **Awards**. Każda nagroda ma cztery etapy — brąz, srebro, złoto,
diament — i liczy **swój własny licznik**. Wcześniej wszystkie brały etap
z poziomu konta (`ceil(poziom/25)`), więc konto na poziomie 101 miało od ręki
diamenty wszędzie, a dni VIP-a, zakupy czy obejrzane filmy nie zmieniały nic.

Katalog siedzi w `achievementCatalog()` w `MspServer.php`: nazwa, zakładka,
ikona, cztery progi i sława za każdy etap. Opisy wymagań („Zostań VIP na {0}
dni”) klient bierze z własnych tłumaczeń, a wartość `{0}` — z serwera.

| nagroda | co liczy | progi |
| --- | --- | --- |
| VIP Star | dni VIP-a (`totalVipDays` — z kodów i z panelu) | 7 / 30 / 180 / 700 |
| MSP Veteran | wiek konta w miesiącach | 1 / 6 / 12 / 24 |
| Follower | najdłuższa seria logowań dzień po dniu | 3 / 7 / 30 / 100 |
| Movie Critic | ocenione cudze filmy | 3 / 50 / 100 / 250 |
| Blockbuster Director | unikalne wyświetlenia najlepszego filmu | 3 / 10 / 25 / 50 |
| Lucky You | StarCoiny wygrane w kołach fortuny | 250 / 1 500 / 7 500 / 25 000 |
| Celebrity / Autograph Hunter | autografy rozdane / otrzymane | 10 / 50 / 200 / 1 000 |
| Jetsetter | StarCoiny wydane w sklepach | 5 000 / 25 000 / 100 000 / 500 000 |
| Big Spender | wydane diamenty | 10 / 50 / 200 / 3 000 |
| Trendy Topper, Fancy Pants, Tap Dancer, Trendsetter | kupione topy, spodnie, buty, fryzury | 5 / 15 / 30 / 45–60 |
| Accessorizer | kupione dodatki (licznik zakupów, w sklepie jest ich siedem — diament wymaga przetwarzania i odkupywania) | 1 / 3 / 5 / 50 |
| High Roller | rzeczy za 500 SC i więcej | 5 / 20 / 50 / 100 |
| Broadway Star / Crazy Collector | kupione animacje / wszystkie zakupy | 5–10 / … / 300–400 |
| Gossip Star (Status Diamond) | wysłane statusy Diamond (`diamondStatusesSent`) | 1 / 10 / 50 / 100 |
| Imprezowicz (Party Starter) | liczba znajomych | 5 / 15 / 30 / 500 |
| Specjalne Pozdrowienie (Special Greeter) | wysłane specjalne pozdrowienia | 1 / 5 / 15 / 500 |
| Mistrz Zwierząt (Pet Master) | zwierzaki wychowane do ich własnego maksymalnego etapu | 3 / 10 / 15 / 25 |

Pozostałe liczą to, co mówi ich opis: polubienia pokoju, artbooka i looka,
looki zrobione innym, polubione pokoje znajomych, prezenty (także te
spełniające życzenie z listy), logowanie i prezent w Boże Narodzenie.

**Ręczna korekta progów (2026-09-28):** kilka nagród było za łatwo wbić, więc
VIP Star, Movie Critic, Big Spender, Accessorizer, Gossip Star, Party Starter,
Special Greeter i Pet Master dostały wyższe progi (tabela wyżej — poza Pet
Masterem, wszystkie inne etapy tych nagród zostały bez zmian, zmienił się
tylko wymieniony próg). Gossip Star dostał też własną, mniejszą sławę za złoto
i diament (15 000 i 50 000 zamiast wspólnych 50 000 i 500 000 z resztą
„trudnych” nagród — `$statusFame` w `achievementCatalog()`), żeby przy niższym
progu (100 zamiast 200) diament nie dawał tyle samo Fame co wcześniej.

**Pet Master liczy teraz co innego.** Zwierzaki w tej wersji mają etap wzrostu
0–4 (jajko → dorosły, `MspPetCatalog::maxStage`), a nie poziom — żaden zwierzak
nie dojdzie do 25. Opis w tłumaczeniach klienta („Osiągnij {0} level Zwierzaka
lub Boonie”) obiecuje poziom, bo jest żywcem wzięty z prawdziwego MSP i nigdy
nie pasował do tego uproszczonego systemu — stary licznik brał najwyższy etap
JEDNEGO zwierzaka (0–4), więc diament (dawniej 4) był od ręki, gdy tylko
jakikolwiek zwierzak dorósł. Nowy licznik zlicza, ILE RÓŻNYCH zwierzaków
wychowałeś do ICH WŁASNEGO maksymalnego etapu — dzięki temu progi 10/15/25
w ogóle da się zdobyć, kosztem posiadania (i wychowania) odpowiednio wielu
zwierzaków naraz (limit konta to 50, `MspPetCatalog::MAX_OWNED`).

Większość wartości wynika wprost ze stanu, więc liczy się **wstecz** — dni
VIP-a, filmy, prezenty czy szafa sprzed tej zmiany od razu dają etapy. To,
czego nie da się odtworzyć (wydatki, wygrane w kole, seria logowań,
pozdrowienia), liczą nowe liczniki w `stats` konta. Przy zakupach bierzemy
większą z dwóch liczb: licznik albo to, co leży w szafie.

Sława za etap idzie od 125 do 500 000, jak na MSP — przy progach poziomów
z MSP diament trudnej nagrody daje na poziomie około 15 jeden–dwa poziomy.
Fame Booster ją podwaja. Etap odbiera się po
kolei; odebrany zostaje, nawet gdy licznik potem spadnie. Liczba na ikonie
zakładki to suma etapów do odebrania na kafelkach w środku.

**Ukryte:** trzynaście nagród, których lokalnie nie da się zdobyć, bo nie ma
tego, co liczą — gry (Gamer), kluby (Club Master), YouTube (Master DJ),
projektowanie (Top Designer, Fashionista), zaproszenia (MSP Guide), aplikacja
mobilna (Smart Star), kalendarz świąteczny (Christmas Collector), rzeczy VIP
w pokoju, ukryte krasnale, „Ostatnia szansa”, kupowanie teł i świąteczne tła.
Mają w katalogu `false` na końcu wiersza — wystarczy je zdjąć, gdy funkcja
powstanie.

**Ikony** ładują się po nazwie z `swf/profile/Icons_Achievements.swf`. Jest ich
33 na 45 nagród, więc część się powtarza, ale nigdy w obrębie jednej zakładki.
Dawne `LoveIcon` nie istniało i zostawiało pusty kafelek.

## Pixi Star

Pixi Star to postać „anchor” wpisana w klienta na sztywno —
`AnchorCharacters.FEMALE_ANCHOR = 3`, `getNameForId(3) = "Pixi Star"` — a nie
konto gracza. Klient ma dla niej osobny wiersz na liście znajomych z zawsze
zapaloną kropką online (`AnchorCharacterSub`), złotą plakietkę z imieniem
w pasku aktywności i własne okienko postaci bez poziomu, zgłaszania i odznaki
VIP (`AnchorCharacterPopup`). Serwer daje jej to, czego klient oczekuje:

- `GetAnchorCharacterList` (serwis `AMFAnchorCharacterService`) zwraca jej
  `AnchorCharacterInfoVO` z `IsOnline = true`. Bez tej listy klient nie wie,
  że jest online, a jej okienko wywala się na pustym opisie. Ten serwis ma
  własne `RequestFriendship` z innymi argumentami niż zwykli znajomi, więc
  rozpoznajemy go po nazwie serwisu, a nie po samej metodzie;
- jest **znajomą każdego gracza** — dopisaną do każdej listy znajomych i do
  paska aktywności, bez zapisywania u graczy, więc nie da się jej usunąć,
  a nowe konta mają ją od razu;
- jest **zawsze online**;
- **status zmienia się co ustalony czas**, kolejno według listy z panelu
  (zakładka **Pixi Star**, plik `local_server/data/anchors.json`). Kolejność
  wynika z zegara, więc nie trzeba harmonogramu i wszyscy widzą to samo.
  Zmiana okna przesuwa `statusUpdatedAt` w pasku aktywności, a klient pokazuje
  wtedy nad Pixi dymek z nowym statusem. Domyślna lista to prawdziwe statusy
  Pixi z tłumaczeń gry (`ANCHOR_PIXI_WAYD_*`), zmiana co 60 minut.

**Pełny profil.** Część różnic względem zwykłego profilu jest zamierzona
przez klienta dla postaci specjalnych (`ProfileBook.hideTabsIfIsAnchorCharacterProfile`,
`MyPage`, `Wall.showInput`): nie ma zakładek nad książką, daty „Członek od”,
licznika wyświetleń ani pola księgi gości. Zepsute było co innego:

- **„Odwiedź Pokój” nic nie robiło** — dla postaci specjalnych klient szedł
  przez `RoomRequester.requestRoom`, czyli połączenie RTMP z serwerem pokoi,
  którego lokalnie nie ma. Łatka `PlayerRoomLocalEnter.pcode` wyłącza tę gałąź
  i jej pokój otwiera się jak pokój gracza;
- **szara ramka „Mój Pokój”** — Pixi nie zapisuje zdjęcia pokoju, a zapasowego
  `img/wallpapers/dummy_room.jpg` nie było. `snapshot-local.php` oddaje teraz
  pusty pokój z domyślną tapetą i podłogą każdemu pokojowi bez zdjęcia;
- **pusta „Aktywność”** — na jej ścianie są jej statusy z kolejnych okien,
  najnowszy na górze;
- **krzywe ikonki nad ścianą** — patrz niżej.

**Ikonki filtrów nad ścianą.** W `swf/profile/wall_masked.swf` stoją
w jednym rzędzie: Wszystko, Projekty, Wpisy, Zdjęcia, Filmy, ArtBooki, Looki,
YouTube. Projektów i zdjęć lokalnie nie ma, więc te dwie są ukryte, a postać
specjalna nie ma też wpisów. Oryginalny kod zamyka luki, przesuwając
przyciski o stałe odległości dobrane do pełnego rzędu, a wcześniejsza lokalna
łatka rozstawiała je co 40 px od punktu zaczepienia, choć każda grafika ma
inny margines. Wychodziły nierówne odstępy, a u Pixi przycisk „Wszystko”
wjeżdżał na filmy. `Wall.as` składa teraz rząd od prawej, od YouTube,
w oryginalnej kolejności, z odstępem 6 px między krawędziami ikonek.

Klient pobiera moduły (`profilemodule_local.swf` i pozostałe SWF-y z katalogu
głównego) bez numeru wersji, więc po przebudowie przeglądarka potrafiła
godzinami podawać starą kopię z cache. `.htaccess` każe teraz sprawdzać je
przy każdym wczytaniu gry — bez zmian serwer odpowiada krótkim 304. Moduł
wczytany raz zostaje w pamięci do końca sesji, więc po przebudowie trzeba
przeładować stronę gry.

Małe ikonki w rogach ramek „Ulubione” to co innego: tak stoją w
`myprofile_book.swf` i klient pokazuje je na cudzym profilu jako znaczek
rodzaju ulubionego.

Jej profil leży w `users.json` pod kluczem `#anchor-3` — znaku `#` nie ma
w imionach, więc nikt go nie zajmie rejestracją. Nie ma hasła, nie ma jej na
liście kont w panelu ani w rankingach, a panel nie pozwala edytować jej jak
konta. Wygląd przy pierwszym uruchomieniu wziął się ze zwykłego konta
o imieniu „Pixi Star”. W zakładce **Pixi Star** można go w każdej chwili
skopiować z innego konta — ubierz dowolne konto w grze i przenieś wygląd.

## Strona główna

Na stronie startowej, przed zalogowaniem, w lewym dolnym rogu stoi MovieStar
i co 3 sekundy robi losową animację, a po kliknięciu od razu kolejną — tak jak
na MSP. Klient ma to wbudowane (`FrontPage2.loadCornerMoviestar`): pyta
serwer o look (`GetRandomLookByLikes`), dociąga jego wygląd z magazynu blobów
i ubiera w niego postać. Lokalnie nikt się tam nie pojawiał z dwóch powodów:

- serwer oddawał `LookItem` z list looków, a klient rzutuje odpowiedź na
  `Look` — rzutowanie rzucało błąd i postać nie powstawała;
- przed zalogowaniem klient dokleja do zapytań bilet z poprzedniej sesji,
  a serwer odrzucał go jako „Sesja wygasła”. `GetRandomLookByLikes`
  i `LoadMovieStarFlat` działają też bez biletu (to publiczne dane), więc
  nieważny bilet traktujemy przy nich jak brak biletu.

Kto tam stoi, ustawia się w panelu w zakładce **Strona główna**
(`local_server/data/frontpage.json`):

- **wybrana MovieStar** — dowolne konto albo Pixi Star (domyślnie), w tym,
  co ma na sobie w grze. Serwer oddaje wtedy zarezerwowany look nr 1,
  a `blob-local.php` pusty wygląd dla niego, więc klient ubiera postać w jej
  aktualny strój;
- **losowy look spośród 10 najczęściej lubianych** — tak robił MSP. Bez
  looków w grze stoi wybrana postać.

Listę animacji klient miał wpisaną na sztywno, a trzech z dwunastu plików
lokalnie nie ma. Łatka `FrontPageRandomAnimation.pcode` bierze listę
z ustawienia `FrontpageAnimations`, które serwer dokleja do każdej odpowiedzi
`GetAppSettings` (klient o nie nie pyta, ale zapisuje każde ustawienie, które
wróci). W panelu lista to nazwy plików z `swf/animations`, po jednej w linii;
domyślna to lista z MSP z odpowiednikami brakujących animacji.

### Lista krajów, czyli język

Pola z flagą na stronie startowej (nad „Graj teraz” i przy logowaniu)
przenosiły na prawdziwą stronę MSP wybranego kraju, np.
`www.moviestarplanet.co.uk`. Lokalnie kraj — a z nim język — bierze się
z parametru `debugcountry` w adresie `index-local.html`, więc łatka
`FrontPageCountryLocal.pcode` przeładowuje tę samą stronę z kodem wybranego
kraju (`?debugcountry=uk&debugremote=true`). Adres jest względny, więc zostaje
ten sam host. Każdy kraj z listy ma tłumaczenie w `translations/mspweb`:
UK, Irlandia, Australia, Nowa Zelandia i Kanada korzystają z `en_us`,
a Hiszpania ma, jak na MSP, logo MyStarPlanet.

`index-local.html` zapamiętuje ostatni kraj w pamięci przeglądarki. Adres
z `debugcountry` zawsze wygrywa, a strona otwarta bez niego (np. przyciskiem
„Otwórz grę” w panelu) wraca do ostatniego wyboru.

### Wylogowanie

Po „Wyloguj” gra pokazuje „Logging out…” i przechodzi na stronę główną
(`BrowseToMovieStarPlanetCommand`) pod adres `Config.basePath`. Dla gry
uruchomionej z localhost ten adres to pusty napis, więc przeglądarka nic nie
robiła i gra wisiała do ręcznego odświeżenia. Łatka
`BrowseToLocalFrontPage.pcode` wraca na `index-local.html` — w ostatnio
wybranym języku. Tej samej komendy używa „Anuluj” w rejestracji nowej
postaci, więc tam też działa powrót.

## Kreator postaci

Kreator („Graj teraz”) nie ma strzałek do przewijania — każda zakładka to
jedna strona 3×3. Serwer (`creatorFaceIds` i `creatorClothes`
w `MspServer.php`) wysyła więc tylko tyle, ile się mieści, osobno dla
dziewczyny i chłopaka:

- **twarz** — rząd oczu, pod nim rząd nosów i rząd ust, po 3. Dziewczyny mają
  oczy Giselle, MovieStar Eyes i Glamour Eyes, trzy małe nosy z DragonBones
  i usta Dress Up Lips, Classic Lips 2 i Classic Lips 1. Chłopcy — oczy
  2000s Camp, Ronaldo i The Man, nosy `nose_7`, `nose_8` i `nose_2` oraz
  zwykłe męskie usta 1–3;
- **włosy, góry, doły i buty** — po 9 rzeczy ze sklepu, wybranych ręcznie po
  obejrzeniu każdej. Chłopak ma 7 par butów, bo tyle nadaje się ze sklepu.

Nic z tego nie jest kostiumem: bez kocich wąsów, kolczyków, piegów, grilli na
zębach czy kopyt. Rzecz, która zniknie ze sklepu albo zostanie ukryta, po
prostu wypada z kreatora. Kategoria, z której nic nie zostało (np. świeża
instalacja bez `custom-items.json`), dostaje dawny zestaw startowy.

Postać „rozjeżdżała się”, bo część wgranych plików ma inny rodzaj niż
kategoria w sklepie (sandały zapisane jako włosy, spódnica jako włosy, okulary
jako bluzka) i założona w kreatorze rzecz stawała w złym miejscu ciała. Serwer
czyta nazwy części z pierwszej klatki pliku (`FrontHead` → włosy, `…Foot` →
buty, biodra i nogi → doły, ręce → góry) i odrzuca niepasujące. Wynik trzyma
w `local_server/data/cloth-kinds.json`, dopóki plik się nie zmieni.

Łatki klienta (wszystkie w `tools/build-client.ps1`):

- `LoadFacePartItems.pcode` i `UpdateSelectedItem.pcode` — kreator pokazuje
  i zakłada z DragonBones także nosy i usta, nie tylko oczy;
- `CreatorFacePreviewPlace.pcode` — podglądy z DragonBones wczytują się
  w dowolnej kolejności i dopisywały na koniec listy, więc rzędy się mieszały.
  Teraz kafelek trafia na swoje miejsce: strona·9 + rząd·3 + kolumna i jest
  wyśrodkowany według swoich granic (wcześniej nosy uciekały w lewo, a usta
  w prawo). Podgląd klient robił funkcją dla oczu (`getFrontEyesMovieClip`),
  więc usta pokazywały kilka kształtów warg naraz i bez kolorów. Nos i usta
  wczytują się więc drugi raz tak jak na postaci: kolory skóry i ust pod kod
  `color` i mina `MouthNeutral`;
- `UpdateSelectedItem.pcode` bierze kolor ust wybrany w palecie — wcześniej
  usta zawsze dostawały kolor domyślny, więc zmiana koloru nic nie dawała;
- `CreatorRandomNose.pcode` i `CreatorRandomMouth.pcode` — losowa postać
  zakładała nos i usta zawsze jak zwykły SWF, więc części z DragonBones znikały;
- `EmptyActorGirlMouth.pcode` — pusta postać pod podglądami miała usta
  w kolorze `0xff0000`, a usta z DragonBones malowały nim skórę wokół warg
  (czerwona plama na ekranie wyboru płci).

Postać w kreatorze co 5 sekund stoi albo gra losową animację z listy
zaszytej w kliencie: `Wave`, `talk`, `sing2`, `Handgesture1`–`3`, `I am Cool`.
`Wave`, `sing2` i `Handgesture3` były zaślepkami — nieruchomą pozą złożoną
pod dziewczynę (`LocalGirlRig`), więc co jakiś czas postać na chwilę
„rozjeżdżała się” (rękawy odstawały od ramion), a potem wracała. Teraz są to
prawdziwe animacje MSP, a `Sidewave` (zamiennik z
`tools/build-animation-stand-ins.ps1`) jest kopią nowego `Wave`. Zaślepki
leżą w `swf/animations_backup/`.

## Buty z nowszych wersji MSP

Skóry postaci pochodzą z 2016 roku i mają płaską stopę: z boku ok. 60 px długości.
Buty dziewczęce z nowszych wersji MSP (mniej więcej od 2018 roku: obcasy,
kozaki, platformy, sandały) rysowano pod stopę na palcach — z boku są krótsze
(ok. 50 px) i sięgają niżej. Pod nimi płaska stopa wystawała palcami przed but.

Łatka `BodyPartDescriptor.as` rozpoznaje taki but po bocznej części stopy
(kończy się przed 58 px i sięga co najmniej 34 px w dół) i dopasowuje do niego
stopę skóry. Z boku palce idą w dół (ścięcie), a stopa skraca się do długości
buta, z przodu zwęża się do jego szerokości. Po zdjęciu buta stopa wraca do
zwykłego kształtu. Starsze buty i buty męskie (dłuższe) zostają bez zmian.

## Zdjęcia

W Movie Town jest przycisk **Zdjęcia** (moduł `picturemodule_local.swf`).
Zakładki: **Nowe**, **Top** (najbardziej lubiane), **Moje**, **Znajomych**
i wyszukiwarka (nick autora albo tytuł). Aparat u góry wgrywa zdjęcie z dysku:
najpierw zasady (akceptacja zapisuje się na koncie), potem wybór pliku (JPG,
PNG albo GIF — łatka poszerza filtr okna, oryginał przyjmował tylko `*.jpg`),
tytuł do 20 znaków i widoczność: **Publiczne** albo **Tylko znajomi**. Klient
sam zmniejsza zdjęcie do 640 px i robi kwadratową miniaturkę 300 px.

Wgrywać można od poziomu 6 albo z VIP-em: 3 zdjęcia na 24 godziny, z VIP-em
10, a naraz na decyzję czeka najwyżej 10 zdjęć jednego gracza. Liczby są
stałymi `PHOTO_*` w `local_server/lib/MspServer.php` (klient dostaje te same
w ustawieniach).

**Moderacja.** Nowe zdjęcie czeka na ekipę: autor widzi je w „Moje” z nakładką
„czeka”, inni gracze go nie widzą. W zakładce **Zdjęcia** panelu są filtry
Czekające (najstarsze pierwsze) / Zatwierdzone / Odrzucone / Wszystkie,
powiększenie, autor z przejściem do profilu, polubienia, wyświetlenia
i komentarze. „Zatwierdź” wpuszcza zdjęcie do gry; „Odrzuć” (przy zatwierdzonym
„Usuń z gry”) kasuje plik, a powód zostaje przy zdjęciu i w historii gracza.
Administrator może też usunąć zdjęcie na zawsze. Autor dowiaduje się o decyzji
okienkiem z miniaturką — od razu, jeśli jest w grze, albo po najbliższym
zalogowaniu. Kliknięte odrzucone zdjęcie znika z „Moje” i pokazuje zasady
(`photo-rules-local.php`).

Zatwierdzone zdjęcia: polubienie raz na gracza (idzie do Magazynu Fame autora),
wyświetlenia liczone raz na gracza, komentarze (kasuje je autor komentarza,
autor zdjęcia albo ekipa w panelu), zgłaszanie zdjęć i komentarzy (dowody
w Zgłoszeniach) i ramka „ulubione zdjęcie” na profilu. Na pełnym profilu nad
„Aktywnością” jest ikonka aparatu, jak w MSP — między Wpisami a Filmami
(`tools/client-patches/Wall.as`); pokazuje zdjęcia właściciela profilu, które
oglądający może zobaczyć.

Adres modułu profilu jest wpisany w kliencie (`SessionAmfService`, mapa
`localModules`), a nie brany z ustawień serwera. Po każdej przebudowie
`profilemodule_local.swf` trzeba podbić tam `?v=` (i w `prepare-client.php`
oraz `GetAppSettings`) — inaczej gra wczytuje starą kopię modułu z pamięci
podręcznej.

Pliki leżą w `local_server/data/photos/` (`<guid>.jpg` i `<guid>-s.jpg`), wpisy
w stanie gry (`photos`). Gra pobiera je przez `photo-local.php` — `.htaccess`
kieruje tam adresy `MSP_<kraj>_pictureUpload_…`. Guid ma 32 losowe znaki, więc
czekające zdjęcie może pobrać tylko autor i ekipa.

## Przetwarzanie rzeczy

Profil → zakładka **Przetwarzanie**. Zakładki na górze dzielą szafę na góry,
doły, buty, włosy, dodatki, rzeczy i zwierzaki; pod każdą rzeczą widać, ile
punktów da jej oddanie.

Punkty to cena rzeczy podzielona przez sześć — dokładnie tyle, ile pokazuje
kafelek (`RECYCLE_PRICE_PER_POINT` w `local_server/lib/MspServer.php`). Za
zwierzaka jest tyle samo co za rzecz z ceną 500 SC. Po potwierdzeniu rzecz
znika z szafy od razu, a licznik pod listą pokazuje nową sumę punktów konta.

Nie da się oddać rzeczy, którą postać ma na sobie, ostatnich włosów, ostatniej
góry ani ostatniego dołu, a także rzeczy kupionych za diamenty — gra blokuje to
w okienku, a serwer sprawdza to drugi raz. Oddanie ostatniego zwierzaka jest
możliwe i konto zostaje bez zwierzaka: startowy Boonie nie wraca już po takim
przetworzeniu (konto ma znacznik `petsSeeded`).

Przy `RECYCLE_GIFT_POINTS` (1000) punktach przycisk pod listą zapala się.
Kliknięcie zabiera 1000 punktów i dokłada do szafy losową rzecz ze sklepu
pasującą do płci postaci, której konto jeszcze nie ma (rzeczy tylko dla VIP-ów
trafiają wyłącznie do kont z VIP-em, a rzeczy za diamenty nie biorą udziału
w losowaniu). Gra pokazuje ją w okienku z podziękowaniem.

W losowaniu biorą udział tylko rzeczy, których plik leży tam, gdzie szuka go gra.
Klient wylicza katalog z numeru kategorii (`ClothingCategories.GetCategorySubPath`):
kategoria 7, czyli „Dodatek” z panelu, prowadzi do `swf/tops/`, a dla kategorii 4
(„Sukienka / komplet”) klient nie ma katalogu i zgłasza błąd — panel zapisuje te
pliki w `swf/accessories/` i `swf/dresses/`, więc do nagrody nie trafiają.
Dodatki ze Sklepu diamentowego mają kategorię 27 i ładują się poprawnie.

Punkty siedzą w kolumnie `recyclePoints` tabeli kont — dopisuje się sama przy
pierwszym żądaniu po wgraniu poprawki. Logika: `local_server/lib/Recycling.php`.

Wcześniej `RecycleItem` był na liście wywołań, na które lokalny backend
odpowiadał samym „tak”. Klient czyta odpowiedź jako liczbę punktów, więc z „tak”
robiła się jedynka: licznik zawsze pokazywał 1 punkt, a rzecz zostawała w szafie,
bo serwer niczego nie usuwał.

## Prezenty i wymiany

Kliknij znajomego, a w jego okienku wybierz ikonę prezentu albo wymiany.

**Prezent.** Okno pokazuje rzeczy, które masz i których nie nosisz. Po wysłaniu
przedmiot od razu znika z Twojej szafy, a u odbiorcy czeka zapakowany, dopóki go
nie otworzy — dopiero wtedy trafia do jego szafy. Nie da się podarować rzeczy,
którą odbiorca już ma, ani obdarować samego siebie. Limit to dwadzieścia
prezentów na dobę.

**Lista życzeń.** Dodane pozycje widzą inni i mogą je kupić za swoje StarCoiny
przyciskiem kupna; własne wpisy usuwasz sam.

**Wymiana.** Proponujesz swój przedmiot i wskazujesz, co chcesz w zamian. Twoja
rzecz idzie w zastaw od razu; druga strona traci swoją dopiero po przyjęciu.
Odrzucenie oddaje zastaw właścicielowi. Zakończone wymiany zostają w logu
wymian po obu stronach.

Po otwarciu prezent na liście „Otrzymane” pokazuje przedmiot, a nie paczkę:
klient rysuje paczkę tylko dla stanu „czeka” (`GiftState.WAITING`), a przedmiot
składa z numeru kategorii i pliku (`AMFGift.ClothesCategoryId` i `SWF`). Serwer
trzyma otwarty prezent w stanie „czeka” z datą otwarcia, więc na liście podaje
go jako zakończony i dokłada plik oraz kategorię; nieotwarta paczka dostaje w
`SWF` papier, który wybrał nadawca. Ponowne otwarcie tego samego prezentu
oddaje ten sam przedmiot (bez drugiej sztuki i bez Fame).

Wynik dawania i kupowania prezentu to obiekt z polami `Code` i `Description`
(tak czyta go `GiftService`). Wcześniej serwer oddawał gołą liczbę, a klient
czytał z niej `Code`, czyli nic — więc błąd, limit dzienny i brak StarCoinów
wyglądały jak udany prezent.

Prezenty dają Fame: dający dostaje `GIFT_GIVE_FAME` (100) przy wysłaniu z szafy
albo kupnie z listy życzeń, a obdarowany `GIFT_OPEN_FAME` (50) przy otwarciu.
Liczy się najwyżej `GIFT_FAME_DAILY` (10) prezentów na dobę w każdą stronę;
Fame Booster podwaja te nagrody jak każde inne. W oknie wyboru prezentu rzeczy
idą grupami — włosy, góry, doły, buty, czapki, dodatki i przedmioty.

### Tła, animacje i VIP w prezencie

Serwer włącza w kliencie funkcję `EXTENDED_GIFTING` (obok `FAME_MAGAZINE`
w `activeFeatureValues`). Okno „Wybierz prezent” dokłada wtedy do ubrań tła
i animacje konta oraz certyfikaty VIP, a księga prezentów w profilu dostaje
zakładkę VIP. Kod: `local_server/lib/ExtendedGifts.php`.

- **Tło** i **animacja** idą w zwykłej paczce (rodzaj 2 i 3). Podarować można
  tylko to, co konto kupiło: tła ze sklepu i animacje za StarCoiny. Dający traci
  je od razu, obdarowany dostaje przy otwarciu (z Fame, jak przy ubraniach).
  Tła dostępne w studiu dla wszystkich też są na liście, ale każdy je już ma —
  gra odpowiada wtedy, że ta osoba ma już to tło. Rzeczy za diamenty gra
  blokuje sama. Otwarta paczka pokazuje miniaturkę: tło z
  `backgrounds/snapshots/<numer>.png`, animację z
  `swf/animations/f_<etykieta>.png` (albo `m_`).
- **VIP w prezencie** to certyfikat. W panelu → Kody (przy kodzie albo w
  zestawie) zaznacz „VIP w prezencie”: kod nie daje wtedy VIP-a
  odbierającemu, tylko certyfikat na te dni i rangę, a gracz dostaje okienko,
  gdzie go szukać. Certyfikat daje się w oknie „Wybierz prezent”, obdarowany
  przyjmuje go albo odrzuca w zakładce VIP księgi prezentów. Przyjęcie dokłada
  dni VIP-a tak samo jak kod; odrzucony wraca do dającego i można go dać komuś
  innemu. Certyfikaty leżą w stanie kont w `vipCertificates`.

Ubrania można podarować także komuś, kto ma już taką rzecz — w MSP da się mieć
kilka egzemplarzy, a klient przy ubraniu nie ma komunikatu o duplikacie, więc
odmowa serwera kończyła się tym, że po kliknięciu „Daj prezent” nic się nie działo.

Prezenty, życzenia i wymiany zapisują się w `local_server/data/users.json`.

## Funkcje moderatora w grze

Ekipa (rola konta: 1 administrator, 2 moderator, 3 ukryty administrator) ma
narzędzia wprost w grze, tak jak w MSP. Logowanie podaje klientowi rolę konta
(`statusDetails` = `eui3hfiu3oa`, po którym klient bierze uprawnienia), więc:

- w okienku każdej postaci jest **przycisk admina** (ikona nad ikoną zgłaszania,
  podpowiedź „Admin”) — otwiera okno **Admin options for &lt;nick&gt;**;
- okno daje: **Lock Forever** (blokada konta na stałe), **Unlock**, **Undelete**,
  **Change Name**, **Force Namechange**, **Reset School**, **Refresh** oraz
  podglądy (Chatlog, Transactions, Mails Sent, Giftlog, Trade Log, Login
  History, Password Log) — wszystko idzie przez AMF do
  `local_server/lib/MspServer.php` (blokady zapisują się w koncie, każda akcja
  ląduje w dzienniku moderacji widocznym w `admin.php`);
- w lewym górnym rogu ekranu ekipa widzi licznik zgłoszeń graczy
  (Priority / Oldest / Amount), odświeżany co 10 sekund;
- w prawym dolnym rogu jest przycisk **Admin**, a przycisk **Chatlog** w oknie
  Admin options otwiera moduł moderacji z 2016 roku (`adminmodule_local.swf`,
  ustawienie `AdminModule`) — ten sam panel co w MSP:
  - **działa z naszym serwerem**: Reports (zgłoszenia graczy z przyciskami
    ostrzeżeń i blokad), Latest Warnings/Locks, Chatlog konta, Alert words
    (złe słowa), Blocked names, Moderators, Transactions;
  - **otwiera się, ale nie ma zaplecza**: uploady (ubrania, rzeczy, tła,
    animacje, części twarzy, ClipArt, tapety, Bonstery), Theme Manager, Tag
    Manager, Admin Shop, szablony ArtBooków, Chat Rooms, Clear New Marking,
    Sort Shops, Clear server Cache, Manage Polls, Upload News. To wszystko robi
    panel `admin.php` — tam te narzędzia są napisane pod nasz serwer;
  - panel pokazuje uploady i menedżery tylko administratorom (role 1 i 3);
    moderator (2) ma samą moderację. W oryginale decydowała o tym usługa SOAP
    `IsAdminSite`, której lokalny serwer nie ma — stąd łatka
    `AdminPanelAllTools.pcode` w module.
  Log czatu pokazuje wiadomości z Poczty — rozmowy w pokojach czatowych chodzą
  tylko w kliencie, więc ich tam nie ma;
- ekipa nie podlega filtrom czatu i — inaczej niż w MSP — może pisać na czacie
  (łatka `ModeratorCanChat.pcode`; w oryginale moderatorowi wyszarzało pole
  z napisem „Moderators are not allowed to chat”);
- **plakietka Moderator** na postaci pokazuje się tylko rolom 1 i 2; ukryty
  administrator (3) chodzi po grze bez żadnego oznaczenia.

Wyłączona zostaje tylko nakładka wsparcia z 2016 roku (szersza niż lokalna
scena, po nieudanym wywołaniu AMF zasłaniała grę) — robi to łatka
`ConfigureActorSettingsCommand.as`. Żeby zabrać ekipie narzędzia w grze, ustaw
`CLIENT_MODERATOR_FEATURES` na `false` w `local_server/lib/MspServer.php`;
panel `admin.php` działa niezależnie od tego ustawienia.

### Ukryty administrator (rola 3)

Dla zwykłych graczy takie konto nie istnieje:

- nie pokazuje się w wyszukiwarce graczy ani w wyszukiwarce znajomych;
- nie ma go w rankingach (movie stars, pokoje, filmy, ArtBooki);
- nie da się mu wysłać **Specjalnego Pozdrowienia**;
- nie da się do niego napisać (wiadomość wraca z błędem „nie znaleziono
  odbiorcy”), ale on sam pisze do wszystkich normalnie;
- nie nosi plakietki Moderator.

Ekipa (role 1, 2 i 3) oraz samo konto widzą je normalnie, więc administratorzy
nadal znajdą się nawzajem. W grach wieloosobowych i pokojach czatowych ukryty
administrator jest widoczny jak każdy gracz — jeśli ma zostać niewidzialny,
po prostu tam nie wchodzi.

## Status, mini profil i szafa

Kliknij tekst w dymku, wpisz status i użyj dyskietki, żeby go zapisać.
Status pozostaje po wyjściu i ponownym logowaniu. Kolorowe kółko otwiera paletę
dla aktywnego VIP-a, a buźka — standardowe emotikony. Kolor i emotikony są
zapisywane razem z tekstem. Zwykłe konta używają czarnego tekstu.

Tekst statusu rysuje się wbudowaną czcionką gry „Aria4” (Arial z polskimi
znakami). Pole `MSP_EmoticonTextFlowArea` przy wstawianiu emotikonów budowało
nowy `TextFlow` bez wyszukiwania czcionki wbudowanej, więc Flash szukał w systemie
czcionki o nazwie „Aria4”, nie znajdował jej i pisał Times New Roman. Łatka
`StatusTextFont.pcode` ustawia czcionkę wbudowaną po każdej zmianie tekstu —
w podglądzie statusu i we wszystkich innych polach tej klasy (dymki statusu,
komentarze, wiadomości).

Statusy i wpisy do Księgi Gości leżą przy koncie w jednej liście
(`guestbook`). Rozróżnia je autor: wpis właściciela na własnej ścianie to
status, wpis kogoś innego to Księga Gości. Na profilu zakładka „Aktywność”
(`getWallActivitiesForActor`) pokazuje tylko statusy, a „Księga Gości”
(`GetWallPosts`) tylko wpisy innych graczy; wcześniej obie dostawały całą listę.

Nazwa na mini profilu wraca do pola tekstowego z oryginalnego projektu.
Klient podmieniał je na `TLFTextField`, a natywny Flash układa TLF wyżej niż
zwykłe pole, więc nazwa uciekała do góry i zostawiała lukę nad plakietkami —
Ruffle rysuje TLF prawie jak zwykłe pole i dlatego wyglądało tam dobrze.
Teraz kopia TLF nie trafia na listę wyświetlania, a nazwa wpisuje się
w oryginalne pole, więc oba odtwarzacze pokazują ją tak samo.

Kliknięcie własnej postaci otwiera oryginalny mini profil. Ikona koszulki
otwiera szafę: wybierz posiadane ubrania i kliknij „OK”, żeby zapisać strój.
Rola Moderator wyświetla oryginalną plakietkę na środku postaci. Judge i Celeb
można niezależnie włączać i wyłączać w panelu kont.

W szafie jest lusterko jak na MSP: osobna, szara zakładka z ikoną lusterka po
prawej, za pięcioma kategoriami. Otwiera nad szafą okno lusterka — to samo okno
co Klinika Piękna, ale tylko z oczami, nosami, ustami, cieniami i odcieniami
skóry, które konto już ma, bez cen i bez „NEW”, z przyciskiem „Ok” zamiast
„Kup”. „Ok” zapisuje wybór za darmo i zamyka okno, a podgląd w szafie od razu
pokazuje nową twarz; krzyżyk zamyka bez zmian. Są tu też stare oczy z kreatora
postaci (pliki SWF, nie DragonBones). Klinika w Zakupach dalej jest sklepem.

Jak to działa: szafa wczytuje nowszą grafikę z MSP
(`swf/shops/dressing/changeclothes_mirror.swf`; szósta zakładka to lusterko,
zakładka „Ulubione” jest schowana, bo lokalnie tej funkcji nie ma), a ikona
lusterka to symbol `ShopBeautyClinicIcon` z nowszej paczki
`swf/iconpacks/icons_clothingcategories.swf` (ma też wszystkie stare ikony).
Łatki: `DressingRoom*.pcode` w `dressingmodule_local_v2.swf`,
`ShoppingModuleManager.as` (flaga `beautyClinicMirror`, `openBeautyClinicMirror`)
w `main_compat.swf` oraz `BeautyClinicModel.as` i `BeautyClinicMirror*.pcode`
w `shoppingmoduleexternal_local_v7.swf`; serwer podaje dane przez
`LoadDataForBeautyClinicMirror` (tylko posiadane części, cena 0). Nazwę pliku
szafy klient ma wpisaną na sztywno (`SessionAmfService.GetAppSetting`, etap w
`tools/build-client.ps1`) i wczytuje moduły bez parametru wersji,
więc każda nowa wersja szafy dostaje nową nazwę pliku — inaczej przeglądarki
brałyby starą z pamięci.

Jury i Judge liczą się z dni VIP-a kupionych łącznie (`totalVipDays`, z kodów
i z panelu): Jury po 90 dniach (3 miesiące), Judge po 360 dniach (12 miesięcy) —
roczny VIP daje więc Judge. Judge zdobyty z dni zapisuje się na koncie jako
plakietka (`judge`, a znacznik `judgeAuto` pilnuje, żeby nagroda padła tylko raz),
więc administrator może ją zabrać w panelu i nie wróci sama. Klient wnioskuje
plakietkę z `TotalVipDays` (Jury od 90 do 179, Judge od 180), dlatego serwer
wysyła mu liczbę dopasowaną do tych progów (`MspAccountState::clientVipDays`).

Są też plakietki Elite, Tester i Admin (`swf/world/frameIcons/elite_rainbow.swf`,
`tester.swf`, `admin_red.swf`). Elite zastępuje Judge u graczy z VIP-em
łącznie na dłużej niż 400 dni — przychodzi sama, raz, tak jak Judge, i panel
może ją zabrać albo nadać. Tester i Admin nadaje się w panelu (konto →
Edycja → Odznaki i rola). W grze stoją nad Judge/Elite w kolumnie pod postacią,
a kto nie ma Judge/Elite/Jury, ma je na tym miejscu; w mini profilu są tej samej
wysokości co oryginalne odznaki: Elite na miejscu Judge, a Admin i Tester
rząd wyżej, nad nickiem, w tym samym rozmiarze (nick i dolny rząd zjeżdżają
wtedy o 5 px; bez Judge/Elite/Jury i Celeb — na miejscu Judge). Na stronie
profilu są przypinkami ze spinaczem jak JUDGE/JURY/CELEB
(`swf/world/frameIcons/clip_elite.png`, `clip_tester.png`, `clip_admin.png`:
spinacz JURY z `myprofile_book.swf` bez napisu, przebarwiony, z nowym napisem;
Elite tęczowy): Elite zastępuje JUDGE, a Admin i Tester są przypięte równo do
górnej krawędzi zdjęcia profilowego, obok spinacza VIP, pod kątem zdjęcia. Klient nie ma na nie pól, więc serwer dokłada je w wyższych bitach
`TotalVipDays` (dni zostają w dolnych 16 bitach), a `ActorUtils` w kliencie
(`tools/client-patches/ActorUtils.as`) je rozdziela i ustawia obrazki. Test:
`tools/test-vip-badges.php`.

Sesje mają osobne bilety logowania: dwa otwarte konta zapisują swoje dane.
Po aktualizacji z wcześniejszej wersji odśwież klienta i zaloguj się ponownie.

## Co zostało poprawione

- bramka Discorda przed grą: logowanie OAuth2, lista zaufanych ID w panelu
  i sprawdzanie podpisanego ciasteczka w każdym żądaniu gry;
- nowy panel admina `admin.php` w miejsce `accounts-local.php`: gracze, kary,
  newsy, sklep, kody, strona główna i Pixi Star w jednym miejscu;
- panel: zmiana i wymuszenie nicku (okno `ForceNameChange` w grze działa przez
  AMF), usuwanie kont jak w MSP z opcją zwolnienia nicku, adresy IP z rejestracji
  i logowań, zakładka Forum z usuwaniem postów i tematów, tła w sklepie;
- sklep z tłami w grze: listy, podkategorie i kupowanie teł dodanych w panelu;
- okno „Gratulacje!” po kodzie na natywnym Flashu (kolekcje `Items`/`Visuals`
  w odpowiedzi) i także dla kont bez VIP-a;
- Pakiet Diamond w panelu i w oknie pakietów w grze, ubrania w zestawach kodów
  i zmiana ich koloru w oknie „Gratulacje!”;
- przetwarzanie rzeczy w profilu: punkty liczą się jak w grze, rzecz znika
  z szafy, a za 1000 punktów wpada losowa rzecz ze sklepu;
- dodatki w końcu widać na postaci (miały numer kategorii koszulki, więc gra
  szukała ich pliku w `swf/tops`), a okulary, kolczyki, ozdoby włosów, szalik,
  tatuaż i czapkę można nosić naraz;
- masowe dodawanie ubrań w panelu: przeciągasz do okna dowolnie wiele plików SWF
  naraz, a nazwy powstają z nazw plików i ceny losują się z czterech podanych;
- sklep z rzeczami: zakładka „Rzeczy” w panelu i działające zakładki sklepu
  w grze (`GetItems*`), a przy masowym dodawaniu panel sam rozpoznaje, co to za
  rzecz, i wkłada ją do właściwej kategorii;
- sklep zwierzaków: trzy jajka na gniazdach zamiast wszystkich naraz,
  wymieniane co poniedziałek;
- ogłoszenia ekipy: zakładka „Ogłoszenie” w panelu i okienko z komunikatem
  u wszystkich, którzy są w grze;
- prezenty: otwarty prezent pokazuje przedmiot zamiast paczki, błędy dawania
  nie udają sukcesu, dający i obdarowany dostają Fame;
- prezenty z tłami, animacjami i VIP-em (certyfikat z kodu „VIP w prezencie”),
  a „Daj prezent” działa także, gdy obdarowany ma już taką samą rzecz;
- looki: zapis z przymierzalni, listy, polubienia i magazyn blobów;
- kafelek looka pokazuje nazwę, datę i nick — `DateUtils.ToDateString`
  wywalał się na brakującym pakiecie `SharedResources` i ubijał
  `LookListItemComponent.Update()` na pierwszej instrukcji;
- kody podarunkowe z panelu: VIP i pakiety bez przelogowywania,
  z oryginalnym oknem „Gratulacje! Jesteś teraz ... VIP!”;
- moduł płatności: okno VIP-a i pakietów pokazuje, co która oferta daje;
- odtwarzanie filmów: brakująca muzyka przerywała `playScene` przed
  utworzeniem timera, więc film stał na klatce 0:00;
- tła w studiu filmowym — pliki w `backgrounds/` plus `GetMyBackgrounds`;
- brakujące animacje z domyślnej listy klienta, w tym `walk`;
- miniaturki filmów i pokoi (`movie-snapshots/`, `entity-snapshots/`);
- dekodowanie odpowiedzi logowania i kolekcji AMF w Ruffle;
- stare wywołania SOAP zastąpione lokalnymi wywołaniami AMF;
- odpowiedzi wymagane do uruchomienia mapy i utrzymania sesji;
- lokalny start bez połączenia z historycznym serwerem XMPP;
- obsługa klasycznych oczu SWF w kreatorze, zamiast wymuszania DragonBones;
- oddzielne miejsca na ubrania — założenie spodni nie usuwa już bluzki;
- oryginalne skiny, oczy, nosy, usta, bluzy i spodnie z dostarczonych plików;
- oryginalne Girl Pose i Boy Pose z ich punktami mocowania i klatkami animacji;
- dostarczone stand, talk, Handgesture1, Handgesture2 i I am Cool;
- oryginalne włosy college/northern, buty golden/kpop, nowa bluzka, spodnie i nose_6;
- zapis walut, Fame, poziomu, VIP-a, moderatora i animacji dla każdego konta;
- inicjalizacja licznika walut po logowaniu i obsługa pustego wyniku formattera Flex;
- dołączony Ruffle oraz odświeżanie poprawionych SWF-ów.
- zapis statusu, kolorów VIP, standardowe emotikony i poprawki warstw tekstu;
- oryginalny mini profil, odczyt szafy oraz zapis noszonych ubrań;
- plakietka Moderator i edytowalne Judge/Celeb;
- osobne sesje oraz kończenie zapisu bez połączenia z serwerem znajomych.
- pełny profil otwiera się z lokalnego modułu, ładuje aktywność i księgę gości;
- zakładka biografii korzysta ze zgodnego edytora dołączonego do klienta;
  ten edytor nie zgłasza końca ładowania, więc łatka profilu sama zdejmuje
  zasłonę ładowania, gdy go pokaże (bez tego biografia „ładowała się w kółko”);
- naklejki w edytorze ArtBooków i biografii podaje serwer (`local_server/lib/ClipArt.php`)
  z katalogu `clipart/<rodzaj>/<kategoria>/`, numery kategorii jak w kliencie.
  Kategorie rodzaju `stickers` pojawiają się w zakładce Naklejki. Kolejne naklejki
  wystarczy wgrać do katalogu kategorii; numer na początku nazwy pliku ustala
  kolejność. Na start są „Kształty” (`clipart/stickers/shapes/`, pliki
  z `tools/make-clipart-shapes.py`): kwadraty i kółka w 20 kolorach oraz
  kwadrat i kółko z dowolnym kolorem dla VIP-ów. Moduł ArtBooka nie wczytuje
  kształtów na płótno, tylko rysuje je z nazwy pliku: `Square` to kwadrat,
  inaczej kółko, a kolor wynika ze słowa w nazwie (`Red`, `LightBlue`, `Skin`…,
  wielkość liter ma znaczenie). Plik służy tylko za miniaturkę. Dlatego serwer
  podaje w tej kategorii tylko nazwy z `Square_`/`Circle_`. Kształty z dowolnym
  kolorem to wpisy `Square_Colorable.swf` i `Circle_Colorable.swf` z `Vip = 1`
  (miniaturki: `Artbook_Shapes_SquareColorpicker.swf`, `…CircleColorpicker.swf`).
  Stary edytor biografii pokazuje zwykłe pliki i nie ma próbnika kolorów;
- osiągnięcia, rankingi i dane profilu są obsługiwane przez lokalny backend.
- funkcje moderatora nie ładują się już przy logowaniu, więc klient wchodzi
  do gry także w natywnym Flash Playerze;
- zamknięcie koła fortuny krzyżykiem nie zawiesza gry — łatka logowania
  tworzyła uszkodzone domknięcie i klient rzucał VerifyError #1107;
- pasek poziomu na koncie z maksymalnym poziomem mieści się w zakresie
  0–100% także przy bardzo dużej Fame;
- działa forum: działy, tematy, posty, ankiety, głosy i narzędzia moderatora;
- działają prezenty, lista życzeń i wymiany przedmiotów między kontami;
- działa sklep z animacjami wraz z zakładką Animacje w panelu.
- progi poziomów z MSP (poziom 101 od 4 335 320 220 Fame) i osobny, wyższy
  sufit dla Fame; poziomy kont przeliczyły się z ich Fame;
- 200 poziomów (200. od 18 mld Fame), niższe progi tańsze o ok. 10%, nazwy
  poziomów 102–200; pasek i okno „Zdobywaj Fame” kończą się na 200;
- Pixi Star — postać specjalna z gry: znajoma każdego, zawsze online, ze
  statusem zmienianym według listy z panelu;
- pełny profil Pixi: działa „Odwiedź Pokój”, ramka pokoju pokazuje pusty
  pokój zamiast szarego pola, a „Aktywność” — historię jej statusów;
- ikonki filtrów nad ścianą stoją w równym rzędzie, jak w oryginale — nie
  rozjeżdżają się i nie nachodzą na siebie na profilu Pixi;
- `index-local.html` nie jest trzymana w cache przeglądarki, więc po
  przebudowie klienta nie ładuje się już jego stara wersja;
- na stronie głównej znów stoi MovieStar z animacjami, jak na MSP — wybrana
  w panelu albo w losowym, lubianym looku;
- lista krajów na stronie głównej zmienia język lokalnej gry zamiast
  przenosić na prawdziwą stronę MSP;
- wylogowanie wraca na stronę główną zamiast wisieć na „Logging out…”;
- kreator postaci: trzy rzędy twarzy (po 3 oczy, nosy i usta) i po 9 ubrań
  na kategorię dla każdej płci, bez strzałek; dziewczyny mają nosy i usta
  z DragonBones, a postać nie rozjeżdża się od źle opisanych plików;
- w kreatorze nosy i usta stoją w równych kolumnach pod oczami, usta mają
  poprawny podgląd i da się zmienić ich kolor;
- prawdziwe animacje `Wave`, `sing2` i `Handgesture3` zamiast zaślepek, przez
  które postać w kreatorze co chwilę na moment się rozjeżdżała;
- dane gry w bazie MySQL (`msp`) z zapisem w transakcjach, skrypt przenoszący
  dane z plików JSON i plik konfiguracyjny pod hosting; bilety sesji
  i dzienniki nie rosną już bez końca;
- nagrody (Awards) liczą prawdziwą aktywność — dni VIP-a, filmy, autografy,
  zakupy, koło fortuny, serię logowań — zamiast brać etap z poziomu konta;
- klinika piękna dla mężczyzn: 33 pary oczu z DragonBones, 5 nosów i 6 ust,
  a do tego 66 cieni do powiek dla kobiet — wcześniej męskie konto dostawało
  `WRONG_GENDER` na każdy zakup, a lista cieni była pusta;
- ikona przyjaciół na górnym pasku pokazuje liczbę („50 Przyjaciele”) —
  `Utils.ThousandString` używał `CurrencyFormatter` bez `alignSymbol`, który
  pochodzi z niewczytywanego pakietu `SharedResources`, więc zwracał pusty
  napis; teraz liczy przez `NumberFormatter.formatNumber` (to samo dotyczyło
  liczników w rankingu, ArtBookach i punktów zabawy zwierzaka);
- powiadomienia na żywo bez XMPP: zaproszenie do znajomych, najlepsi
  przyjaciele i serduszko wyskakują od razu na dolnym pasku z przyciskami,
  a przy ikonie przyjaciół pojawia się licznik (`SendLocalNotification`,
  odbiór w `GetChatUpdates`);
- Fame za aktywność: 10 za każde wydane 100 StarCoins, 100 za status (co
  5 minut), 200 za nowy film, artbook i look, 50 za zmianę biografii;
- po zalogowaniu i założeniu konta nie wyskakuje już pytanie o aktualizację
  gry: serwer nie podaje numeru wersji (`Version` w
  `LoadActorDetailsExtended`), a klient porównywał go z nazwą pliku SWF
  (`ValidateClientVersion`) i przy `main_local.swf` zawsze wychodziła różnica;
- Koło Fame w sklepie diamentowym pokazuje liczby i wygraną
  (`shoppingmodule_local.swf`): pola koła miały własne, niepełne czcionki
  „Arial” i „Blue Highway”, a Flash przy wpisywaniu tekstu brał czcionkę
  o tej samej nazwie wczytaną wcześniej, bez cyfr. Podgląd koła zamienia teraz
  czcionki wszystkich swoich pól na pełne czcionki gry, jak inne podglądy
  sklepu (`FameWheelFonts.pcode`);
- w rankingach widać numer poziomu na gwiazdce: pole miało czcionkę systemową
  i filtr obrysu, a Flash nie rysuje czcionki systemowej pod filtrem — renderer
  przełącza je na wbudowaną czcionkę gry;
- Fame Booster podwaja Fame także za autografy (podpisującemu), ocenę filmu
  i odbiór z gazetki Fame. Gazetka pokazuje odebrane Fame jako „liczone przez
  klienta” i przy boosterze sama wyświetlała podwójną kwotę, a konto dostawało
  pojedynczą, więc po odświeżeniu licznik spadał. Zasada boostera jest teraz
  w jednym miejscu (`boostedFame` w `MspServer.php`);
- na maksymalnym poziomie (101) pasek poziomu jest pełny i po najechaniu pisze
  „Masz już maksymalny poziom” (`FAME_MAX_LEVEL` w tłumaczeniach). Wcześniej
  liczył postęp do poziomu 102 — serwer dopisuje za progami sztuczny próg,
  żeby klient poprawnie wyznaczał poziom z Fame (`LevelBarMaxLevel.pcode`);
- w klinice da się zmienić każdy kolor ust — także kolczyk, serduszko, aparat,
  grille i drugi kolor ust dwukolorowych; wcześniej wszystkie usta miały jeden
  kolor do wyboru (opis w „Kolory ust”);
- stopy nie wystają już z nowszych butów na obcasie, kozaków i sandałów —
  stopa skóry dopasowuje się do butów robionych pod stopę na palcach (opis
  w „Buty z nowszych wersji MSP”);
- rzęsy i brwi w nowszych oczach nie malują się już kolorem skóry: każde oko
  z DragonBones ma własne domyślne kolory wzięte z jego grafiki, a zapisane
  wcześniej kolory kont naprawiają się same (opis w „Kolory oczu”);
- działa zgłaszanie i blokowanie graczy: okno „Zgłoś” nie wisi już bez
  odpowiedzi, „Zablokuj” i „Usuń Blokadę” działają, a w panelu jest zakładka
  **Zgłoszenia** z dowodami (opis w „Zgłoszenia i blokowanie graczy”);
- podgląd statusu nie pisze już czcionką Times: pola tekstu z emotikonami
  dostają wbudowaną czcionkę gry (opis w „Status, mini profil i szafa”);
- działają zdjęcia: przycisk w Movie Town, wgrywanie, polubienia, komentarze
  i zdjęcia na profilu, a każde nowe zdjęcie przed pokazaniem innym zatwierdza
  ekipa w zakładce **Zdjęcia** panelu (opis w „Zdjęcia”);
- w ArtBookach „Love it” może dać każdy gracz (wcześniej po pierwszym
  polubieniu przycisk był zablokowany wszystkim innym) i działają komentarze
  (opis w „ArtBooki: „Love it” i komentarze”);
- Opcje → Zmień hasło naprawdę zmienia hasło (wcześniej okno mówiło „zmieniono”,
  a serwer tylko odpowiadał `true`): serwer sprawdza sesję, stare hasło i długość
  nowego (6–50 znaków), a inne sesje konta kończy — ta, z której zmieniono hasło,
  zostaje. Test: `tools/test-change-password.php`;
- w klinice piękna raz kupiony kształt oczu/nosa/ust/cienia i odcień skóry
  wraca za darmo (opis w „Lusterko: raz kupiona rzecz wraca za darmo”);
- nowe plakietki Elite (VIP łącznie na ponad 400 dni, zamiast Judge), Tester
  i Admin — w grze obok Judge/Celeb, nadawane w panelu (opis przy Jury i Judge);
- w szafie jest lusterko jak na MSP (osobna zakładka z ikoną lusterka): wybór
  spośród posiadanych oczu, nosów, ust, cieni i odcieni skóry, bez cen, z „Ok”
  (opis w „Status, mini profil i szafa”);
- zwykłe pokoje czatu (plaża, kawiarnia, centrum handlowe, skatepark, park,
  klub VIP, kino, szkoła, pokój tematyczny) naprawdę łączą graczy — widać
  w nich innych, da się chodzić i pisać (dymki widzą wszyscy), jest lista pokoi
  („Kawiarnia Flora”, „Kawiarnia Luna”…) z przechodzeniem między nimi, a inni
  widzą tylko założone ubrania (opis w „Zwykłe pokoje czatu”);
- skrócone napisy kończą się „...” zamiast „null” (np. imię na liście zaproszeń
  do pokoju czatu: „Kolor...” zamiast „Kolonull”) — klientowi brakowało
  znacznika skracania z paczki „core” Flexa, łatka `SetupAlertCommand.as` go
  dokłada;

## Zakres lokalnej wersji

To ograniczona wersja do tworzenia postaci i oglądania wnętrza gry. Mapa i jej
działy korzystają z zachowanych grafik. Pełny sklep, ekonomia, czat z innymi
graczami, pokoje i filmy wymagają dalszej implementacji usług (z gier sieciowych działa Quiz).
Waluty i uprawnienia można już zapisywać i edytować, ale pełne mechaniki zakupów,
zarabiania oraz narzędzia moderowania innych graczy nie są zaimplementowane.
Niektóre podstrony mogą być puste. Dostępny katalog wyglądu jest niewielki.

## Diagnostyka i odtwarzanie

- `http://localhost:1600/health.php` sprawdza pliki i pokazuje ostatnie wywołania
  (po zalogowaniu w panelu admina).
- `C:\xampp\php\php.exe tools\test-local.php` sprawdza rejestrację, limit
  kreatora (po 3 części twarzy i najwyżej 9 ubrań na kategorię), hasła,
  zapis wyglądu, typy i referencje AMF oraz dane startowe w osobnej bazie testowej.
- `prepare-client.php` odtwarza `main_local.swf` z przygotowanej bazy
  `local_server/client/main_compat.swf` i stosuje lokalne adresy; w przeglądarce
  tylko dla zalogowanej ekipy, a z wiersza poleceń bez logowania
  (`C:\xampp\php\php.exe prepare-client.php`).
- `tools/build-client.ps1` odtwarza bazę kompatybilności za pomocą FFDec.
- `C:\xampp\php\php.exe tools\migrate-to-mysql.php` przenosi dane z plików JSON
  do bazy, a `tools\test-mysql-storage.php` sprawdza pracę na MySQL.
- `C:\xampp\php\php.exe tools\test-account-state.php` sprawdza zapis i migrację
  kont, poziomy, VIP, role, animacje, walidację i konflikty edycji.
- `C:\xampp\php\php.exe tools\test-discord.php` sprawdza bramkę Discorda:
  listę zaufanych ID, podpisane ciasteczka (podrobione, po terminie, po usunięciu
  ID), zliczanie odrzuconych prób, unieważnianie logowań i zamknięte endpointy gry.
- `C:\xampp\php\php.exe tools\test-admin.php` sprawdza panel admina: logowanie
  tylko dla ról, przerwy po nieudanych próbach, listy i profil gracza, kary,
  reset hasła, ograniczenia edycji, szkice ubrań i animacji, newsy i zestawy kodów.
- `tools/import-avatar-assets.ps1` instaluje 12 dostarczonych SWF-ów z Downloads.
- `C:\xampp\php\php.exe tools\test-forum.php` sprawdza działy, tematy, posty,
  filtry, ankiety, głosowanie, limity i uprawnienia moderatora na forum.
- `C:\xampp\php\php.exe tools\test-gifts.php` sprawdza wysyłkę i odbiór
  prezentów, listę życzeń, wymiany, log wymian, limity i trwałość zapisu.
- `C:\xampp\php\php.exe tools\test-looks.php` sprawdza zapis looka razem z jego
  wyglądem, cztery listy, polubienia ze sławą, losowy look i kasowanie.
- `C:\xampp\php\php.exe tools\test-redeem-codes.php` sprawdza generowanie kodów,
  nagrody VIP i pakietowe, jednorazowość, dokładanie dni VIP-a, unieważnianie,
  okno z podsumowaniem zakupu i listę ofert.
- `C:\xampp\php\php.exe tools\test-animations.php` sprawdza dodawanie animacji
  z panelu, ceny, waluty, blokadę VIP, zakupy i ustawienie ich na postaci.
- `C:\xampp\php\php.exe tools\test-autographs.php` sprawdza dawanie autografów,
  sławę dla obu stron, liczniki, znacznik czasu dla klienta i przerwę VIP.
- `C:\xampp\php\php.exe tools\test-movies.php` sprawdza zapis filmu, wierność
  treści scen, publikację, listy, wyszukiwanie, oglądanie (jedno na konto,
  50 SC dla reżysera), oceny, kasowanie oraz katalog teł i muzyki.
- `C:\xampp\php\php.exe tools\test-rooms.php` sprawdza wejście do pokoju,
  tapetę i podłogę, zapis rzeczy, odwiedziny, polubienia i trwałość.
- `C:\xampp\php\php.exe tools\test-relationships.php` sprawdza prośby o serduszko
  i najlepszą przyjaźń, zgody, odmowy, limity, wymianę relacji i trwałość zapisu.
- `C:\xampp\php\php.exe tools\test-social-state.php` sprawdza status, kolory VIP,
  nadawanie i usuwanie plakietek, szafę oraz rozdzielenie równoczesnych sesji.
- `C:\xampp\php\php.exe tools\test-pixi-star.php` sprawdza listę postaci specjalnych, Pixi jako
  znajomą każdego i online, statusy z panelu, jej ścianę, postać i kopiowanie wyglądu.
- `C:\xampp\php\php.exe tools\test-frontpage.php` sprawdza postać ze strony głównej: odpowiedź
  typu Look, domyślną Pixi, stary bilet sprzed logowania, animacje w ustawieniach, panel, tryb looków
  i tłumaczenia dla każdego kraju z listy.
- `C:\xampp\php\php.exe tools\test-awards.php` sprawdza, że nagrody liczą dni VIP-a, zakupy według
  rodzaju, wygrane w kole, autografy, filmy i serię logowań, a nie poziom.
- `C:\xampp\php\php.exe tools\test-beauty-clinic.php` sprawdza katalog kliniki
  dla obu płci, ceny, pliki zestawów DragonBones, zakupy, blokadę płci,
  cienie do powiek wraz ze zdejmowaniem i trwałość zapisu.
- `C:\xampp\php\php.exe tools\expand-dragonbone-jsonc.php --apply` rozpakowuje
  zestawy DragonBones, które przyszły tylko w wariancie `.jsonc`.
- `tools/avatar/` zawiera poprzednie źródła zastępcze; bieżący katalog używa oryginałów.

Oryginalny `main_20161031_132744.swf` pozostaje źródłem archiwalnym. Po zmianie
pliku klienta lub assetów trzeba zmienić wersję w launcherze i odświeżyć stronę.
Folder `local_server` jest zablokowany dla bezpośrednich żądań HTTP.
Ta konfiguracja jest przeznaczona do pracy na localhost.
