4 * License : The MIT License
5 * Copyright(c) 2020 olyutorskii
8 package jp.sfjp.jindolf.data.html;
10 import java.util.HashMap;
11 import java.util.LinkedList;
12 import java.util.List;
14 import jp.osdn.jindolf.parser.EntityConverter;
15 import jp.osdn.jindolf.parser.HtmlAdapter;
16 import jp.osdn.jindolf.parser.HtmlParseException;
17 import jp.osdn.jindolf.parser.PageType;
18 import jp.osdn.jindolf.parser.SeqRange;
19 import jp.osdn.jindolf.parser.content.DecodedContent;
20 import jp.sfjp.jindolf.data.Avatar;
21 import jp.sfjp.jindolf.data.Period;
22 import jp.sfjp.jindolf.data.SysEvent;
23 import jp.sfjp.jindolf.data.Talk;
24 import jp.sfjp.jindolf.data.Topic;
25 import jp.sfjp.jindolf.data.Village;
26 import jp.sourceforge.jindolf.corelib.EventFamily;
27 import jp.sourceforge.jindolf.corelib.GameRole;
28 import jp.sourceforge.jindolf.corelib.PeriodType;
29 import jp.sourceforge.jindolf.corelib.SysEventType;
30 import jp.sourceforge.jindolf.corelib.TalkType;
31 import jp.sourceforge.jindolf.corelib.Team;
35 * 各日(Period)のHTMLをパースし、
36 * 会話やイベントの通知を受け取るためのハンドラ。
39 * あらかじめ指定したPeriodインスタンスに
40 * 会話やイベントのリストが適切に更新される。
42 * <p>各種ビューが対応するまでの間、Unicodeの非BMP面文字には代替文字で対処。
44 * <p>※ 人狼BBS:G国におけるG2087村のエピローグが終了した段階で、
45 * 人狼BBSは過去ログの提供しか行っていない。
46 * だがこのクラスには進行中の村の各日をパースするための
49 class PeriodHandler extends HtmlAdapter {
51 private static final int TALKTYPE_NUM = TalkType.values().length;
53 private final EntityConverter converter =
54 new EntityConverter(true);
55 // TODO: 非BMP面文字に対応するまでの暫定措置
57 /** 非別、Avatar別、会話種別の会話通し番号。 */
58 private final Map<Avatar, int[]> countMap =
61 private Period period = null;
63 private TalkType talkType;
64 private Avatar avatar;
66 private String anchorId;
68 private int talkMinute;
69 private DecodedContent talkContent = null;
71 private EventFamily eventFamily;
72 private SysEventType sysEventType;
73 private DecodedContent eventContent = null;
74 private final List<Avatar> avatarList = new LinkedList<>();
75 private final List<GameRole> roleList = new LinkedList<>();
76 private final List<Integer> integerList = new LinkedList<>();
77 private final List<CharSequence> charseqList =
93 * @param period Period
95 void setPeriod(Period period){
102 * フルネーム文字列からAvatarインスタンスを得る。
104 * <p>村に未登録のAvatarであればついでに登録される。
107 * @param range 文字列内のAvatarフルネームを示す領域
110 private Avatar toAvatar(DecodedContent content, SeqRange range){
111 Village village = this.period.getVillage();
112 String fullName = this.converter
113 .convert(content, range)
115 Avatar result = village.getAvatar(fullName);
117 result = new Avatar(fullName);
118 village.addAvatar(result);
125 * パース中の各種コンテキストをリセットする。
128 this.countMap.clear();
137 * パース中の会話コンテキストをリセットする。
139 private void resetTalkContext(){
140 this.talkType = null;
143 this.anchorId = null;
145 this.talkMinute = -1;
146 this.talkContent = null;
151 * パース中のイベントコンテキストをリセットする。
153 private void resetEventContext(){
154 this.eventFamily = null;
155 this.sysEventType = null;
156 this.eventContent = null;
157 this.avatarList.clear();
158 this.roleList.clear();
159 this.integerList.clear();
160 this.charseqList.clear();
167 * @param content {@inheritDoc}
168 * @throws HtmlParseException {@inheritDoc}
171 public void startParse(DecodedContent content)
172 throws HtmlParseException{
175 this.period.setLoginName(null);
176 this.period.clearTopicList();
184 * <p>各PeriodのHTML上部にあるログイン名が通知されたのなら、
185 * それはPOSTやCookieを使ってのログインに成功したと言うこと。
187 * <p>ログイン名中の文字実体参照は展開される。
189 * <p>※ 2020-02現在、人狼BBS各国へのログインは無意味。
191 * @param content {@inheritDoc}
192 * @param loginRange {@inheritDoc}
193 * @throws HtmlParseException {@inheritDoc}
196 public void loginName(DecodedContent content, SeqRange loginRange)
197 throws HtmlParseException{
198 DecodedContent loginName =
199 this.converter.convert(content, loginRange);
201 this.period.setLoginName(loginName.toString());
209 * <p>受信したHTMLがPeriodページでないのならパースを中止する。
211 * @param type {@inheritDoc}
212 * @throws HtmlParseException {@inheritDoc}
215 public void pageType(PageType type) throws HtmlParseException{
216 if(type != PageType.PERIOD_PAGE){
217 throw new HtmlParseException(
218 "意図しないページを読み込もうとしました。");
228 * @param month {@inheritDoc}
229 * @param day {@inheritDoc}
230 * @param hour {@inheritDoc}
231 * @param minute {@inheritDoc}
232 * @throws HtmlParseException {@inheritDoc}
235 public void commitTime(int month, int day, int hour, int minute)
236 throws HtmlParseException{
237 this.period.setLimit(hour, minute);
244 * <p>このPeriodが進行中(Hot!)か否か判定する。
246 * <p>PeriodのHTML内に自分自身へのリンクが無いかチェックする。
247 * 自分へのリンクが見つかればこのPeriodを非Hotにする。
249 * 今受信しているHTMLは別のPeriodから辿るために書かれたものということ。
251 * <p>原因としては、HotだったPeriodがゲーム進行に従い
252 * Hotでなくなったことなどが考えられる。
255 * 村情報受信を通じて事前に設定されていなければならない。
257 * <p>※ 2020-02現在、HotなPeriodを受信する機会はないはず。
259 * @param content {@inheritDoc}
260 * @param anchorRange {@inheritDoc}
261 * @param periodType {@inheritDoc}
262 * @param day {@inheritDoc}
263 * @throws HtmlParseException {@inheritDoc}
266 public void periodLink(DecodedContent content,
267 SeqRange anchorRange,
268 PeriodType periodType,
270 throws HtmlParseException{
271 if(this.period.getType() != periodType) return;
273 boolean isProgress = periodType == PeriodType.PROGRESS;
274 boolean dayMatch = this.period.getDay() == day;
275 if(isProgress && ! dayMatch){
279 if( ! anchorRange.isValid() ) return;
281 this.period.setHot(false);
289 * @throws HtmlParseException {@inheritDoc}
292 public void startTalk() throws HtmlParseException{
294 this.talkContent = new DecodedContent(100 + 1);
301 * @param type {@inheritDoc}
302 * @throws HtmlParseException {@inheritDoc}
305 public void talkType(TalkType type)
306 throws HtmlParseException{
307 this.talkType = type;
314 * @param content {@inheritDoc}
315 * @param avatarRange {@inheritDoc}
316 * @throws HtmlParseException {@inheritDoc}
319 public void talkAvatar(DecodedContent content, SeqRange avatarRange)
320 throws HtmlParseException{
321 this.avatar = toAvatar(content, avatarRange);
328 * @param hour {@inheritDoc}
329 * @param minute {@inheritDoc}
330 * @throws HtmlParseException {@inheritDoc}
333 public void talkTime(int hour, int minute)
334 throws HtmlParseException{
335 this.talkHour = hour;
336 this.talkMinute = minute;
343 * @param tno {@inheritDoc}
344 * @throws HtmlParseException {@inheritDoc}
347 public void talkNo(int tno) throws HtmlParseException{
355 * @param content {@inheritDoc}
356 * @param idRange {@inheritDoc}
357 * @throws HtmlParseException {@inheritDoc}
360 public void talkId(DecodedContent content, SeqRange idRange)
361 throws HtmlParseException{
362 this.anchorId = content.subSequence(idRange.getStartPos(),
363 idRange.getEndPos() )
371 * <p>会話中の文字実体参照は展開される。
373 * @param content {@inheritDoc}
374 * @param textRange {@inheritDoc}
375 * @throws HtmlParseException {@inheritDoc}
378 public void talkText(DecodedContent content, SeqRange textRange)
379 throws HtmlParseException{
380 this.converter.append(this.talkContent, content, textRange);
387 * @throws HtmlParseException {@inheritDoc}
390 public void talkBreak()
391 throws HtmlParseException{
392 this.talkContent.append('\n');
397 * 日別、Avatar別、会話種ごとに発言回数をインクリメントする。
399 * @param targetAvatar 対象Avatar
400 * @param targetType 対象会話種
403 private int countUp(Avatar targetAvatar, TalkType targetType){
404 int[] avatarCount = this.countMap.get(targetAvatar);
405 if(avatarCount == null){
406 avatarCount = new int[TALKTYPE_NUM];
407 this.countMap.put(targetAvatar, avatarCount);
410 int typeIdx = targetType.ordinal();
411 int count = ++avatarCount[typeIdx];
419 * <p>パース中の各種コンテキストから会話を組み立て、
422 * @throws HtmlParseException {@inheritDoc}
425 public void endTalk() throws HtmlParseException{
426 Talk talk = new Talk(this.period,
431 this.talkHour, this.talkMinute,
434 int count = countUp(this.avatar, this.talkType);
435 talk.setCount(count);
437 this.period.addTopic(talk);
447 * @param family {@inheritDoc}
448 * @throws HtmlParseException {@inheritDoc}
451 public void startSysEvent(EventFamily family)
452 throws HtmlParseException{
455 this.eventFamily = family;
456 this.eventContent = new DecodedContent();
464 * @param type {@inheritDoc}
465 * @throws HtmlParseException {@inheritDoc}
468 public void sysEventType(SysEventType type)
469 throws HtmlParseException{
470 this.sysEventType = type;
477 * <p>イベント文字列中の文字実体参照は展開される。
479 * @param content {@inheritDoc}
480 * @param contentRange {@inheritDoc}
481 * @throws HtmlParseException {@inheritDoc}
484 public void sysEventContent(DecodedContent content,
485 SeqRange contentRange)
486 throws HtmlParseException{
487 this.converter.append(this.eventContent, content, contentRange);
494 * <p>イベント文内Aタグ内容の文字実体参照は展開される。
497 * @param content {@inheritDoc}
498 * @param anchorRange {@inheritDoc}
499 * @param contentRange {@inheritDoc}
500 * @throws HtmlParseException {@inheritDoc}
503 public void sysEventContentAnchor(DecodedContent content,
504 SeqRange anchorRange,
505 SeqRange contentRange)
506 throws HtmlParseException{
507 this.converter.append(this.eventContent, content, contentRange);
514 * @throws HtmlParseException {@inheritDoc}
517 public void sysEventContentBreak() throws HtmlParseException{
518 this.eventContent.append('\n');
525 * <p>Avatarリストの先頭にAvatarが、
526 * intリストの先頭にエントリー番号が入る。
528 * @param content {@inheritDoc}
529 * @param entryNo {@inheritDoc}
530 * @param avatarRange {@inheritDoc}
531 * @throws HtmlParseException {@inheritDoc}
534 public void sysEventOnStage(DecodedContent content,
536 SeqRange avatarRange)
537 throws HtmlParseException{
538 Avatar newAvatar = toAvatar(content, avatarRange);
539 this.integerList.add(entryNo);
540 this.avatarList.add(newAvatar);
547 * <p>役職者数開示に伴い役職リストとintリストに一件ずつ追加される。
549 * @param role {@inheritDoc}
550 * @param num {@inheritDoc}
551 * @throws HtmlParseException {@inheritDoc}
554 public void sysEventOpenRole(GameRole role, int num)
555 throws HtmlParseException{
556 this.roleList.add(role);
557 this.integerList.add(num);
564 * <p>噛み及びハム溶けに伴いAvatarリストに1件ずつ追加される。
566 * @param content {@inheritDoc}
567 * @param avatarRange {@inheritDoc}
568 * @throws HtmlParseException {@inheritDoc}
571 public void sysEventMurdered(DecodedContent content,
572 SeqRange avatarRange)
573 throws HtmlParseException{
574 Avatar murdered = toAvatar(content, avatarRange);
575 this.avatarList.add(murdered);
582 * <p>生存者表示に伴いAvatarリストに1件ずつ追加される。
584 * @param content {@inheritDoc}
585 * @param avatarRange {@inheritDoc}
586 * @throws HtmlParseException {@inheritDoc}
589 public void sysEventSurvivor(DecodedContent content,
590 SeqRange avatarRange)
591 throws HtmlParseException{
592 Avatar survivor = toAvatar(content, avatarRange);
593 this.avatarList.add(survivor);
601 * 投票元と投票先の順でAvatarリストに追加される。
603 * <p>被処刑者がいればAvatarリストの最後に追加される。
605 * @param content {@inheritDoc}
606 * @param voteByRange {@inheritDoc}
607 * @param voteToRange {@inheritDoc}
608 * @throws HtmlParseException {@inheritDoc}
611 public void sysEventCounting(DecodedContent content,
612 SeqRange voteByRange,
613 SeqRange voteToRange)
614 throws HtmlParseException{
615 if(voteByRange.isValid()){
616 Avatar voteBy = toAvatar(content, voteByRange);
617 this.avatarList.add(voteBy);
619 Avatar voteTo = toAvatar(content, voteToRange);
620 this.avatarList.add(voteTo);
628 * 投票元と投票先の順でAvatarリストに追加される。
630 * @param content {@inheritDoc}
631 * @param voteByRange {@inheritDoc}
632 * @param voteToRange {@inheritDoc}
633 * @throws HtmlParseException {@inheritDoc}
636 public void sysEventCounting2(DecodedContent content,
637 SeqRange voteByRange,
638 SeqRange voteToRange)
639 throws HtmlParseException{
640 sysEventCounting(content, voteByRange, voteToRange);
647 * <p>Avatarリストの先頭に突然死者が入る。
649 * @param content {@inheritDoc}
650 * @param avatarRange {@inheritDoc}
651 * @throws HtmlParseException {@inheritDoc}
654 public void sysEventSuddenDeath(DecodedContent content,
655 SeqRange avatarRange)
656 throws HtmlParseException{
657 Avatar suddenDeath = toAvatar(content, avatarRange);
658 this.avatarList.add(suddenDeath);
667 * 文字列リストにURLとプレイヤー名の2件、
668 * intリストに生死(1or0)が1件、
669 * Roleリストに役職が1件追加される。
671 * @param content {@inheritDoc}
672 * @param avatarRange {@inheritDoc}
673 * @param anchorRange {@inheritDoc}
674 * @param loginRange {@inheritDoc}
675 * @param isLiving {@inheritDoc}
676 * @param role {@inheritDoc}
677 * @throws HtmlParseException {@inheritDoc}
680 public void sysEventPlayerList(DecodedContent content,
681 SeqRange avatarRange,
682 SeqRange anchorRange,
686 throws HtmlParseException{
687 Avatar who = toAvatar(content, avatarRange);
690 if(anchorRange.isValid()){
691 anchor = this.converter.convert(content, anchorRange);
695 CharSequence account = this.converter
696 .convert(content, loginRange);
699 if(isLiving) liveOrDead = 1;
702 this.avatarList.add(who);
703 this.charseqList.add(anchor);
704 this.charseqList.add(account);
705 this.integerList.add(liveOrDead);
706 this.roleList.add(role);
714 * <p>G国処刑に伴い、Avatarリストに投票先が1件、
715 * intリストに得票数が1件追加される。
716 * 最後に被処刑者がAvatarリストに1件、負の値がintリストに1件追加される。
718 * @param content {@inheritDoc}
719 * @param avatarRange {@inheritDoc}
720 * @param votes {@inheritDoc}
721 * @throws HtmlParseException {@inheritDoc}
724 public void sysEventExecution(DecodedContent content,
725 SeqRange avatarRange,
727 throws HtmlParseException{
728 Avatar who = toAvatar(content, avatarRange);
730 this.avatarList.add(who);
731 this.integerList.add(votes);
740 * intリストに分数、最小メンバ数、最大メンバ数の3件が設定される。
742 * @param hour {@inheritDoc}
743 * @param minute {@inheritDoc}
744 * @param minLimit {@inheritDoc}
745 * @param maxLimit {@inheritDoc}
746 * @throws HtmlParseException {@inheritDoc}
749 public void sysEventAskEntry(int hour, int minute,
750 int minLimit, int maxLimit)
751 throws HtmlParseException{
752 this.integerList.add(hour * 60 + minute);
753 this.integerList.add(minLimit);
754 this.integerList.add(maxLimit);
761 * <p>エントリー完了に伴い、分数をintリストに設定する。
763 * @param hour {@inheritDoc}
764 * @param minute {@inheritDoc}
765 * @throws HtmlParseException {@inheritDoc}
768 public void sysEventAskCommit(int hour, int minute)
769 throws HtmlParseException{
770 this.integerList.add(hour * 60 + minute);
778 * 未発言者はAvatarリストへ1件ずつ追加される。
780 * @param content {@inheritDoc}
781 * @param avatarRange {@inheritDoc}
782 * @throws HtmlParseException {@inheritDoc}
785 public void sysEventNoComment(DecodedContent content,
786 SeqRange avatarRange)
787 throws HtmlParseException{
788 Avatar noComAvatar = toAvatar(content, avatarRange);
789 this.avatarList.add(noComAvatar);
797 * Roleリストに勝者が1件、intリスト分数が1件設定される。
799 * <p>村勝利の場合は素村役職が用いられる。
801 * @param winner {@inheritDoc}
802 * @param hour {@inheritDoc}
803 * @param minute {@inheritDoc}
804 * @throws HtmlParseException {@inheritDoc}
807 public void sysEventStayEpilogue(Team winner, int hour, int minute)
808 throws HtmlParseException{
809 GameRole role = null;
812 case VILLAGE: role = GameRole.INNOCENT; break;
813 case WOLF: role = GameRole.WOLF; break;
814 case HAMSTER: role = GameRole.HAMSTER; break;
815 default: assert false; break;
818 this.roleList.add(role);
819 this.integerList.add(hour * 60 + minute);
827 * <p>護衛に伴い、Avatarリストに護衛元1件と護衛先1件が設定される。
829 * @param content {@inheritDoc}
830 * @param guardByRange {@inheritDoc}
831 * @param guardToRange {@inheritDoc}
832 * @throws HtmlParseException {@inheritDoc}
835 public void sysEventGuard(DecodedContent content,
836 SeqRange guardByRange,
837 SeqRange guardToRange)
838 throws HtmlParseException{
839 Avatar guardBy = toAvatar(content, guardByRange);
840 Avatar guardTo = toAvatar(content, guardToRange);
841 this.avatarList.add(guardBy);
842 this.avatarList.add(guardTo);
850 * 占い元が1件、占い先が1件Avatarリストに設定される。
852 * @param content {@inheritDoc}
853 * @param judgeByRange {@inheritDoc}
854 * @param judgeToRange {@inheritDoc}
855 * @throws HtmlParseException {@inheritDoc}
858 public void sysEventJudge(DecodedContent content,
859 SeqRange judgeByRange,
860 SeqRange judgeToRange)
861 throws HtmlParseException{
862 Avatar judgeBy = toAvatar(content, judgeByRange);
863 Avatar judgeTo = toAvatar(content, judgeToRange);
864 this.avatarList.add(judgeBy);
865 this.avatarList.add(judgeTo);
872 * <p>パースの完了した1件のイベントインスタンスを
875 * <p>襲撃もしくは襲撃なしのイベントの前に、
876 * 「今日がお前の命日だ!」で終わる赤ログが出現した場合、
879 * @throws HtmlParseException {@inheritDoc}
882 public void endSysEvent() throws HtmlParseException{
883 SysEvent event = new SysEvent();
884 event.setEventFamily(this.eventFamily);
885 event.setSysEventType(this.sysEventType);
886 event.setContent(this.eventContent);
887 event.addAvatarList(this.avatarList);
888 event.addRoleList(this.roleList);
889 event.addIntegerList(this.integerList);
890 event.addCharSequenceList(this.charseqList);
892 this.period.addTopic(event);
894 boolean isMurderResult =
895 this.sysEventType == SysEventType.MURDERED
896 || this.sysEventType == SysEventType.NOMURDER;
899 for(Topic topic : this.period.getTopicList()){
900 if( ! (topic instanceof Talk) ) continue;
901 Talk talk = (Talk) topic;
902 if(talk.isMurderNotice()){
904 this.countMap.clear();
918 * @throws HtmlParseException {@inheritDoc}
921 public void endParse() throws HtmlParseException{