Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdaad0c940 |
@@ -1,5 +1,12 @@
|
||||
# Sanctuary
|
||||
|
||||
## beta.159 — recherche communautaire et galerie adaptable (pack local)
|
||||
|
||||
Recherche par joueur, titre ou texte, historique Serveur en lecture seule,
|
||||
année dans les dates et galerie adaptée au GUI avec grand aperçu.
|
||||
Repères « ! » et infobulles sur la carte pour les demandes suivies géolocalisées.
|
||||
[Résultat et vérifications](docs/community-search-beta159.md).
|
||||
|
||||
## beta.158 — cartes communautaires et demandes suivies (pack local)
|
||||
|
||||
Grilles à deux colonnes, articles photo/texte, annonces colorées avec visages,
|
||||
|
||||
@@ -199,3 +199,42 @@ Les anciens boutons de liaison et d'historique public de l'intendance disparaiss
|
||||
|
||||
En mode fichier, le panneau d’intendance en jeu s’ouvre avec
|
||||
`/sanctuary community admin` ; le serveur exige les permissions opérateur.
|
||||
|
||||
## Évolution beta.159 — recherche et lecture de l’intendance
|
||||
|
||||
Le schéma reste **v3** : aucune migration de données. Cette évolution remplace
|
||||
la restriction de lecture publique de beta.158 : les onglets Gazette, Tableau
|
||||
et Serveur permettent de lire leurs historiques. La publication et la gestion
|
||||
de l’intendance restent dans l’administration, sans réponse ni abonnement.
|
||||
|
||||
`list` et `cards` acceptent une recherche facultative dans `Request.body` (120
|
||||
caractères maximum, vide par défaut). Le site utilise le paramètre GET `q`,
|
||||
conservé dans la pagination. Recherche littérale de sous-chaîne dans le nom de
|
||||
l’auteur, le titre **ou** le corps, sans distinction de casse, avant pagination.
|
||||
Les accents restent significatifs et `%` / `_` sont des caractères ordinaires.
|
||||
Le filtre reste limité au serveur et à la rubrique sélectionnés ; les contenus
|
||||
masqués restent exclus. Les épingles gardent leur priorité. Le client attend
|
||||
300 ms après la frappe et ignore les réponses correspondant à un ancien filtre.
|
||||
Les pages restent bornées à 10 publications ; aucune recherche dans les réponses.
|
||||
|
||||
Les dates des publications affichent l’année (UTC). Le menu pause affiche la date
|
||||
complète dans la langue et le fuseau local du joueur. La galerie utilise la
|
||||
surface disponible du GUI, adapte ses colonnes et conserve ses pages de 24
|
||||
captures défilantes. Le clic ouvre un aperçu dans cette surface, en conservant
|
||||
les proportions et les boutons Retour / Assigner. Le formulaire conserve son
|
||||
aperçu compact. Les PNG sources ne sont pas modifiés.
|
||||
|
||||
Les demandes suivies ouvertes qui ont une position affichent un `!` sur la carte
|
||||
de pause et le grand atlas, dans la dimension correspondante. Elles ne révèlent
|
||||
pas le terrain et ne chargent aucun chunk. L’infobulle montre titre, auteur,
|
||||
résumé, coordonnées, matériaux et récompense ; les listes longues sont abrégées
|
||||
pour rester dans le GUI. Le clic ouvre l’annonce. « Voir sur la carte » centre
|
||||
l’atlas sur son lieu dans la dimension courante, sans téléportation. Les repères
|
||||
suivent tous les abonnements, indépendamment des trois suivis visibles dans le
|
||||
HUD ; ils disparaissent au désabonnement, à la clôture ou au masquage. Un
|
||||
rafraîchissement des suivis reste actif lorsque la carte ou la pause est ouverte
|
||||
(période de cinq secondes, différée si une autre requête est en cours).
|
||||
|
||||
Le message réseau `following` ajoute `summary`, extrait littéral du corps limité
|
||||
à 240 caractères Unicode plus une ellipse. Le texte intégral et les photos ne
|
||||
sont pas diffusés dans ce message. Aucun champ persistant supplémentaire.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# beta.159 — recherche communautaire et galerie adaptable
|
||||
|
||||
Branche `codex/community-search-gallery-beta159`, socle beta.158.
|
||||
|
||||
## Résultat
|
||||
|
||||
Une barre de recherche filtre les publications par auteur, titre ou texte côté
|
||||
serveur, avant pagination. Le filtre accompagne les pages suivantes, conserve
|
||||
les épingles et se réinitialise en effaçant le champ. Les réponses aux anciennes
|
||||
recherches sont ignorées et le focus de saisie est conservé.
|
||||
|
||||
Les trois onglets Gazette / Tableau / Serveur donnent accès aux publications de
|
||||
chaque rubrique. L’en-tête d’intendance de la pause ouvre aussi son historique.
|
||||
La lecture est publique ; la publication et la gestion des messages du serveur
|
||||
restent réservées au panneau administrateur, même pour un opérateur visitant
|
||||
l’écran public. Les messages du serveur n’acceptent ni réponses ni abonnements.
|
||||
|
||||
L’année figure dans la date de pause et les métadonnées des publications. La
|
||||
galerie utilise l’espace disponible aux différentes échelles de GUI et tailles
|
||||
de fenêtre. Les captures restent paginées par 24 avec défilement. Cliquer une
|
||||
capture ouvre un grand aperçu avec les commandes de retour et d’assignation.
|
||||
La photo assignée reste compacte dans le formulaire et les brouillons sont
|
||||
conservés lors du retour. Aucune modification des PNG originaux.
|
||||
|
||||
Les annonces suivies géolocalisées portent un « ! » sur les deux cartes.
|
||||
L’infobulle fournit titre, auteur, résumé, coordonnées, matériaux et récompense
|
||||
sans ouvrir la discussion ; le clic reste disponible. Un bouton dans l’annonce
|
||||
centre le grand atlas sur sa position, dans la dimension courante. Aucune
|
||||
téléportation ni révélation du terrain : seules les coordonnées publiées sont
|
||||
projetées. Les annonces fermées, masquées, sans lieu ou non suivies ne portent
|
||||
pas de repère. Les longues infobulles sont abrégées pour tenir dans le GUI.
|
||||
|
||||
Le site suit le même contrat de recherche et de lecture. Schéma v3 inchangé,
|
||||
aucune migration de sauvegarde ni de base. Libellés français et anglais.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- Stockage fichier et MariaDB : recherche par auteur et corps, casse et accents,
|
||||
filtre avant pagination, épingles conservées, caractères `%` / `_` littéraux,
|
||||
rubrique et serveur isolés, contenus masqués exclus, borne de 120 caractères.
|
||||
- Client natif Minecraft 26.3 / Java 25 / Vulkan : galerie et aperçu en FR/EN,
|
||||
GUI 2/3/4 et fenêtres 1280×960 / 1920×1080 ; retour au brouillon, assignation,
|
||||
publication, listes défilantes et chargement des pages suivantes.
|
||||
- Recherche en jeu au-delà de la première page, filtre par joueur, effacement,
|
||||
conservation du focus et rejet d’une réponse correspondant à l’ancien filtre.
|
||||
- Lecture des messages serveur par un joueur ordinaire, aucun contrôle d’écriture
|
||||
dans les pages publiques, y compris pour l’opérateur ; administration inchangée.
|
||||
- Carte : projection dans la dimension correspondante, titre/auteur/résumé,
|
||||
matériaux et récompense au survol ; ouverture de la demande par clic sur les
|
||||
deux cartes ; retrait immédiat du repère après désabonnement.
|
||||
- Date avec année en FR/EN, GUI 2/3/4, sans chevauchement de l’intendance.
|
||||
- Site : 24 tests / 157 assertions sur SQLite puis MariaDB de développement,
|
||||
formatage Pint réussi. Commit local `e70e78d`, branche `codex/community-search`.
|
||||
|
||||
La commande complète `./gradlew :sanctuary:runClientGameTest check build assemblePack`
|
||||
avec les propriétés client Cards158, QuickTests et Vulkan a exécuté 252 GameTests :
|
||||
229 réussites et les mêmes 23 échecs que beta.158. Aucun test serveur supprimé
|
||||
ou modifié. La comparaison est conservée dans `build/search159-server-failures.json`
|
||||
et le journal dans `build/beta159-check-build.log`.
|
||||
|
||||
Les contrôles restants, la compilation et `assemblePack` passent avec la suite
|
||||
serveur déjà exécutée exclue (`-x :sanctuary:runGameTest`) : 133 tâches, journal
|
||||
`build/beta159-release.log`. Le parcours client final passe en mode MariaDB,
|
||||
puis en mode fichier (`build/beta159-file-client.log`). Les repères ont été
|
||||
vérifiés avec et sans terrain d’atlas débloqué ; les infobulles restent dans la
|
||||
fenêtre aux GUI 2/3/4. Captures représentatives inspectées :
|
||||
`build/search159-file-screens/` et `build/search159-database-screens/`.
|
||||
|
||||
L’export packwiz est vérifié : intégrité ZIP, version beta.159, JAR embarqué
|
||||
identique au JAR de livraison. Les 29 ressources de shaders sont identiques à
|
||||
beta.158, qui conserve le socle beta.151. Les anciens JAR et MRpack beta.154 à
|
||||
beta.158 restent présents. Aucun monde personnel, déploiement de serveur, canal
|
||||
packwiz ou instance Prism n’est modifié.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.159.jar` — SHA-256 `c30d57a5f37e0a5c9dbb387ca831ce9e5c15a1dfe4cb55af0af20a376e862576`.
|
||||
|
||||
- `build/Sanctuary-beta.159.mrpack` — SHA-256 `868932d88bc241bfb526a7c5e4054009e247658bd0784b7141d82017edcc2823`.
|
||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.158
|
||||
pack_version=beta.158
|
||||
mod_version=beta.159
|
||||
pack_version=beta.159
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
+93
-13
@@ -43,17 +43,26 @@ public final class Community158ClientChecks implements FabricClientGameTest {
|
||||
var config=CommunityConfig.load(net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json"));
|
||||
CommunityStore store=database?new SqlCommunityStore(config.jdbcUrl(),config.user(),"",config.serverId()):new FileCommunityStore(server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"),config.serverId());
|
||||
var author=new Community.Actor(UUID.randomUUID().toString(),"Voisin",true);
|
||||
store.change(author,new Community.Change("create",UUID.randomUUID().toString(),"","bulletin","info","Annonce officielle159","Le serveur vous souhaite la bienvenue.",0));
|
||||
for(int i=0;i<13;i++) {
|
||||
store.change(author,new Community.Change("create",UUID.randomUUID().toString(),"","article","info","Gazette "+i,"Le village raconte. ".repeat(90),0,testPhoto()));
|
||||
store.change(author,new Community.Change("create",i==0?noticeId:UUID.randomUUID().toString(),"","notice",List.of("info","event","work","need").get(i%4),"Demande "+i,"Rendez-vous au village. ".repeat(40),0,null,CommunityTask.EMPTY));
|
||||
store.change(author,new Community.Change("create",UUID.randomUUID().toString(),"","article","info","Gazette "+i,"Le village raconte. ".repeat(90)+(i==0?" Unique159":""),0,testPhoto()));
|
||||
store.change(author,new Community.Change("create",i==0?noticeId:UUID.randomUUID().toString(),"","notice",List.of("info","event","work","need").get(i%4),"Demande "+i,"Rendez-vous au village. ".repeat(40),0,null,i==0?new CommunityTask(new CommunityTask.Location("minecraft:overworld",0,64,0),List.of(new CommunityTask.Material("minecraft:oak_log",128)),"shared","custom",List.of(),"Un repas au village"):CommunityTask.EMPTY));
|
||||
}
|
||||
}catch(Exception error){throw new AssertionError(error);}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Cards158 "+UUID.randomUUID());s.setSeed("157");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s->s.getPlayerList().getPlayers().getFirst().getAbilities().invulnerable=true);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
world.getServer().runOnServer(s->{
|
||||
var player=s.getPlayerList().getPlayers().getFirst();player.getAbilities().invulnerable=true;player.setExperienceLevels(20);
|
||||
check(fr.koka.sanctuary.progression.ProgressionService.purchase(player,"atlas",fr.koka.sanctuary.progression.ProgressEffects.get(player).revision()),"Atlas unlocked through progression");
|
||||
fr.koka.sanctuary.progression.ProgressionService.send(player,"");
|
||||
for(int z=-2;z<=2;z++)for(int x=-2;x<=2;x++) {
|
||||
var tile=fr.koka.sanctuary.atlas.AtlasSampler.sample(player.level(),player.chunkPosition().x()+x,player.chunkPosition().z()+z);
|
||||
if(tile!=null)fr.koka.sanctuary.atlas.AtlasService.observations(player).observe(tile);
|
||||
}
|
||||
});
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4))for(int windowWidth:List.of(1280,1920)) {
|
||||
c.runOnClient(m->{
|
||||
m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.getWindow().setWindowed(windowWidth,windowWidth==1280?960:1080);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
CommunityClient.open(new PauseScreen(true),"article","",true);
|
||||
check(!button(m.gui.screen(),"publish").active,"Photo is mandatory in the composer");
|
||||
edit(m.gui.screen()).setValue("Une journée au village");body(m.gui.screen()).setValue("La photo rejoint la Gazette.");
|
||||
@@ -63,23 +72,29 @@ public final class Community158ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->{
|
||||
var s=m.gui.screen();check(s instanceof CommunityScreenshotsScreen,"Dedicated screenshot view");
|
||||
var a=widgets(s).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).findFirst().orElseThrow();
|
||||
check(a.maxScrollAmount()>0,"Gallery scrolls");s.mouseScrolled(a.getX()+5,a.getY()+5,0,-4);check(a.scrollAmount()>0,"Wheel moves screenshots");a.setScrollAmount(0);
|
||||
if(a.maxScrollAmount()>0){s.mouseScrolled(a.getX()+5,a.getY()+5,0,-4);check(a.scrollAmount()>0,"Wheel moves screenshots");a.setScrollAmount(0);}
|
||||
var photos=widgets(s).filter(CommunityPhotoWidget.class::isInstance).map(CommunityPhotoWidget.class::cast).toList();
|
||||
check(photos.stream().mapToInt(AbstractWidget::getRight).max().orElseThrow()>s.width*0.8,"Gallery uses GUI width");
|
||||
for(var child:s.children())if(child instanceof AbstractWidget w)check(w.getX()>=0&&w.getRight()<=s.width&&w.getY()>=0&&w.getBottom()<=s.height,"Gallery bounds "+w.getMessage());
|
||||
});
|
||||
c.takeScreenshot("cards158-gallery-"+locale+"-"+scale+(database?"-database":"-file"));
|
||||
c.takeScreenshot("search159-gallery-"+locale+"-"+scale+"-"+windowWidth+(database?"-database":"-file"));
|
||||
c.runOnClient(m->widgets(m.gui.screen()).filter(CommunityPhotoWidget.class::isInstance).map(CommunityPhotoWidget.class::cast).findFirst().orElseThrow().onPress(enter()));
|
||||
c.runOnClient(m->{var s=m.gui.screen();var image=widgets(s).filter(CommunityPhotoWidget.class::isInstance).map(CommunityPhotoWidget.class::cast).findFirst().orElseThrow();check(image.getWidth()>s.width*0.8&&image.getHeight()>s.height*0.5,"Preview fills available GUI");check(button(s,"photo_assign").getBottom()<s.height-30,"Preview controls remain visible");});
|
||||
c.takeScreenshot("search159-preview-"+locale+"-"+scale+"-"+windowWidth);
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.screenshots_back"));
|
||||
c.runOnClient(m->m.gui.screen().onClose());
|
||||
c.runOnClient(m->{check(edit(m.gui.screen()).getValue().equals("Une journée au village"),"Cancel retains text");check(!button(m.gui.screen(),"publish").active,"Cancel leaves photo unassigned");});
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());press(m.gui.screen(),"sanctuary.community.photo_choose");});
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(2);m.resizeGui();m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());press(m.gui.screen(),"sanctuary.community.photo_choose");});
|
||||
c.waitFor(m->widgets(m.gui.screen()).anyMatch(CommunityPhotoWidget.class::isInstance),500);
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.next"));
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityPhotoWidget.class::isInstance).count()>0&&!button(m.gui.screen(),"next").active,500);
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityPhotoWidget.class::isInstance).count()>0&&button(m.gui.screen(),"previous").active,500);
|
||||
c.runOnClient(m->widgets(m.gui.screen()).filter(CommunityPhotoWidget.class::isInstance).map(CommunityPhotoWidget.class::cast).findFirst().orElseThrow().onPress(enter()));
|
||||
c.takeScreenshot("cards158-preview");
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.photo_assign"));
|
||||
c.runOnClient(m->{check(button(m.gui.screen(),"publish").active,"Photo enables publishing");check(edit(m.gui.screen()).getValue().equals("Une journée au village"),"Selection retains draft");press(m.gui.screen(),"sanctuary.community.publish");});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.reply")&&!CommunityClient.pending(),600);
|
||||
c.runOnClient(m->{var article=widgets(m.gui.screen()).filter(CommunityArticle.class::isInstance).map(CommunityArticle.class::cast).findFirst().orElseThrow();var face=widgets(m.gui.screen()).filter(PlayerFaceWidget.class::isInstance).map(PlayerFaceWidget.class::cast).findFirst().orElseThrow();check(face.getY()>=article.getBottom(),"Author metadata below the article");check(button(m.gui.screen(),"actions").getY()==face.getY(),"Author metadata next to ellipsis");});
|
||||
c.runOnClient(m->{var article=widgets(m.gui.screen()).filter(CommunityArticle.class::isInstance).map(CommunityArticle.class::cast).findFirst().orElseThrow();var face=widgets(m.gui.screen()).filter(PlayerFaceWidget.class::isInstance).map(PlayerFaceWidget.class::cast).findFirst().orElseThrow();check(face.getY()>=article.getBottom(),"Author metadata below the article");check(button(m.gui.screen(),"actions").getY()==face.getY(),"Author metadata next to ellipsis");check(widgets(m.gui.screen()).anyMatch(w->w instanceof AbstractWidget widget && widget.getMessage().getString().contains("/"+java.time.Year.now()+" ")),"Article date includes year");});
|
||||
c.takeScreenshot("cards158-article"+(database?"-database":"-file"));
|
||||
world.getServer().runOnServer(s->{try {
|
||||
var config=CommunityConfig.load(net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json"));
|
||||
@@ -92,7 +107,7 @@ public final class Community158ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->{var screen=m.gui.screen();var cards=widgets(screen).filter(CommunityCard.class::isInstance).map(CommunityCard.class::cast).toList();
|
||||
check(cards.get(0).getY()==cards.get(1).getY()&&cards.get(0).getRight()<cards.get(1).getX(),"Two-column grid");
|
||||
check(cards.get(2).getY()>cards.get(0).getY(),"Rows continue vertically");
|
||||
check(!has(screen,"sanctuary.community.link")&&!has(screen,"sanctuary.community.bulletin"),"Public navigation excludes linking and bulletin");
|
||||
check(!has(screen,"sanctuary.community.link")&&has(screen,"sanctuary.community.server_tab"),"Public navigation restores read-only server history");
|
||||
check(widgets(screen).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).anyMatch(a->a.maxScrollAmount()>0),"Grid scrolls");
|
||||
});
|
||||
c.takeScreenshot("cards158-grid-"+kind+"-"+locale+"-"+scale+(database?"-database":"-file"));
|
||||
@@ -100,6 +115,24 @@ public final class Community158ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->{var a=widgets(m.gui.screen()).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).findFirst().orElseThrow();a.setScrollAmount(a.maxScrollAmount());press(m.gui.screen(),"sanctuary.community.more");});
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).count()==13&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->check(widgets(m.gui.screen()).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).anyMatch(a->a.scrollAmount()>0),"Appending cards retains scroll"));
|
||||
c.runOnClient(m->{CommunityClient.open(new PauseScreen(true),"article","",false);edit(m.gui.screen()).setValue("unique159");});
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).count()==1&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->{check(text(m.gui.screen(),"Gazette 0"),"Search finds body beyond first page");edit(m.gui.screen()).setValue("VoISin");});
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).count()==10&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.more"));
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).count()==13&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->{edit(m.gui.screen()).setValue("absent159");m.gui.screen().setFocused(edit(m.gui.screen()));for(int tick=0;tick<12;tick++)m.gui.screen().tick();check(CommunityClient.pending(),"Old search is in flight");edit(m.gui.screen()).setValue("Gazette 12");});
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).count()==1&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->{check(text(m.gui.screen(),"Gazette 12"),"Latest search wins over prior pending request");check(edit(m.gui.screen()).isFocused(),"Search retains keyboard focus");});
|
||||
c.takeScreenshot("search159-filtered");
|
||||
c.runOnClient(m->edit(m.gui.screen()).setValue(""));
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).count()==10&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.server_tab"));
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).count()==1&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->{check(!has(m.gui.screen(),"sanctuary.community.publish"),"Public server listing is read only");widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).map(CommunityCard.class::cast).findFirst().orElseThrow().onPress(enter());});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.back")&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->{check(!has(m.gui.screen(),"sanctuary.community.actions")&&!has(m.gui.screen(),"sanctuary.community.reply"),"Public bulletin has no write actions");check(text(m.gui.screen(),"Annonce officielle159"),"Server bulletin readable by ordinary player");});
|
||||
c.takeScreenshot("search159-server-readonly");
|
||||
world.getServer().runOnServer(server->{var inventory=server.getPlayerList().getPlayers().getFirst().getInventory();inventory.setItem(6,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.OAK_LOG,17));inventory.setItem(7,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIAMOND,3));});
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();CommunityClient.open(new PauseScreen(true),"notice","",true);edit(m.gui.screen()).setValue("Le coffre descriptif");body(m.gui.screen()).setValue("Du bois pour le pont");press(m.gui.screen(),"sanctuary.community.task_configure");press(m.gui.screen(),"sanctuary.community.location_current");press(m.gui.screen(),"sanctuary.community.materials");edit(m.gui.screen()).setValue("minecraft:oak_log");});
|
||||
c.waitTicks(3);
|
||||
@@ -115,6 +148,33 @@ public final class Community158ClientChecks implements FabricClientGameTest {
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.unfollow")&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->check(NotificationClient.tracked().stream().anyMatch(t->t.title().getString().equals("Demande 0")),"Server-backed quest appears in HUD"));
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(5);c.takeScreenshot("cards158-follow-hud");
|
||||
c.runOnClient(m->{CommunityClient.open(new PauseScreen(true),"notice",noticeId,false);});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.map_show")&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.map_show"));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen a&&!a.questPins().isEmpty(),200);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();var pin=a.questPins().getFirst();check(pin.marker().id().equals(noticeId),"Followed location appears on atlas");check(CommunityMapMarkers.project("minecraft:the_nether",x->100,z->100,0,0,300,300).isEmpty(),"Other dimension has no pin");
|
||||
var tooltip=new StringBuilder();for(var line:CommunityMapMarkers.tooltip(pin.marker())){line.accept((index,style,point)->{tooltip.appendCodePoint(point);return true;});tooltip.append(' ');}
|
||||
check(tooltip.toString().contains("Voisin")&&tooltip.toString().contains("128")&&tooltip.toString().contains("Un repas")&&tooltip.toString().contains("Rendez-vous"),"Hover includes author, summary, materials and reward");
|
||||
hover(m,pin.x(),pin.y());
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("search159-map-hover"+(database?"-database":"-file"));
|
||||
for(int scale:List.of(3,4)) {
|
||||
c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();var pin=((SanctuaryAtlasScreen)m.gui.screen()).questPins().getFirst();hover(m,pin.x(),pin.y());});
|
||||
c.waitTicks(3);c.takeScreenshot("search159-map-hover-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();var pin=a.questPins().getFirst();check(a.mouseClicked(new net.minecraft.client.input.MouseButtonEvent(pin.x(),pin.y(),new net.minecraft.client.input.MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_LEFT,0)),false),"Quest marker click handled");});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.unfollow")&&!CommunityClient.pending(),300);
|
||||
world.getServer().runOnServer(server->server.getPlayerList().getPlayers().getFirst().teleportTo(20.5,65,20.5));
|
||||
c.waitTicks(5);c.runOnClient(m->m.gui.setScreen(new PauseScreen(true)));c.waitTicks(20);
|
||||
c.runOnClient(m->{var map=((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap();var pin=map.questPins().getFirst();check(pin.marker().id().equals(noticeId),"Pause map includes followed location");hover(m,pin.x(),pin.y());});
|
||||
c.waitTicks(3);c.takeScreenshot("search159-pause-map-hover"+(database?"-database":"-file"));
|
||||
c.runOnClient(m->{var map=((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap();var pin=map.questPins().getFirst();map.mouseClicked(new net.minecraft.client.input.MouseButtonEvent(pin.x(),pin.y(),new net.minecraft.client.input.MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_LEFT,0)),false);});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.unfollow")&&!CommunityClient.pending(),300);c.waitTicks(25);
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.unfollow"));
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.follow")&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->check(CommunityMapMarkers.project("minecraft:overworld",x->100,z->100,0,0,300,300).isEmpty(),"Unfollowing removes the map marker immediately"));
|
||||
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
c.runOnClient(m->m.getConnection().sendCommand("sanctuary community admin"));c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.gui.screen()==null,"Non-operator cannot open administration"));
|
||||
@@ -124,9 +184,29 @@ public final class Community158ClientChecks implements FabricClientGameTest {
|
||||
c.waitFor(m->m.gui.screen() instanceof CommunityScreen&&has(m.gui.screen(),"sanctuary.community.publish")&&!CommunityClient.pending(),200);
|
||||
c.runOnClient(m->{press(m.gui.screen(),"sanctuary.community.publish");edit(m.gui.screen()).setValue("Message officiel");body(m.gui.screen()).setValue("Bienvenue !");press(m.gui.screen(),"sanctuary.community.publish");});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.back")&&!CommunityClient.pending(),250);
|
||||
c.runOnClient(m->{check(!has(m.gui.screen(),"sanctuary.community.follow")&&!has(m.gui.screen(),"sanctuary.community.reply"),"Administrative bulletin has no quest or replies");m.gui.setScreen(null);});
|
||||
System.out.println("CARDS158_CLIENT_PASS mode="+(database?"database":"file"));
|
||||
c.runOnClient(m->{check(!has(m.gui.screen(),"sanctuary.community.follow")&&!has(m.gui.screen(),"sanctuary.community.reply"),"Administrative bulletin has no quest or replies");CommunityClient.open(null,"bulletin","",false);});
|
||||
c.waitFor(m->widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).count()==2&&!CommunityClient.pending(),250);
|
||||
c.runOnClient(m->{check(!has(m.gui.screen(),"sanctuary.community.publish"),"Public bulletin remains read only for operators");widgets(m.gui.screen()).filter(CommunityCard.class::isInstance).map(CommunityCard.class::cast).findFirst().orElseThrow().onPress(enter());});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.back")&&!CommunityClient.pending(),250);
|
||||
c.runOnClient(m->{check(!has(m.gui.screen(),"sanctuary.community.actions"),"Operator uses administration to edit official messages");m.gui.setScreen(new PauseScreen(true));});
|
||||
c.waitFor(m->CommunityClient.overview.has("bulletins")&&!CommunityClient.pending(),200);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
|
||||
c.waitTicks(20);
|
||||
c.runOnClient(m->{
|
||||
var date=widgets(m.gui.screen()).filter(StringWidget.class::isInstance).map(StringWidget.class::cast).filter(w->w.getMessage().getString().endsWith(java.time.Year.now().toString())).findFirst().orElseThrow();
|
||||
var official=widgets(m.gui.screen()).filter(PauseTextButton.class::isInstance).map(PauseTextButton.class::cast).filter(w->w.getMessage().getString().contains("Message officiel")).findFirst().orElseThrow();
|
||||
check(date.getRight()<official.getX(),"Full date and server message do not overlap");
|
||||
});
|
||||
c.takeScreenshot("search159-pause-year-"+locale+"-"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
System.out.println("SEARCH159_CLIENT_PASS mode="+(database?"database":"file"));
|
||||
} finally {intro.set(false);}
|
||||
}
|
||||
private static void hover(net.minecraft.client.Minecraft m,double x,double y) {
|
||||
var w=m.getWindow();double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();
|
||||
m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();
|
||||
}
|
||||
private static AbstractButton button(Screen screen,String key) {return widgets(screen).filter(AbstractButton.class::isInstance).map(AbstractButton.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.community."+key)).findFirst().orElseThrow();}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ final class CommunityCard extends AbstractButton implements AutoCloseable {
|
||||
CommunityCard(int width,JsonObject entry,boolean compact,Runnable action) {
|
||||
super(0,0,width,compact?44:90,Component.literal(str(entry,"title")));this.entry=entry;this.compact=compact;this.action=action;article=str(entry,"kind").equals("article");
|
||||
face=new PlayerFaceWidget(compact?20:24,ResolvableProfile.createUnresolved(UUID.fromString(str(entry,"authorId"))));
|
||||
setTooltip(Tooltip.create(getMessage().copy().append("\n").append(str(entry,"authorName"))));
|
||||
setTooltip(Tooltip.create(getMessage().copy().append("\n").append(str(entry,"authorName")).append("\n").append(CommunityScreen.date(entry))));
|
||||
if(article&&!compact) {
|
||||
height=width*9/16+78;
|
||||
try {
|
||||
@@ -51,7 +51,7 @@ final class CommunityCard extends AbstractButton implements AutoCloseable {
|
||||
if(compact)g.text(font,font.plainSubstrByWidth(text("category."+str(entry,"category")).append(" · ").append(text(str(entry,"status"))).getString(),width-14),x+7,y+31,accent,false);
|
||||
else {
|
||||
g.text(font,font.plainSubstrByWidth(str(entry,"authorName"),width-14),x+7,top+30,0xffd6d4c9,false);
|
||||
g.text(font,CommunityScreen.date(entry),x+7,top+42,0xffa2aaa4,false);
|
||||
g.text(font,font.plainSubstrByWidth(CommunityScreen.date(entry),width-14),x+7,top+42,0xffa2aaa4,false);
|
||||
if(!article)g.text(font,font.plainSubstrByWidth(text("category."+str(entry,"category")).append(" · ").append(text(str(entry,"status"))).getString(),width-14),x+7,top+59,accent,false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,13 +32,13 @@ public final class CommunityClient {
|
||||
}));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((h,c) -> { pending = 0; owner = null; callback = null; overview = new JsonObject(); stale = true; overviewError = ""; DRAFTS.clear(); });
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
if(++followTicks%100==0 && pending==0 && client.player!=null && client.gui.screen()==null && available())request(null,"following","","","notice","info","","",0,0,r->{});
|
||||
if(++followTicks%100==0 && pending==0 && client.player!=null && (client.gui.screen()==null || client.gui.screen() instanceof SanctuaryAtlasScreen || client.gui.screen() instanceof PauseAtlasAccess) && available())request(null,"following","","","notice","info","","",0,0,r->{});
|
||||
if (pending != 0 && ++ticks > 200) { var result = new JsonObject(); result.addProperty("error","unavailable"); finish(result); }
|
||||
});
|
||||
}
|
||||
private static void finish(JsonObject result) {
|
||||
pending = 0;
|
||||
if(result.has("following"))NotificationClient.community(result.getAsJsonArray("following"));
|
||||
if(result.has("following")){NotificationClient.community(result.getAsJsonArray("following"));CommunityMapMarkers.accept(result.getAsJsonArray("following"));}
|
||||
if (overviewRequest) {
|
||||
if (!result.has("error")) { overview = result; stale = false; overviewError = ""; }
|
||||
else { stale = true; overviewError = str(result,"error"); }
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import fr.koka.sanctuary.community.CommunityTask;
|
||||
import java.util.*;
|
||||
import java.util.function.DoubleToIntFunction;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Locations deliberately shared by followed notices; these never reveal terrain or generate chunks. */
|
||||
final class CommunityMapMarkers {
|
||||
record Marker(String id,String title,String author,String summary,CommunityTask task,int color) { }
|
||||
record Pin(Marker marker,int x,int y) {
|
||||
boolean contains(double mx,double my){return Math.abs(mx-x)<=7 && Math.abs(my-y)<=8;}
|
||||
}
|
||||
private static List<Marker> markers=List.of();
|
||||
private CommunityMapMarkers() { }
|
||||
static void clear(){markers=List.of();}
|
||||
static void accept(JsonArray entries) {
|
||||
var next=new ArrayList<Marker>();
|
||||
for(var value:entries) {
|
||||
var e=value.getAsJsonObject();
|
||||
if(!CommunityClient.str(e,"status").equals("open") || !e.has("task") || e.get("task").isJsonNull())continue;
|
||||
var task=new com.google.gson.Gson().fromJson(e.get("task"),CommunityTask.class);
|
||||
if(task.location()!=null)next.add(new Marker(CommunityClient.str(e,"id"),CommunityClient.str(e,"title"),CommunityClient.str(e,"authorName"),CommunityClient.str(e,"summary"),task,CommunityCard.color(CommunityClient.str(e,"category"))));
|
||||
}
|
||||
markers=List.copyOf(next);
|
||||
}
|
||||
static List<Pin> project(String dimension,DoubleToIntFunction screenX,DoubleToIntFunction screenZ,int left,int top,int right,int bottom) {
|
||||
var pins=new ArrayList<Pin>();
|
||||
for(var marker:markers) {
|
||||
var location=marker.task().location();if(!location.dimension().equals(dimension))continue;
|
||||
int x=screenX.applyAsInt(location.x()+.5),y=screenZ.applyAsInt(location.z()+.5);
|
||||
if(x>=left+7 && x<right-7 && y>=top+8 && y<bottom-8)pins.add(new Pin(marker,x,y));
|
||||
}
|
||||
return List.copyOf(pins);
|
||||
}
|
||||
static Pin at(List<Pin> pins,double x,double y){return pins.stream().filter(p->p.contains(x,y)).findFirst().orElse(null);}
|
||||
static boolean render(GuiGraphicsExtractor g,List<Pin> pins,int mouseX,int mouseY) {
|
||||
var font=Minecraft.getInstance().font;
|
||||
for(var pin:pins) {
|
||||
int x=pin.x(),y=pin.y();
|
||||
g.fill(x-7,y-8,x+8,y+9,pin.contains(mouseX,mouseY)?0xfff4e7b4:pin.marker().color());
|
||||
g.fill(x-6,y-7,x+7,y+8,0xff20251e);
|
||||
g.fill(x-1,y-5,x+2,y+2,0xffffd35a);
|
||||
g.fill(x-1,y+4,x+2,y+6,0xffffd35a);
|
||||
}
|
||||
var hovered=at(pins,mouseX,mouseY);
|
||||
if(hovered==null)return false;
|
||||
g.setTooltipForNextFrame(font,tooltip(hovered.marker()),mouseX,mouseY);
|
||||
return true;
|
||||
}
|
||||
static List<net.minecraft.util.FormattedCharSequence> tooltip(Marker marker) {
|
||||
var client=Minecraft.getInstance();var font=client.font;var task=marker.task();var location=task.location();
|
||||
int width=Math.min(230,client.getWindow().getGuiScaledWidth()-28);
|
||||
var lines=new ArrayList<net.minecraft.util.FormattedCharSequence>();
|
||||
lines.addAll(font.split(Component.literal(marker.title()).withStyle(net.minecraft.ChatFormatting.BOLD),width).stream().limit(2).toList());
|
||||
lines.add(Component.literal(font.plainSubstrByWidth(marker.author(),width)).getVisualOrderText());
|
||||
lines.addAll(font.split(Component.literal(marker.summary()),width).stream().limit(3).toList());
|
||||
lines.addAll(font.split(Component.literal("X "+location.x()+" Y "+location.y()+" Z "+location.z()),width));
|
||||
if(!task.materials().isEmpty()) {
|
||||
lines.add(CommunityClient.text("materials").getVisualOrderText());
|
||||
itemLines(lines,task.materials(),width,2);
|
||||
}
|
||||
lines.addAll(font.split(CommunityClient.text("reward."+task.reward()),width));
|
||||
if(task.reward().equals("items"))itemLines(lines,task.rewards(),width,2);
|
||||
if(task.reward().equals("custom"))lines.addAll(font.split(Component.literal(task.customReward()),width).stream().limit(2).toList());
|
||||
lines.addAll(font.split(CommunityClient.text("audience."+task.audience()),width));
|
||||
int limit=Math.max(5,(client.getWindow().getGuiScaledHeight()-34)/font.lineHeight-2);
|
||||
if(lines.size()>limit){lines=new ArrayList<>(lines.subList(0,limit-1));lines.add(Component.literal("…").getVisualOrderText());}
|
||||
lines.addAll(font.split(CommunityClient.text("map_followed"),width));
|
||||
return List.copyOf(lines);
|
||||
}
|
||||
private static void itemLines(List<net.minecraft.util.FormattedCharSequence> lines,List<CommunityTask.Material> items,int width,int limit) {
|
||||
var font=Minecraft.getInstance().font;
|
||||
for(var item:items.stream().limit(limit).toList()) {
|
||||
var name=net.minecraft.core.registries.BuiltInRegistries.ITEM.getOptional(net.minecraft.resources.Identifier.parse(item.item()))
|
||||
.map(i->i.getDefaultInstance().getHoverName()).orElse(Component.literal(item.item()));
|
||||
lines.addAll(font.split(Component.literal(item.count()+" × ").append(name),width));
|
||||
}
|
||||
if(items.size()>limit)lines.addAll(font.split(CommunityClient.text("map_more_materials",items.size()-limit),width));
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public final class CommunityPausePanels implements AutoCloseable {
|
||||
clock=new StringWidget(rightX,8,right,20,Component.empty(),font);
|
||||
add.accept(date);add.accept(clock);
|
||||
int headerSide=Math.max(84,screen.width/5);
|
||||
bulletin=new PauseTextButton(headerSide,3,screen.width-2*headerSide,32,text("bulletin"),()->{});
|
||||
bulletin=new PauseTextButton(headerSide,3,screen.width-2*headerSide,32,text("bulletin"),()->open(screen,"bulletin","",false));
|
||||
add.accept(bulletin);
|
||||
String[] sections={"blocks","history","progress","identity"};
|
||||
int optionsTop=screen.height-76;
|
||||
@@ -64,7 +64,7 @@ public final class CommunityPausePanels implements AutoCloseable {
|
||||
private void updateHeader() {
|
||||
var locale=Locale.forLanguageTag(Minecraft.getInstance().getLanguageManager().getSelected().replace('_','-'));
|
||||
var now=ZonedDateTime.now();
|
||||
date.setMessage(Component.literal(DateTimeFormatter.ofPattern("d MMMM",locale).format(now)));
|
||||
date.setMessage(Component.literal(DateTimeFormatter.ofPattern("d MMMM yyyy",locale).format(now)));
|
||||
clock.setMessage(Component.literal(DateTimeFormatter.ofPattern("HH:mm",locale).format(now)));
|
||||
clock.setX(screen.width-8-Minecraft.getInstance().font.width(clock.getMessage()));clock.setWidth(Minecraft.getInstance().font.width(clock.getMessage()));
|
||||
var list=items("bulletins");
|
||||
@@ -72,7 +72,10 @@ public final class CommunityPausePanels implements AutoCloseable {
|
||||
if(!overviewError.isEmpty())message=text(overviewError);
|
||||
bulletin.setMessage(message);
|
||||
bulletin.setTooltip(Tooltip.create(text("bulletin").append("\n").append(list.isEmpty()?message:Component.literal(str(list.get(0).getAsJsonObject(),"body")))));
|
||||
bulletin.active=false;
|
||||
date.setWidth(Minecraft.getInstance().font.width(date.getMessage()));
|
||||
int left=Math.max(date.getRight()+8,screen.width/5),right=screen.width-Math.max(56,screen.width/5);
|
||||
bulletin.setRectangle(Math.max(24,right-left),32,left,3);
|
||||
bulletin.active=available();
|
||||
}
|
||||
public void tick() {
|
||||
if(closed)return;
|
||||
|
||||
@@ -12,6 +12,11 @@ import static fr.koka.sanctuary.client.CommunityClient.*;
|
||||
/** Native community pages: user content is always a literal Minecraft component. */
|
||||
public final class CommunityScreen extends BlocodexFrame {
|
||||
private final String kind;
|
||||
private final boolean admin;
|
||||
private String search="";
|
||||
private int searchAfter;
|
||||
private boolean rebuildSearch;
|
||||
private EditBox searchBox;
|
||||
private String id, editingId = "", message = "", expandedId = "";
|
||||
private int page, revision;
|
||||
private boolean composing, needsLoad = true, moderator;
|
||||
@@ -36,12 +41,19 @@ public final class CommunityScreen extends BlocodexFrame {
|
||||
}
|
||||
static CommunityScreen admin(Screen parent){return new CommunityScreen(parent,"bulletin","",false,true);}
|
||||
private CommunityScreen(Screen parent,String kind,String id,boolean compose,boolean admin) {
|
||||
super(parent); this.kind = kind.equals("bulletin")&&!admin?"article":kind; this.id = id; this.composing = compose;
|
||||
super(parent); this.kind = kind; this.admin=admin; this.id = id; this.composing = compose && (!kind.equals("bulletin") || admin);
|
||||
moderator = flag(overview,"moderator");
|
||||
if (compose) useDraft("new:" + kind);
|
||||
}
|
||||
private void useDraft(String key) { draft = DRAFTS.computeIfAbsent(key, unused -> new Draft()); }
|
||||
@Override protected void rebuildWidgets() {
|
||||
boolean restoreFocus=searchBox!=null && searchBox.isFocused() && !composing && id.isEmpty();
|
||||
int cursor=restoreFocus?searchBox.getCursorPosition():0;
|
||||
super.rebuildWidgets();
|
||||
if(restoreFocus && searchBox!=null){setInitialFocus(searchBox);searchBox.moveCursorTo(cursor,false);}
|
||||
}
|
||||
@Override protected void init() {
|
||||
searchBox=null;
|
||||
closePhotos();initFrame("history",text(kind)); contentX=(width-contentWidth)/2; int w = contentWidth - 28;
|
||||
var layout = LinearLayout.vertical().spacing(8);
|
||||
if (!message.isEmpty()) line(layout,w,text(message));
|
||||
@@ -55,9 +67,12 @@ public final class CommunityScreen extends BlocodexFrame {
|
||||
}
|
||||
private void listing(LinearLayout layout,int w) {
|
||||
var tabs=layout.addChild(LinearLayout.horizontal().spacing(6));
|
||||
if(!kind.equals("bulletin"))for(String target:new String[]{"article","notice"})tabs.addChild(Button.builder(text(target),b->open(parent,target,"",false)).width((w-6)/2).build());
|
||||
if(!admin)for(String target:new String[]{"article","notice","bulletin"})tabs.addChild(Button.builder(text(target.equals("bulletin")?"server_tab":target.equals("notice")?"notice_tab":target),b->open(parent,target,"",false)).width((w-12)/3).build());
|
||||
searchBox=layout.addChild(new EditBox(font,w,20,text("search")));
|
||||
searchBox.setMaxLength(120);searchBox.setHint(text("search"));searchBox.setValue(search);
|
||||
searchBox.setResponder(value->{search=value;page=0;more=false;entries.clear();restoreScroll=0;needsLoad=true;searchAfter=ticks+6;rebuildSearch=true;});
|
||||
var toolbar=layout.addChild(LinearLayout.horizontal().spacing(6));
|
||||
button(toolbar,(w-6)/2,"publish",()->{composing=true;useDraft("new:"+kind);rebuildWidgets();});
|
||||
if(!kind.equals("bulletin") || admin)button(toolbar,(w-6)/2,"publish",()->{composing=true;useDraft("new:"+kind);rebuildWidgets();});
|
||||
button(toolbar,(w-6)/2,"refresh",()->{page=0;needsLoad=true;});
|
||||
if(entries.isEmpty())line(layout,w,text(needsLoad?"loading":"empty"));
|
||||
int cardWidth=(w-8)/2;
|
||||
@@ -122,11 +137,12 @@ public final class CommunityScreen extends BlocodexFrame {
|
||||
}).size(20,18).tooltip(Tooltip.create(text("actions_for",str(entry,"authorName")))).build());
|
||||
}
|
||||
private void message(LinearLayout layout,int w,JsonObject entry) {
|
||||
author(layout,w,entry,true);
|
||||
author(layout,w,entry,!kind.equals("bulletin") || admin);
|
||||
layout.addChild(new CommunityBubble(w,str(entry,"body"),own(entry)));
|
||||
if(expandedId.equals(str(entry,"id"))) controls(layout,w,entry);
|
||||
}
|
||||
private void controls(LinearLayout layout,int w,JsonObject entry) {
|
||||
if(kind.equals("bulletin") && !admin)return;
|
||||
boolean own = own(entry);
|
||||
var row=layout.addChild(LinearLayout.horizontal().spacing(4)); int size=Math.min(100,(w-12)/4);
|
||||
if (own) button(row,size,"edit",() -> {
|
||||
@@ -193,7 +209,12 @@ public final class CommunityScreen extends BlocodexFrame {
|
||||
}
|
||||
private void taskDetails(LinearLayout layout,int w,JsonObject entry) {
|
||||
var task=readTask(entry);
|
||||
if(task.location()!=null)line(layout,w,text("location_value",task.location().dimension(),task.location().x(),task.location().y(),task.location().z()));
|
||||
if(task.location()!=null) {
|
||||
line(layout,w,text("location_value",task.location().dimension(),task.location().x(),task.location().y(),task.location().z()));
|
||||
var map=layout.addChild(Button.builder(text("map_show"),b->minecraft.gui.setScreen(new SanctuaryAtlasScreen(this,task.location()))).width(Math.min(w,220)).build());
|
||||
map.active=minecraft.level!=null && task.location().dimension().equals(minecraft.level.dimension().identifier().toString());
|
||||
if(!map.active)map.setTooltip(Tooltip.create(text("map_other_dimension")));
|
||||
}
|
||||
if(!task.materials().isEmpty()) {line(layout,w,text("materials"));CommunityMaterialsScreen.showItems(layout,w,task.materials());}
|
||||
line(layout,w,text("audience."+task.audience()));line(layout,w,text("reward."+task.reward()));
|
||||
if(task.reward().equals("items"))CommunityMaterialsScreen.showItems(layout,w,task.rewards());
|
||||
@@ -229,9 +250,10 @@ public final class CommunityScreen extends BlocodexFrame {
|
||||
private void button(LinearLayout layout,int w,String key,Runnable action) {
|
||||
layout.addChild(Button.builder(text(key),b -> { if (!publishing) action.run(); }).width(w).build());
|
||||
}
|
||||
static String date(JsonObject e) { return DateTimeFormatter.ofPattern("dd/MM HH:mm 'UTC'").withZone(ZoneOffset.UTC).format(Instant.ofEpochMilli(e.get("createdAt").getAsLong())); }
|
||||
static String date(JsonObject e) { return DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm 'UTC'").withZone(ZoneOffset.UTC).format(Instant.ofEpochMilli(e.get("createdAt").getAsLong())); }
|
||||
@Override public void tick() {
|
||||
super.tick(); ticks++;
|
||||
if(rebuildSearch){rebuildSearch=false;rebuildWidgets();}
|
||||
if(publishing && !pending() && ticks>=sendAfter) {
|
||||
if(!taskSent) {
|
||||
request(this,"task",editingId.isEmpty()?draft.operation:editingId,"","notice","info","",new GsonBuilder().disableHtmlEscaping().create().toJson(draft.task),0,0,result->{
|
||||
@@ -248,7 +270,9 @@ public final class CommunityScreen extends BlocodexFrame {
|
||||
});
|
||||
} else if(commit!=null) {var actionToRun=commit;commit=null;actionToRun.run();}
|
||||
}
|
||||
if (!composing && needsLoad && ticks%4==0 && request(this,id.isEmpty()?(kind.equals("bulletin")?"list":"cards"):"thread",id,"",kind,"info","","",0,page,result -> {
|
||||
String requestedSearch=search,requestedId=id;int requestedPage=page;
|
||||
if (!composing && needsLoad && ticks>=searchAfter && ticks%4==0 && request(this,id.isEmpty()?(kind.equals("bulletin")?"list":"cards"):"thread",id,"",kind,"info","",id.isEmpty()?search:"",0,page,result -> {
|
||||
if(!search.equals(requestedSearch) || !id.equals(requestedId) || page!=requestedPage || composing)return;
|
||||
if (result.has("error")) message=str(result,"error");
|
||||
else {data=result;moderator=flag(result,"moderator");
|
||||
if(id.isEmpty()&&result.has("page")) {
|
||||
|
||||
+5
-5
@@ -61,7 +61,7 @@ public final class CommunityScreenshotsScreen extends BlocodexFrame {
|
||||
}
|
||||
@Override protected void init() {
|
||||
closeTextures();initFrame("history",text("screenshots"));
|
||||
contentWidth=Math.min(420,contentWidth);contentX=(width-contentWidth)/2;
|
||||
contentWidth=Math.max(240,width-24);contentX=(width-contentWidth)/2;
|
||||
if(!loaded)load(true);
|
||||
if(selected!=null) {preview();return;}
|
||||
int w=contentWidth-24;
|
||||
@@ -70,12 +70,12 @@ public final class CommunityScreenshotsScreen extends BlocodexFrame {
|
||||
controls.addChild(new StringWidget(Math.max(30,w-228),20,text("page",page+1),font));
|
||||
controls.addChild(Button.builder(text("next"),b->{page++;load(false);rebuildWidgets();}).width(72).build()).active=(page+1)*PAGE_SIZE<paths.size() && loading==null;
|
||||
controls.addChild(Button.builder(text("refresh"),b->{page=0;load(true);rebuildWidgets();}).width(76).build()).active=loading==null;
|
||||
controls.setPosition(contentX+8,contentTop+Math.max(24,Math.min(180,contentBottom-contentTop-28))+8);controls.arrangeElements();controls.visitWidgets(this::addRenderableWidget);
|
||||
controls.setPosition(contentX+8,contentTop+Math.max(24,contentBottom-contentTop-28)+8);controls.arrangeElements();controls.visitWidgets(this::addRenderableWidget);
|
||||
var layout=LinearLayout.vertical().spacing(8);
|
||||
if(loading!=null)layout.addChild(new StringWidget(text("loading"),font));
|
||||
else if(!error.isEmpty() || shots.isEmpty())layout.addChild(new MultiLineTextWidget(text(error.isEmpty()?"screenshots_empty":error),font).setMaxWidth(w));
|
||||
else {
|
||||
int columns=w>=360?4:3,cell=(w-(columns-1)*8)/columns;
|
||||
int columns=Math.max(2,w/112),cell=(w-(columns-1)*8)/columns;
|
||||
for(int i=0;i<shots.size();i+=columns) {
|
||||
var row=layout.addChild(LinearLayout.horizontal().spacing(8));
|
||||
for(int j=i;j<Math.min(i+columns,shots.size());j++) {
|
||||
@@ -85,11 +85,11 @@ public final class CommunityScreenshotsScreen extends BlocodexFrame {
|
||||
}
|
||||
}
|
||||
}
|
||||
var scroll=new ScrollableLayout(minecraft,layout,Math.max(24,Math.min(180,contentBottom-contentTop-28)),ScrollableLayout.ReserveStrategy.RIGHT);
|
||||
var scroll=new ScrollableLayout(minecraft,layout,Math.max(24,contentBottom-contentTop-28),ScrollableLayout.ReserveStrategy.RIGHT);
|
||||
scroll.setPosition(contentX+8,contentTop);scroll.arrangeElements();scroll.visitWidgets(this::addRenderableWidget);
|
||||
}
|
||||
private void preview() {
|
||||
int w=Math.min(260,contentWidth-16),h=Math.max(24,Math.min(146,contentBottom-contentTop-34));
|
||||
int w=contentWidth-16,h=Math.max(24,contentBottom-contentTop-34);
|
||||
int left=(width-w)/2;
|
||||
try {
|
||||
var image=CommunityPhoto.decode(Base64.getDecoder().decode(selected.photo()),CommunityPhoto.WIDTH,CommunityPhoto.HEIGHT);
|
||||
|
||||
@@ -41,7 +41,7 @@ public final class NotificationClient {
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((h,m)->reset());
|
||||
ClientTickEvents.END_CLIENT_TICK.register(NotificationClient::tick);
|
||||
}
|
||||
public static void reset(){communityTracks=List.of();FEED.clear();OBJECTIVES.reset();inhabitant=null;trackingScope=null;hasRoom=true;recipes=null;blocks=totalBlocks=totalItems=totalRecipes=ticks=0;lastClock=0;animatedBlocks=animatedItems=animatedRecipes=0;}
|
||||
public static void reset(){CommunityMapMarkers.clear();communityTracks=List.of();FEED.clear();OBJECTIVES.reset();inhabitant=null;trackingScope=null;hasRoom=true;recipes=null;blocks=totalBlocks=totalItems=totalRecipes=ticks=0;lastClock=0;animatedBlocks=animatedItems=animatedRecipes=0;}
|
||||
public static void clearFeed(){FEED.clear();}
|
||||
public static List<NotificationFeed.Entry<Message>> messages(){return FEED.entries();}
|
||||
public static List<Track> tracked(){var tracks=new ArrayList<>(OBJECTIVES.shown(options().pinnedAdvancements,options().advancements));if(options().pinnedAdvancements)tracks.addAll(communityTracks);return tracks.stream().distinct().limit(6).toList();}
|
||||
|
||||
@@ -69,6 +69,16 @@ public final class PauseAtlasWidget extends AbstractButton implements AutoClosea
|
||||
if(--refresh<=0)request();
|
||||
}
|
||||
public int knownPixels() {return pixels;}
|
||||
private double zoom(){return Math.max(.75,Math.max(width,height)/992.);}
|
||||
java.util.List<CommunityMapMarkers.Pin> questPins() {
|
||||
return CommunityMapMarkers.project(dimension,wx->getX()+width/2+(int)Math.round((wx-centerX)*zoom()),
|
||||
wz->getY()+height/2+(int)Math.round((wz-centerZ)*zoom()),getX(),getY()+20,getRight(),getBottom()-20);
|
||||
}
|
||||
@Override public boolean mouseClicked(net.minecraft.client.input.MouseButtonEvent event,boolean doubleClick) {
|
||||
var pin=CommunityMapMarkers.at(questPins(),event.x(),event.y());
|
||||
if(event.button()==com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_LEFT && pin!=null){CommunityClient.open(owner,"notice",pin.marker().id(),false);return true;}
|
||||
return super.mouseClicked(event,doubleClick);
|
||||
}
|
||||
@Override public void onPress(InputWithModifiers input) {ProgressionClient.open(owner,"history");}
|
||||
@Override protected void extractContents(GuiGraphicsExtractor g,int mx,int my,float delta) {
|
||||
if(texture==null)return;
|
||||
@@ -77,7 +87,7 @@ public final class PauseAtlasWidget extends AbstractButton implements AutoClosea
|
||||
g.fill(x,y,getRight(),getBottom(),0x90151B19);
|
||||
g.enableScissor(x,y,getRight(),getBottom());
|
||||
if(readable) {
|
||||
double zoom=Math.max(.75,Math.max(width,height)/992.);
|
||||
double zoom=zoom();
|
||||
int left=x+width/2+(int)((originX-centerX)*zoom),top=y+height/2+(int)((originZ-centerZ)*zoom);
|
||||
g.blit(RenderPipelines.GUI_TEXTURED,TEXTURE,left,top,0,0,(int)(SIZE*zoom),(int)(SIZE*zoom),SIZE,SIZE,SIZE,SIZE);
|
||||
int px=x+width/2,pz=y+height/2;
|
||||
@@ -89,6 +99,8 @@ public final class PauseAtlasWidget extends AbstractButton implements AutoClosea
|
||||
int color=ProgressionClient.identity==null?0xFFFFFFFF:0xFF000000|ProgressionClient.identity.color();
|
||||
g.fill(px-3,pz-3,px+4,pz+4,0xFF141818);g.fill(px-2,pz-2,px+3,pz+3,color);
|
||||
}
|
||||
boolean questHovered=CommunityMapMarkers.render(g,questPins(),mx,my);
|
||||
setTooltip(questHovered?null:Tooltip.create(Component.translatable("sanctuary.pause.map_hint")));
|
||||
g.text(font,"N ↑",x+7,y+7,0xFFFFFFFF);
|
||||
if(pixels==0)g.textWithWordWrap(font,SanctuaryAtlasScreen.label(readable?"empty":status),x+8,y+height/2+12,width-16,0xFFCCCCCC);
|
||||
g.text(font,font.plainSubstrByWidth(getMessage().getString(),width-14),x+7,getBottom()-16,0xFFF0EEE6);
|
||||
|
||||
@@ -38,7 +38,9 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
private String status="loading";
|
||||
private String dimension="";
|
||||
|
||||
public SanctuaryAtlasScreen(Screen parent) {super(parent);}
|
||||
private final fr.koka.sanctuary.community.CommunityTask.Location initialTarget;
|
||||
public SanctuaryAtlasScreen(Screen parent) {this(parent,null);}
|
||||
SanctuaryAtlasScreen(Screen parent,fr.koka.sanctuary.community.CommunityTask.Location target) {super(parent);initialTarget=target;}
|
||||
public static Component label(String key,Object... args) {return Component.translatable("sanctuary.atlas."+key,args);}
|
||||
@Override protected void init() {
|
||||
dragging=false;
|
||||
@@ -75,7 +77,10 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
minecraft.getTextureManager().register(TEXTURE,texture);
|
||||
footprintTexture=new DynamicTexture("Sanctuary atlas footprints",SIZE,SIZE,false);
|
||||
minecraft.getTextureManager().register(FOOTPRINT_TEXTURE,footprintTexture);
|
||||
clear();recenter();
|
||||
clear();
|
||||
if(initialTarget!=null && minecraft.level!=null && initialTarget.dimension().equals(minecraft.level.dimension().identifier().toString())) {
|
||||
centerX=initialTarget.x()+.5;centerZ=initialTarget.z()+.5;request();
|
||||
} else recenter();
|
||||
}
|
||||
}
|
||||
private void clear() {
|
||||
@@ -193,6 +198,8 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
@Override public boolean mouseClicked(MouseButtonEvent event,boolean doubleClick) {
|
||||
// Disabled buttons also own their hit area; clicking one must never start a map gesture.
|
||||
if (overControl(event.x(),event.y())) return super.mouseClicked(event,doubleClick);
|
||||
var quest=CommunityMapMarkers.at(questPins(),event.x(),event.y());
|
||||
if(event.button()==InputConstants.MOUSE_BUTTON_LEFT && quest!=null){CommunityClient.open(this,"notice",quest.marker().id(),false);return true;}
|
||||
if (event.button()==InputConstants.MOUSE_BUTTON_LEFT && inside(event.x(),event.y()) && readable()) {
|
||||
setFocused(null);setDragging(false);dragging=true;return true;
|
||||
}
|
||||
@@ -267,13 +274,18 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
graphics.textWithWordWrap(font,message,x,y,textWidth,0xFFE0E6EE);
|
||||
}
|
||||
renderCoordinates(graphics,mouseX,mouseY);
|
||||
if (!renderMarkerHint(graphics,mouseX,mouseY)) renderFootprintHint(graphics,mouseX,mouseY);
|
||||
boolean questHint=CommunityMapMarkers.render(graphics,questPins(),mouseX,mouseY);
|
||||
if (!questHint && !renderMarkerHint(graphics,mouseX,mouseY)) renderFootprintHint(graphics,mouseX,mouseY);
|
||||
}
|
||||
public void accept(fr.koka.sanctuary.atlas.AtlasMarkerPayload payload) {
|
||||
if (payload.token()!=token || !readable() || minecraft.level==null
|
||||
|| !dimension.equals(minecraft.level.dimension().identifier().toString())) return;
|
||||
markers=payload.markers();
|
||||
}
|
||||
java.util.List<CommunityMapMarkers.Pin> questPins() {
|
||||
return CommunityMapMarkers.project(dimension,this::screenX,this::screenZ,0,contentTop,width,height-62).stream()
|
||||
.filter(pin->!overControl(pin.x(),pin.y())).toList();
|
||||
}
|
||||
private Component markerName(fr.koka.sanctuary.atlas.AtlasMarker marker) {
|
||||
return marker.name().isBlank() ? label("marker_"+marker.kind()) : Component.literal(marker.name());
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ public final class Community {
|
||||
public Refused(String code) { super(code); this.code = code; }
|
||||
}
|
||||
public static void fail(String code) { throw new Refused(code); }
|
||||
public static String search(String value) {
|
||||
if(value==null || value.codePointCount(0,value.length())>120)fail("invalid");
|
||||
value=value.strip();
|
||||
if(!value.isEmpty())text(value,120);
|
||||
return value.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
public static String uuid(String value) {
|
||||
if (value == null || !value.matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")) fail("invalid");
|
||||
return value;
|
||||
|
||||
@@ -79,17 +79,17 @@ public final class CommunityNetwork {
|
||||
if (System.nanoTime() - now > 10_000_000_000L) fail("busy");
|
||||
var store = runtime.store(); result = new JsonObject(); result.addProperty("moderator",actor.moderator());
|
||||
result.addProperty("serverTime",System.currentTimeMillis()); result.addProperty("mode",runtime.config.mode());
|
||||
if(request.kind().equals("bulletin") && !Set.of("overview","following").contains(request.action()) && !actor.moderator())fail("forbidden");
|
||||
if(request.kind().equals("bulletin") && !Set.of("overview","following","list","cards","thread").contains(request.action()) && !actor.moderator())fail("forbidden");
|
||||
switch (request.action()) {
|
||||
case "overview" -> {
|
||||
result.add("articles", JSON.toJsonTree(store.list("article","",0).entries().stream().limit(3).toList()));
|
||||
result.add("notices", JSON.toJsonTree(store.list("notice","",0).entries().stream().limit(3).toList()));
|
||||
result.add("bulletins", JSON.toJsonTree(store.list("bulletin","",0).entries().stream().limit(1).toList()));
|
||||
}
|
||||
case "list" -> result.add("page",JSON.toJsonTree(store.list(request.kind(),"",request.page())));
|
||||
case "list" -> result.add("page",JSON.toJsonTree(store.list(request.kind(),"",request.page(),request.body())));
|
||||
case "cards" -> {
|
||||
if(!Set.of("article","notice").contains(request.kind()))fail("invalid");
|
||||
var page=store.list(request.kind(),"",request.page());var cards=JSON.toJsonTree(page).getAsJsonObject();
|
||||
var page=store.list(request.kind(),"",request.page(),request.body());var cards=JSON.toJsonTree(page).getAsJsonObject();
|
||||
for(var value:cards.getAsJsonArray("entries")) {
|
||||
var entry=value.getAsJsonObject();String photo=entry.has("photo")?entry.get("photo").getAsString():"";
|
||||
if(!photo.isEmpty())entry.addProperty("thumbnail",runtime.thumbnail(entry.get("id").getAsString()+":"+entry.get("revision").getAsInt(),photo));
|
||||
@@ -99,7 +99,6 @@ public final class CommunityNetwork {
|
||||
}
|
||||
case "thread" -> {
|
||||
var thread=store.thread(request.id(),request.page());
|
||||
if(thread.entry().kind().equals("bulletin")&&!actor.moderator())fail("forbidden");
|
||||
result.add("thread",JSON.toJsonTree(thread));
|
||||
if(thread.entry().kind().equals("notice")) {
|
||||
var members=store.members(request.id());result.add("members",JSON.toJsonTree(members));
|
||||
@@ -154,7 +153,11 @@ public final class CommunityNetwork {
|
||||
}
|
||||
private static JsonElement following(CommunityStore store,Actor actor) throws Exception {
|
||||
var result=JSON.toJsonTree(store.following(actor));omitPhotos(result);
|
||||
result.getAsJsonArray().forEach(e->e.getAsJsonObject().remove("body"));return result;
|
||||
result.getAsJsonArray().forEach(e->{
|
||||
var entry=e.getAsJsonObject();String body=entry.remove("body").getAsString();
|
||||
int count=body.codePointCount(0,body.length());
|
||||
entry.addProperty("summary",count>240?body.substring(0,body.offsetByCodePoints(0,240))+"…":body);
|
||||
});return result;
|
||||
}
|
||||
private static void omitTasks(JsonElement value) {
|
||||
if(value.isJsonArray())value.getAsJsonArray().forEach(CommunityNetwork::omitTasks);
|
||||
|
||||
@@ -9,7 +9,8 @@ public interface CommunityStore extends AutoCloseable {
|
||||
void save(Entry entry, boolean insert) throws Exception;
|
||||
int followers(String id) throws Exception;
|
||||
}
|
||||
Page list(String kind, String parent, int page) throws Exception;
|
||||
default Page list(String kind, String parent, int page) throws Exception { return list(kind,parent,page,""); }
|
||||
Page list(String kind, String parent, int page, String search) throws Exception;
|
||||
ThreadView thread(String id, int page) throws Exception;
|
||||
Entry change(Actor actor, Change change) throws Exception;
|
||||
java.util.List<CommunityTask.Follow> members(String id) throws Exception;
|
||||
|
||||
@@ -40,20 +40,22 @@ public final class FileCommunityStore implements CommunityStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override public synchronized Page list(String kind, String parent, int page) {
|
||||
@Override public synchronized Page list(String kind, String parent, int page, String search) {
|
||||
String needle=Community.search(search);
|
||||
page(page); if (!KINDS.contains(kind)) fail("invalid");
|
||||
if (!parent.isEmpty()) visible(entries.get(parent));
|
||||
Comparator<Entry> order = Comparator.comparingLong(Entry::createdAt).thenComparing(Entry::id);
|
||||
if (kind.equals("bulletin")) order = order.reversed();
|
||||
else if (!kind.equals("reply")) order = Comparator.comparing(Entry::pinned).reversed().thenComparing(order.reversed());
|
||||
var found = entries.values().stream().filter(e -> e.kind().equals(kind) && e.parentId().equals(parent)
|
||||
&& !e.status().equals("hidden")).sorted(order).skip((long) page * PAGE_SIZE).limit(PAGE_SIZE + 1).toList();
|
||||
&& !e.status().equals("hidden") && (needle.isEmpty() || e.authorName().toLowerCase(Locale.ROOT).contains(needle)
|
||||
|| e.title().toLowerCase(Locale.ROOT).contains(needle) || e.body().toLowerCase(Locale.ROOT).contains(needle))).sorted(order).skip((long) page * PAGE_SIZE).limit(PAGE_SIZE + 1).toList();
|
||||
return new Page(found.subList(0, Math.min(PAGE_SIZE, found.size())), page, found.size() > PAGE_SIZE);
|
||||
}
|
||||
@Override public synchronized ThreadView thread(String id, int page) {
|
||||
Entry entry = visible(entries.get(uuid(id)));
|
||||
if (entry.kind().equals("reply")) fail("missing");
|
||||
return new ThreadView(entry, list("reply", id, page));
|
||||
return new ThreadView(entry, list("reply", id, page, ""));
|
||||
}
|
||||
@Override public synchronized Entry change(Actor actor, Change change) throws Exception {
|
||||
var candidate = new LinkedHashMap<>(entries);
|
||||
|
||||
@@ -42,27 +42,30 @@ public final class SqlCommunityStore implements CommunityStore {
|
||||
try (var s = query(c, "SELECT * FROM sanctuary_community_entries WHERE server_id=? AND id=?" + (lock ? " FOR UPDATE" : ""), serverId, id);
|
||||
var r = s.executeQuery()) { return r.next() ? row(r) : null; }
|
||||
}
|
||||
private Page list(Connection c, String kind, String parent, int page) throws Exception {
|
||||
private Page list(Connection c, String kind, String parent, int page, String search) throws Exception {
|
||||
String needle=Community.search(search);
|
||||
page(page); if (!KINDS.contains(kind)) fail("invalid");
|
||||
if (!parent.isEmpty()) visible(get(c, uuid(parent), false));
|
||||
String parentSql = parent.isEmpty() ? "parent_id IS NULL" : "parent_id=?";
|
||||
String order = kind.equals("reply") ? "created_at ASC,id ASC" : kind.equals("bulletin") ? "created_at DESC,id DESC" : "pinned DESC,created_at DESC,id DESC";
|
||||
var values = new ArrayList<Object>(List.of(serverId, kind)); if (!parent.isEmpty()) values.add(parent);
|
||||
String filter=needle.isEmpty()?"":" AND (LOCATE(?,BINARY LOWER(author_name))>0 OR LOCATE(?,BINARY LOWER(title))>0 OR LOCATE(?,BINARY LOWER(body))>0)";
|
||||
if(!needle.isEmpty())values.addAll(List.of(needle,needle,needle));
|
||||
values.add(PAGE_SIZE + 1); values.add(page * PAGE_SIZE);
|
||||
try (var s = query(c, "SELECT * FROM sanctuary_community_entries WHERE server_id=? AND kind=? AND " + parentSql
|
||||
+ " AND status<>'hidden' ORDER BY " + order + " LIMIT ? OFFSET ?", values.toArray()); var r = s.executeQuery()) {
|
||||
+ " AND status<>'hidden'" + filter + " ORDER BY " + order + " LIMIT ? OFFSET ?", values.toArray()); var r = s.executeQuery()) {
|
||||
var entries = new ArrayList<Entry>(); while (r.next()) entries.add(row(r));
|
||||
return new Page(List.copyOf(entries.subList(0, Math.min(PAGE_SIZE, entries.size()))), page, entries.size() > PAGE_SIZE);
|
||||
}
|
||||
}
|
||||
@Override public Page list(String kind, String parent, int page) throws Exception {
|
||||
try (var c = connect()) { c.setAutoCommit(false); var result = list(c, kind, parent, page); c.commit(); return result; }
|
||||
@Override public Page list(String kind, String parent, int page, String search) throws Exception {
|
||||
try (var c = connect()) { c.setAutoCommit(false); var result = list(c, kind, parent, page, search); c.commit(); return result; }
|
||||
}
|
||||
@Override public ThreadView thread(String id, int page) throws Exception {
|
||||
try (var c = connect()) {
|
||||
c.setAutoCommit(false); Entry entry = visible(get(c, uuid(id), false));
|
||||
if (entry.kind().equals("reply")) fail("missing");
|
||||
var result = new ThreadView(entry, list(c, "reply", id, page)); c.commit(); return result;
|
||||
var result = new ThreadView(entry, list(c, "reply", id, page, "")); c.commit(); return result;
|
||||
}
|
||||
}
|
||||
@Override public Entry change(Actor actor, Change change) throws Exception {
|
||||
|
||||
@@ -2341,6 +2341,13 @@
|
||||
"sanctuary.shaders.ssr_distance_help": "Independent SSR range: 16, 32, 64, 128 or 256 blocks. Default: 64 blocks. Limited to visible terrain and mesh budget.",
|
||||
"sanctuary.community.article": "Gazette",
|
||||
"sanctuary.community.notice": "Notice board",
|
||||
"sanctuary.community.notice_tab": "Notice board",
|
||||
"sanctuary.community.map_followed": "Followed request · click for discussion",
|
||||
"sanctuary.community.map_more_materials": "+ %s more materials",
|
||||
"sanctuary.community.map_show": "Show on map",
|
||||
"sanctuary.community.map_other_dimension": "Visit this dimension to open its map.",
|
||||
"sanctuary.community.search": "Search: player, title or text…",
|
||||
"sanctuary.community.server_tab": "Server",
|
||||
"sanctuary.community.bulletin": "Server notices",
|
||||
"sanctuary.community.publish": "Publish",
|
||||
"sanctuary.community.reply": "Reply",
|
||||
|
||||
@@ -2341,6 +2341,13 @@
|
||||
"sanctuary.shaders.ssr_distance_help": "Portée indépendante du SSR : 16, 32, 64, 128 ou 256 blocs. Défaut : 64 blocs. Limitée au terrain visible et au budget de maillage.",
|
||||
"sanctuary.community.article": "Gazette",
|
||||
"sanctuary.community.notice": "Tableau",
|
||||
"sanctuary.community.notice_tab": "Tableau",
|
||||
"sanctuary.community.map_followed": "Demande suivie · cliquer pour la discussion",
|
||||
"sanctuary.community.map_more_materials": "+ %s autres matériaux",
|
||||
"sanctuary.community.map_show": "Voir sur la carte",
|
||||
"sanctuary.community.map_other_dimension": "Rejoignez cette dimension pour ouvrir sa carte.",
|
||||
"sanctuary.community.search": "Rechercher : joueur, titre ou texte…",
|
||||
"sanctuary.community.server_tab": "Serveur",
|
||||
"sanctuary.community.bulletin": "Intendance",
|
||||
"sanctuary.community.publish": "Publier",
|
||||
"sanctuary.community.reply": "Répondre",
|
||||
|
||||
@@ -50,6 +50,28 @@ public final class CommunitySmoke {
|
||||
check(new com.google.gson.GsonBuilder().disableHtmlEscaping().create().toJson(task).length()<8000,"Task fits request envelope");
|
||||
System.out.println("COMMUNITY158_TASKS_PASS "+store.getClass().getSimpleName());return limited;
|
||||
}
|
||||
static void search(CommunityStore store) throws Exception {
|
||||
var author=new Actor(UUID.randomUUID().toString(),"Chercheur_159",false);
|
||||
Entry first=null;
|
||||
for(int i=0;i<13;i++) {
|
||||
var e=store.change(author,create(UUID.randomUUID().toString(),"","notice","Recherche159 "+i,i==0?"Épingle 50%_exact":"Description159"));
|
||||
if(i==0)first=e;
|
||||
}
|
||||
var pinned=store.change(moderator,action("pin",first));
|
||||
check(store.list("notice","",0," CHERCHEUR_159 ").entries().getFirst().id().equals(pinned.id()),"Search author and retain pins");
|
||||
check(store.list("notice","",0,"recherche159").more()&&store.list("notice","",1,"recherche159").entries().size()==3,"Search before pagination");
|
||||
check(store.list("notice","",0,"50%_exact").entries().size()==1,"Literal SQL wildcard characters");
|
||||
check(store.list("notice","",0,"ÉPINGLE").entries().size()==1,"Accented case-insensitive body search");
|
||||
check(store.list("notice","",0,"%").entries().size()==1,"Percent is literal");
|
||||
check(store.list("article","",0,"Chercheur_159").entries().isEmpty(),"Search stays within selected section");
|
||||
store.change(moderator,action("hide",pinned));
|
||||
check(store.list("notice","",0,"50%_exact").entries().isEmpty(),"Hidden search result excluded");
|
||||
refuse("invalid",()->store.list("notice","",0,"x".repeat(121)));
|
||||
var official=store.change(moderator,create(UUID.randomUUID().toString(),"","bulletin","Serveur159","Message officiel"));
|
||||
check(store.list("bulletin","",0,"serveur159").entries().size()==1 && store.thread(official.id(),0).entry().equals(official),"Read server history");
|
||||
refuse("forbidden",()->store.change(owner,action("hide",official)));
|
||||
System.out.println("COMMUNITY159_SEARCH_PASS "+store.getClass().getSimpleName());
|
||||
}
|
||||
static Entry scenario(CommunityStore store) throws Exception {
|
||||
var request=create(UUID.randomUUID().toString(),"","notice","Un pont","Du bois et des bras 🌳");
|
||||
var post=store.change(owner,request);
|
||||
@@ -92,7 +114,7 @@ public final class CommunitySmoke {
|
||||
Path dir=Files.createTempDirectory("sanctuary-community-check-");
|
||||
String server=UUID.randomUUID().toString();Path file=dir.resolve("community.json");
|
||||
var store=new FileCommunityStore(file,server);var article=scenario(store);
|
||||
var follower=tasks(store);
|
||||
var follower=tasks(store);search(store);
|
||||
var reopened=new FileCommunityStore(file,server);
|
||||
check(reopened.following(follower).equals(store.following(follower)) && reopened.following(follower).size()==16,"Follow state survives restart");
|
||||
check(reopened.thread(article.id(),0).entry().equals(article),"Durable restart");
|
||||
@@ -102,7 +124,7 @@ public final class CommunitySmoke {
|
||||
String url=System.getenv("SANCTUARY_COMMUNITY_TEST_JDBC");
|
||||
if(url!=null){
|
||||
var sql=new SqlCommunityStore(url,"root","",server);
|
||||
var value=scenario(sql);tasks(sql);
|
||||
var value=scenario(sql);tasks(sql);search(sql);
|
||||
check(new SqlCommunityStore(url,"root","",server).thread(value.id(),0).entry().equals(value),"Database restart");
|
||||
var isolated=new SqlCommunityStore(url,"root","",UUID.randomUUID().toString());
|
||||
check(isolated.list("notice","",0).entries().isEmpty(),"Different server isolated");
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.158"
|
||||
version = "beta.159"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user