• R/O
  • HTTP
  • SSH
  • HTTPS

Jindolf: Commit

Jindolfプロジェクトは、CGIゲーム「人狼BBS」を快適にプレイするための専用クライアントを製作するために発足したオープンソースプロジェクトです。


Commit MetaInfo

Revision33f0674923567a71080c714f722dc95d16b7ab84 (tree)
Time2020-02-21 09:37:53
AuthorOlyutorskii <olyutorskii@user...>
CommiterOlyutorskii

Log Message

Merge branch 'topic/freeparser' into develop

Change Summary

Incremental Difference

--- a/src/main/java/jp/sfjp/jindolf/Controller.java
+++ b/src/main/java/jp/sfjp/jindolf/Controller.java
@@ -22,7 +22,6 @@ import java.lang.reflect.InvocationTargetException;
2222 import java.net.URL;
2323 import java.text.MessageFormat;
2424 import java.util.List;
25-import java.util.SortedSet;
2625 import java.util.concurrent.Executor;
2726 import java.util.concurrent.Executors;
2827 import java.util.logging.Handler;
@@ -57,6 +56,9 @@ import jp.sfjp.jindolf.data.Period;
5756 import jp.sfjp.jindolf.data.RegexPattern;
5857 import jp.sfjp.jindolf.data.Talk;
5958 import jp.sfjp.jindolf.data.Village;
59+import jp.sfjp.jindolf.data.html.PeriodLoader;
60+import jp.sfjp.jindolf.data.html.VillageInfoLoader;
61+import jp.sfjp.jindolf.data.html.VillageListLoader;
6062 import jp.sfjp.jindolf.dxchg.CsvExporter;
6163 import jp.sfjp.jindolf.dxchg.WebIPCDialog;
6264 import jp.sfjp.jindolf.dxchg.WolfBBS;
@@ -769,7 +771,7 @@ public class Controller
769771 + "日目のデータを読み込んでいます";
770772 updateStatusBar(message);
771773 try{
772- Period.parsePeriod(period, true);
774+ PeriodLoader.parsePeriod(period, true);
773775 }catch(IOException e){
774776 showNetworkError(village, e);
775777 return;
@@ -1021,7 +1023,7 @@ public class Controller
10211023 + "日目のデータを読み込んでいます";
10221024 updateStatusBar(message);
10231025 try{
1024- Period.parsePeriod(period, false);
1026+ PeriodLoader.parsePeriod(period, false);
10251027 }catch(IOException e){
10261028 showNetworkError(village, e);
10271029 return;
@@ -1185,9 +1187,9 @@ public class Controller
11851187 * @param land 国
11861188 */
11871189 private void taskReloadVillageList(Land land){
1188- SortedSet<Village> villageList;
1190+ List<Village> villageList;
11891191 try{
1190- villageList = land.downloadVillageList();
1192+ villageList = VillageListLoader.loadVillageList(land);
11911193 }catch(IOException e){
11921194 showNetworkError(land, e);
11931195 return;
@@ -1245,7 +1247,7 @@ public class Controller
12451247 try{
12461248 wasHot = period.isHot();
12471249 try{
1248- Period.parsePeriod(period, force);
1250+ PeriodLoader.parsePeriod(period, force);
12491251 }catch(IOException e){
12501252 showNetworkError(village, e);
12511253 }
@@ -1258,7 +1260,7 @@ public class Controller
12581260 private boolean updatePeriodList(){
12591261 updateStatusBar("村情報を読み直しています…");
12601262 try{
1261- Village.updateVillage(village);
1263+ VillageInfoLoader.updateVillageInfo(village);
12621264 }catch(IOException e){
12631265 showNetworkError(village, e);
12641266 return false;
@@ -1451,7 +1453,7 @@ public class Controller
14511453 updateStatusBar("村情報を読み込み中…");
14521454
14531455 try{
1454- Village.updateVillage(village);
1456+ VillageInfoLoader.updateVillageInfo(village);
14551457 }catch(IOException e){
14561458 showNetworkError(village, e);
14571459 return;
--- a/src/main/java/jp/sfjp/jindolf/data/Land.java
+++ b/src/main/java/jp/sfjp/jindolf/data/Land.java
@@ -11,42 +11,25 @@ import java.awt.image.BufferedImage;
1111 import java.io.IOException;
1212 import java.net.MalformedURLException;
1313 import java.net.URI;
14-import java.net.URISyntaxException;
1514 import java.net.URL;
1615 import java.util.Collections;
1716 import java.util.LinkedList;
1817 import java.util.List;
19-import java.util.SortedSet;
20-import java.util.TreeSet;
2118 import java.util.logging.Level;
2219 import java.util.logging.Logger;
23-import jp.osdn.jindolf.parser.HtmlAdapter;
24-import jp.osdn.jindolf.parser.HtmlParseException;
25-import jp.osdn.jindolf.parser.HtmlParser;
26-import jp.osdn.jindolf.parser.PageType;
27-import jp.osdn.jindolf.parser.SeqRange;
28-import jp.osdn.jindolf.parser.content.DecodedContent;
29-import jp.sfjp.jindolf.net.HtmlSequence;
3020 import jp.sfjp.jindolf.net.ServerAccess;
3121 import jp.sourceforge.jindolf.corelib.LandDef;
32-import jp.sourceforge.jindolf.corelib.LandState;
33-import jp.sourceforge.jindolf.corelib.VillageState;
3422
3523 /**
3624 * いわゆる「国」。
3725 */
3826 public class Land {
3927
40- // 古国ID
41- private static final String ID_VANILLAWOLF = "wolf";
42-
4328 private static final Logger LOGGER = Logger.getAnonymousLogger();
4429
4530
4631 private final LandDef landDef;
4732 private final ServerAccess serverAccess;
48- private final HtmlParser parser = new HtmlParser();
49- private final VillageListHandler handler = new VillageListHandler();
5033
5134 private final List<Village> villageList = new LinkedList<>();
5235
@@ -69,89 +52,11 @@ public class Land {
6952 }
7053 this.serverAccess = new ServerAccess(url, this.landDef.getEncoding());
7154
72- this.parser.setBasicHandler(this.handler);
73-
7455 return;
7556 }
7657
7758
7859 /**
79- * クエリー文字列から特定キーの値を得る。
80- * クエリーの書式例:「{@literal a=b&c=d&e=f}」この場合キーcの値はd
81- * @param key キー
82- * @param allQuery クエリー
83- * @return 値
84- */
85- public static String getValueFromCGIQueries(String key,
86- String allQuery){
87- String result = null;
88-
89- String[] queries = allQuery.split("\\Q&\\E");
90-
91- for(String pair : queries){
92- if(pair == null) continue;
93- String[] namevalue = pair.split("\\Q=\\E");
94- if(namevalue == null) continue;
95- if(namevalue.length != 2) continue;
96- String name = namevalue[0];
97- String value = namevalue[1];
98- if(name == null) continue;
99- if( name.equals(key) ){
100- result = value;
101- if(result == null) continue;
102- if(result.length() <= 0) continue;
103- break;
104- }
105- }
106-
107- return result;
108- }
109-
110- /**
111- * AタグのHREF属性値からクエリー部を抽出する。
112- * 「{@literal &amp;}」は「{@literal &}」に解釈される。
113- * @param hrefValue HREF属性値
114- * @return クエリー文字列
115- */
116- public static String getRawQueryFromHREF(CharSequence hrefValue){
117- if(hrefValue == null) return null;
118-
119- // HTML 4.01 B.2.2 rule
120- String pureHREF = hrefValue.toString().replace("&amp;", "&");
121-
122- URI uri;
123- try{
124- uri = new URI(pureHREF);
125- }catch(URISyntaxException e){
126- LOGGER.warning(
127- "不正なURI["
128- + hrefValue
129- + "]を検出しました");
130- return null;
131- }
132-
133- String rawQuery = uri.getRawQuery();
134-
135- return rawQuery;
136- }
137-
138- /**
139- * AタグのHREF属性値から村IDを得る。
140- * @param hrefValue HREF値
141- * @return village 村ID
142- */
143- public static String getVillageIDFromHREF(CharSequence hrefValue){
144- String rawQuery = getRawQueryFromHREF(hrefValue);
145- if(rawQuery == null) return null;
146-
147- String villageID = getValueFromCGIQueries("vid", rawQuery);
148- if(villageID == null) return null;
149- if(villageID.length() <= 0) return null;
150-
151- return villageID;
152- }
153-
154- /**
15560 * 国定義を得る。
15661 * @return 国定義
15762 */
@@ -240,66 +145,10 @@ public class Land {
240145 }
241146
242147 /**
243- * 村一覧情報をダウンロードする。
244- * リスト元情報は国のトップページと村一覧ページ。
245- * 古国の場合は村一覧にアクセスせずトップページのみ。
246- * 古国以外で村建てをやめた国はトップページにアクセスしない。
247- * 村リストはVillageの実装に従いソートされる。重複する村は排除。
248- *
249- * @return ソートされた村一覧
250- * @throws java.io.IOException ネットワーク入出力の異常
251- */
252- public SortedSet<Village> downloadVillageList() throws IOException {
253- LandDef thisLand = getLandDef();
254- LandState state = thisLand.getLandState();
255- boolean isVanillaWolf = thisLand.getLandId().equals(ID_VANILLAWOLF);
256-
257- ServerAccess server = getServerAccess();
258-
259- // たまに同じ村が複数回出現するので注意!
260- SortedSet<Village> result = new TreeSet<>();
261-
262- // トップページ
263- if(state.equals(LandState.ACTIVE) || isVanillaWolf){
264- HtmlSequence html = server.getHTMLTopPage();
265- DecodedContent content = html.getContent();
266- try{
267- this.parser.parseAutomatic(content);
268- }catch(HtmlParseException e){
269- LOGGER.log(Level.WARNING, "トップページを認識できない", e);
270- }
271- List<Village> list = this.handler.getVillageList();
272- if(list != null){
273- result.addAll(list);
274- }
275- }
276-
277- // 村一覧ページ
278- if( ! isVanillaWolf ){
279- HtmlSequence html = server.getHTMLLandList();
280- DecodedContent content = html.getContent();
281- try{
282- this.parser.parseAutomatic(content);
283- }catch(HtmlParseException e){
284- LOGGER.log(Level.WARNING, "村一覧ページを認識できない", e);
285- }
286- List<Village> list = this.handler.getVillageList();
287- if(list != null){
288- result.addAll(list);
289- }
290- }
291-
292- this.parser.reset();
293- this.handler.reset();
294-
295- return result;
296- }
297-
298- /**
299148 * 村リストを更新する。
300149 * @param vset ソート済みの村一覧
301150 */
302- public void updateVillageList(SortedSet<Village> vset){
151+ public void updateVillageList(List<Village> vset){
303152 // TODO 村リスト更新のイベントリスナがあると便利か?
304153 this.villageList.clear();
305154 this.villageList.addAll(vset);
@@ -315,126 +164,4 @@ public class Land {
315164 return getLandDef().getLandName();
316165 }
317166
318- /**
319- * 村一覧取得用ハンドラ。
320- */
321- private class VillageListHandler extends HtmlAdapter{
322-
323- private List<Village> villageList = null;
324-
325- /**
326- * コンストラクタ。
327- */
328- public VillageListHandler(){
329- super();
330- return;
331- }
332-
333- /**
334- * 村一覧を返す。
335- * 再度パースを行うまで呼んではいけない。
336- * @return 村一覧
337- * @throws IllegalStateException パース前に呼び出された。
338- * あるいはパース後すでにリセットされている。
339- */
340- public List<Village> getVillageList() throws IllegalStateException{
341- if(this.villageList == null){
342- throw new IllegalStateException("パースが必要です。");
343- }
344-
345- List<Village> result = this.villageList;
346-
347- return result;
348- }
349-
350- /**
351- * リセットを行う。
352- * 村一覧は空になる。
353- */
354- public void reset(){
355- this.villageList = null;
356- return;
357- }
358-
359- /**
360- * {@inheritDoc}
361- * 村一覧リストが初期化される。
362- * @param content {@inheritDoc}
363- * @throws HtmlParseException {@inheritDoc}
364- */
365- @Override
366- public void startParse(DecodedContent content)
367- throws HtmlParseException{
368- reset();
369- this.villageList = new LinkedList<>();
370- return;
371- }
372-
373- /**
374- * {@inheritDoc}
375- * 自動判定の結果がトップページでも村一覧ページでもなければ
376- * 例外を投げる。
377- * @param type {@inheritDoc}
378- * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
379- */
380- @Override
381- public void pageType(PageType type) throws HtmlParseException{
382- if( type != PageType.VILLAGELIST_PAGE
383- && type != PageType.TOP_PAGE ){
384- throw new HtmlParseException(
385- "トップページか村一覧ページが必要です。");
386- }
387- return;
388- }
389-
390- /**
391- * {@inheritDoc}
392- * @param content {@inheritDoc}
393- * @param anchorRange {@inheritDoc}
394- * @param villageRange {@inheritDoc}
395- * @param hour {@inheritDoc}
396- * @param minute {@inheritDoc}
397- * @param villageState {@inheritDoc}
398- * @throws HtmlParseException {@inheritDoc}
399- */
400- @Override
401- public void villageRecord(DecodedContent content,
402- SeqRange anchorRange,
403- SeqRange villageRange,
404- int hour, int minute,
405- VillageState villageState)
406- throws HtmlParseException{
407- LandDef landdef = getLandDef();
408- LandState landState = landdef.getLandState();
409-
410- CharSequence href = anchorRange.sliceSequence(content);
411- String villageID = getVillageIDFromHREF(href);
412- if( villageID == null
413- || villageID.length() <= 0 ){
414- LOGGER.warning(
415- "認識できないURL[" + href + "]に遭遇しました。");
416- return;
417- }
418-
419- CharSequence fullVillageName =
420- villageRange.sliceSequence(content);
421-
422- // TODO 既に出来ているかもしれないVillageを再度作るのは無駄?
423- Village village = new Village(Land.this,
424- villageID,
425- fullVillageName.toString() );
426-
427- if(landState == LandState.HISTORICAL){
428- village.setState(VillageState.GAMEOVER);
429- }else{
430- village.setState(villageState);
431- }
432-
433- this.villageList.add(village);
434-
435- return;
436- }
437-
438- }
439-
440167 }
--- a/src/main/java/jp/sfjp/jindolf/data/Period.java
+++ b/src/main/java/jp/sfjp/jindolf/data/Period.java
@@ -7,34 +7,14 @@
77
88 package jp.sfjp.jindolf.data;
99
10-import java.io.IOException;
1110 import java.util.Collections;
12-import java.util.HashMap;
1311 import java.util.HashSet;
1412 import java.util.LinkedList;
1513 import java.util.List;
16-import java.util.Map;
1714 import java.util.Set;
18-import java.util.logging.Level;
19-import java.util.logging.Logger;
20-import jp.osdn.jindolf.parser.EntityConverter;
21-import jp.osdn.jindolf.parser.HtmlAdapter;
22-import jp.osdn.jindolf.parser.HtmlParseException;
23-import jp.osdn.jindolf.parser.HtmlParser;
24-import jp.osdn.jindolf.parser.PageType;
25-import jp.osdn.jindolf.parser.SeqRange;
26-import jp.osdn.jindolf.parser.content.DecodedContent;
27-import jp.sfjp.jindolf.net.HtmlSequence;
28-import jp.sfjp.jindolf.net.ServerAccess;
29-import jp.sfjp.jindolf.util.StringUtils;
30-import jp.sourceforge.jindolf.corelib.EventFamily;
31-import jp.sourceforge.jindolf.corelib.GameRole;
3215 import jp.sourceforge.jindolf.corelib.LandDef;
3316 import jp.sourceforge.jindolf.corelib.PeriodType;
3417 import jp.sourceforge.jindolf.corelib.SysEventType;
35-import jp.sourceforge.jindolf.corelib.TalkType;
36-import jp.sourceforge.jindolf.corelib.Team;
37-import jp.sourceforge.jindolf.corelib.VillageState;
3818
3919 /**
4020 * いわゆる「日」。
@@ -47,18 +27,6 @@ import jp.sourceforge.jindolf.corelib.VillageState;
4727 public class Period{
4828 // TODO Comparable も implement する?
4929
50- private static final HtmlParser PARSER = new HtmlParser();
51- private static final PeriodHandler HANDLER =
52- new PeriodHandler();
53-
54- private static final Logger LOGGER = Logger.getAnonymousLogger();
55-
56- static{
57- PARSER.setBasicHandler (HANDLER);
58- PARSER.setSysEventHandler(HANDLER);
59- PARSER.setTalkHandler (HANDLER);
60- }
61-
6230 private final Village homeVillage;
6331 private final PeriodType periodType;
6432 private final int day;
@@ -144,51 +112,6 @@ public class Period{
144112
145113
146114 /**
147- * Periodを更新する。Topicのリストが更新される。
148- * @param period 日
149- * @param force trueなら強制再読み込み。
150- * falseならまだ読み込んで無い時のみ読み込み。
151- * @throws IOException ネットワーク入力エラー
152- */
153- public static void parsePeriod(Period period, boolean force)
154- throws IOException{
155- if( ! force && period.hasLoaded() ) return;
156-
157- Village village = period.getVillage();
158- Land land = village.getParentLand();
159- ServerAccess server = land.getServerAccess();
160-
161- if(village.getState() != VillageState.PROGRESS){
162- period.isFullOpen = true;
163- }else if(period.getType() != PeriodType.PROGRESS){
164- period.isFullOpen = true;
165- }else{
166- period.isFullOpen = false;
167- }
168-
169- HtmlSequence html = server.getHTMLPeriod(period);
170-
171- period.topicList.clear();
172-
173- boolean wasHot = period.isHot();
174-
175- HANDLER.setPeriod(period);
176- DecodedContent content = html.getContent();
177- try{
178- PARSER.parseAutomatic(content);
179- }catch(HtmlParseException e){
180- LOGGER.log(Level.WARNING, "発言抽出に失敗", e);
181- }
182-
183- if(wasHot && ! period.isHot() ){
184- parsePeriod(period, true);
185- return;
186- }
187-
188- return;
189- }
190-
191- /**
192115 * 所属する村を返す。
193116 * @return 村
194117 */
@@ -216,6 +139,18 @@ public class Period{
216139 }
217140
218141 /**
142+ * 更新時刻を設定する。
143+ *
144+ * @param hour 時
145+ * @param minute 分
146+ */
147+ public void setLimit(int hour, int minute){
148+ this.limitHour = hour;
149+ this.limitMinute = minute;
150+ return;
151+ }
152+
153+ /**
219154 * 更新時刻の文字表記を返す。
220155 * @return 更新時刻の文字表記
221156 */
@@ -344,6 +279,14 @@ public class Period{
344279 }
345280
346281 /**
282+ * Topicのリスト内容を消す。
283+ */
284+ public void clearTopicList(){
285+ this.topicList.clear();
286+ return;
287+ }
288+
289+ /**
347290 * Periodに含まれるTopicの総数を返す。
348291 * @return Topic総数
349292 */
@@ -356,7 +299,7 @@ public class Period{
356299 * @param topic Topic
357300 * @throws java.lang.NullPointerException nullが渡された場合。
358301 */
359- protected void addTopic(Topic topic) throws NullPointerException{
302+ public void addTopic(Topic topic) throws NullPointerException{
360303 if(topic == null) throw new NullPointerException();
361304 this.topicList.add(topic);
362305 return;
@@ -398,6 +341,15 @@ public class Period{
398341 }
399342
400343 /**
344+ * ログイン名を設定する。
345+ * @param loginName ログイン名
346+ */
347+ public void setLoginName(String loginName){
348+ this.loginName = loginName;
349+ return;
350+ }
351+
352+ /**
401353 * 公開発言番号にマッチする発言を返す。
402354 * @param talkNo 公開発言番号
403355 * @return 発言。見つからなければnull
@@ -415,14 +367,33 @@ public class Period{
415367 }
416368
417369 /**
418- * このPeriodの内容にゲーム進行上隠された部分がある可能性を判定する。
419- * @return 隠れた要素がありうるならfalse
370+ * このPeriodの内容が全て公に開示されたものであるか判定する。
371+ *
372+ * <p>公に開示とは、非狼プレイヤーでも赤ログを閲覧できる状況を指す。
373+ *
374+ * <p>※ 2020-02の時点で、全てのPeriodは公に開示されているものとする。
375+ *
376+ * @return すべて開示されているならtrue
420377 */
421378 public boolean isFullOpen(){
422379 return this.isFullOpen;
423380 }
424381
425382 /**
383+ * このPeriodの内容が全て公に開示されたものであるか設定する。
384+ *
385+ * <p>公に開示とは、非狼プレイヤーでも赤ログを閲覧できる状況を指す。
386+ *
387+ * <p>※ 2020-02の時点で、全てのPeriodは公に開示されているものとする。
388+ *
389+ * @param fullOpen すべて開示されているならtrue
390+ */
391+ public void setFullOpen(boolean fullOpen){
392+ this.isFullOpen = fullOpen;
393+ return;
394+ }
395+
396+ /**
426397 * ロード済みか否かチェックする。
427398 * @return ロード済みならtrue
428399 */
@@ -516,735 +487,4 @@ public class Period{
516487 return null;
517488 }
518489
519- /**
520- * Periodパース用ハンドラ。
521- */
522- private static class PeriodHandler extends HtmlAdapter{
523-
524- private static final int TALKTYPE_NUM = TalkType.values().length;
525-
526- private final EntityConverter converter =
527- new EntityConverter(true);
528- // TODO: SMP面文字に彩色対応するまでの暫定措置
529-
530- private final Map<Avatar, int[]> countMap =
531- new HashMap<>();
532-
533- private Period period = null;
534-
535- private TalkType talkType;
536- private Avatar avatar;
537- private int talkNo;
538- private String anchorId;
539- private int talkHour;
540- private int talkMinute;
541- private DecodedContent talkContent = null;
542-
543- private EventFamily eventFamily;
544- private SysEventType sysEventType;
545- private DecodedContent eventContent = null;
546- private final List<Avatar> avatarList = new LinkedList<>();
547- private final List<GameRole> roleList = new LinkedList<>();
548- private final List<Integer> integerList = new LinkedList<>();
549- private final List<CharSequence> charseqList =
550- new LinkedList<>();
551-
552- /**
553- * コンストラクタ。
554- */
555- public PeriodHandler(){
556- super();
557- return;
558- }
559-
560- /**
561- * パース結果を格納するPeriodを設定する。
562- * @param period Period
563- */
564- public void setPeriod(Period period){
565- this.period = period;
566- return;
567- }
568-
569- /**
570- * 文字列断片からAvatarを得る。
571- * 村に未登録のAvatarであればついでに登録される。
572- * @param content 文字列
573- * @param range 文字列内のAvatarフルネームを示す領域
574- * @return Avatar
575- */
576- private Avatar toAvatar(DecodedContent content, SeqRange range){
577- Village village = this.period.getVillage();
578- String fullName = this.converter
579- .convert(content, range)
580- .toString();
581- Avatar result = village.getAvatar(fullName);
582- if(result == null){
583- result = new Avatar(fullName);
584- village.addAvatar(result);
585- }
586-
587- return result;
588- }
589-
590- /**
591- * Avatar別、会話種ごとに発言回数をカウントする。
592- * 1から始まる。
593- * @param targetAvatar 対象Avatar
594- * @param targetType 対象会話種
595- * @return カウント数
596- */
597- private int countUp(Avatar targetAvatar, TalkType targetType){
598- int[] countArray = this.countMap.get(targetAvatar);
599- if(countArray == null){
600- countArray = new int[TALKTYPE_NUM];
601- this.countMap.put(targetAvatar, countArray);
602- }
603- int count = ++countArray[targetType.ordinal()];
604- return count;
605- }
606-
607- /**
608- * {@inheritDoc}
609- * @param content {@inheritDoc}
610- * @throws HtmlParseException {@inheritDoc}
611- */
612- @Override
613- public void startParse(DecodedContent content)
614- throws HtmlParseException{
615- this.period.loginName = null;
616- this.period.topicList.clear();
617- this.countMap.clear();
618- return;
619- }
620-
621- /**
622- * {@inheritDoc}
623- * @param content {@inheritDoc}
624- * @param loginRange {@inheritDoc}
625- * @throws HtmlParseException {@inheritDoc}
626- */
627- @Override
628- public void loginName(DecodedContent content, SeqRange loginRange)
629- throws HtmlParseException{
630- DecodedContent loginName =
631- this.converter.convert(content, loginRange);
632-
633- this.period.loginName = loginName.toString();
634-
635- return;
636- }
637-
638- /**
639- * {@inheritDoc}
640- * @param type {@inheritDoc}
641- * @throws HtmlParseException {@inheritDoc}
642- */
643- @Override
644- public void pageType(PageType type) throws HtmlParseException{
645- if(type != PageType.PERIOD_PAGE){
646- throw new HtmlParseException(
647- "意図しないページを読み込もうとしました。");
648- }
649- return;
650- }
651-
652- /**
653- * {@inheritDoc}
654- * @param month {@inheritDoc}
655- * @param day {@inheritDoc}
656- * @param hour {@inheritDoc}
657- * @param minute {@inheritDoc}
658- * @throws HtmlParseException {@inheritDoc}
659- */
660- @Override
661- public void commitTime(int month, int day, int hour, int minute)
662- throws HtmlParseException{
663- this.period.limitHour = hour;
664- this.period.limitMinute = minute;
665- return;
666- }
667-
668- /**
669- * {@inheritDoc}
670- * 自分へのリンクが無いかチェックする。
671- * 自分へのリンクが見つかればこのPeriodを非Hotにする。
672- * 自分へのリンクがあるということは、
673- * 今読んでるHTMLは別のPeriodのために書かれたものということ。
674- * 考えられる原因は、HotだったPeriodがゲーム進行に従い
675- * Hotでなくなったこと。
676- * @param content {@inheritDoc}
677- * @param anchorRange {@inheritDoc}
678- * @param periodType {@inheritDoc}
679- * @param day {@inheritDoc}
680- * @throws HtmlParseException {@inheritDoc}
681- */
682- @Override
683- public void periodLink(DecodedContent content,
684- SeqRange anchorRange,
685- PeriodType periodType,
686- int day)
687- throws HtmlParseException{
688-
689- if(this.period.getType() != periodType) return;
690-
691- if( periodType == PeriodType.PROGRESS
692- && this.period.getDay() != day ){
693- return;
694- }
695-
696- if( ! anchorRange.isValid() ) return;
697-
698- this.period.setHot(false);
699-
700- return;
701- }
702-
703- /**
704- * {@inheritDoc}
705- * @throws HtmlParseException {@inheritDoc}
706- */
707- @Override
708- public void startTalk() throws HtmlParseException{
709- this.talkType = null;
710- this.avatar = null;
711- this.talkNo = -1;
712- this.anchorId = null;
713- this.talkHour = -1;
714- this.talkMinute = -1;
715- this.talkContent = new DecodedContent(100 + 1);
716-
717- return;
718- }
719-
720- /**
721- * {@inheritDoc}
722- * @param type {@inheritDoc}
723- * @throws HtmlParseException {@inheritDoc}
724- */
725- @Override
726- public void talkType(TalkType type)
727- throws HtmlParseException{
728- this.talkType = type;
729- return;
730- }
731-
732- /**
733- * {@inheritDoc}
734- * @param content {@inheritDoc}
735- * @param avatarRange {@inheritDoc}
736- * @throws HtmlParseException {@inheritDoc}
737- */
738- @Override
739- public void talkAvatar(DecodedContent content, SeqRange avatarRange)
740- throws HtmlParseException{
741- this.avatar = toAvatar(content, avatarRange);
742- return;
743- }
744-
745- /**
746- * {@inheritDoc}
747- * @param hour {@inheritDoc}
748- * @param minute {@inheritDoc}
749- * @throws HtmlParseException {@inheritDoc}
750- */
751- @Override
752- public void talkTime(int hour, int minute)
753- throws HtmlParseException{
754- this.talkHour = hour;
755- this.talkMinute = minute;
756- return;
757- }
758-
759- /**
760- * {@inheritDoc}
761- * @param tno {@inheritDoc}
762- * @throws HtmlParseException {@inheritDoc}
763- */
764- @Override
765- public void talkNo(int tno) throws HtmlParseException{
766- this.talkNo = tno;
767- return;
768- }
769-
770- /**
771- * {@inheritDoc}
772- * @param content {@inheritDoc}
773- * @param idRange {@inheritDoc}
774- * @throws HtmlParseException {@inheritDoc}
775- */
776- @Override
777- public void talkId(DecodedContent content, SeqRange idRange)
778- throws HtmlParseException{
779- this.anchorId = content.subSequence(idRange.getStartPos(),
780- idRange.getEndPos() )
781- .toString();
782- return;
783- }
784-
785- /**
786- * {@inheritDoc}
787- * @param content {@inheritDoc}
788- * @param textRange {@inheritDoc}
789- * @throws HtmlParseException {@inheritDoc}
790- */
791- @Override
792- public void talkText(DecodedContent content, SeqRange textRange)
793- throws HtmlParseException{
794- this.converter.append(this.talkContent, content, textRange);
795- return;
796- }
797-
798- /**
799- * {@inheritDoc}
800- * @throws HtmlParseException {@inheritDoc}
801- */
802- @Override
803- public void talkBreak()
804- throws HtmlParseException{
805- this.talkContent.append('\n');
806- return;
807- }
808-
809- /**
810- * {@inheritDoc}
811- * @throws HtmlParseException {@inheritDoc}
812- */
813- @Override
814- public void endTalk() throws HtmlParseException{
815- Talk talk = new Talk(this.period,
816- this.talkType,
817- this.avatar,
818- this.talkNo,
819- this.anchorId,
820- this.talkHour, this.talkMinute,
821- this.talkContent );
822-
823- int count = countUp(this.avatar, this.talkType);
824- talk.setCount(count);
825-
826- this.period.addTopic(talk);
827-
828- this.talkType = null;
829- this.avatar = null;
830- this.talkNo = -1;
831- this.anchorId = null;
832- this.talkHour = -1;
833- this.talkMinute = -1;
834- this.talkContent = null;
835-
836- return;
837- }
838-
839- /**
840- * {@inheritDoc}
841- * @param family {@inheritDoc}
842- * @throws HtmlParseException {@inheritDoc}
843- */
844- @Override
845- public void startSysEvent(EventFamily family)
846- throws HtmlParseException{
847- this.eventFamily = family;
848- this.sysEventType = null;
849- this.eventContent = new DecodedContent();
850- this.avatarList.clear();
851- this.roleList.clear();
852- this.integerList.clear();
853- this.charseqList.clear();
854- return;
855- }
856-
857- /**
858- * {@inheritDoc}
859- * @param type {@inheritDoc}
860- * @throws HtmlParseException {@inheritDoc}
861- */
862- @Override
863- public void sysEventType(SysEventType type)
864- throws HtmlParseException{
865- this.sysEventType = type;
866- return;
867- }
868-
869- /**
870- * {@inheritDoc}
871- * @param content {@inheritDoc}
872- * @param contentRange {@inheritDoc}
873- * @throws HtmlParseException {@inheritDoc}
874- */
875- @Override
876- public void sysEventContent(DecodedContent content,
877- SeqRange contentRange)
878- throws HtmlParseException{
879- this.converter.append(this.eventContent, content, contentRange);
880- return;
881- }
882-
883- /**
884- * {@inheritDoc}
885- * @param content {@inheritDoc}
886- * @param anchorRange {@inheritDoc}
887- * @param contentRange {@inheritDoc}
888- * @throws HtmlParseException {@inheritDoc}
889- */
890- @Override
891- public void sysEventContentAnchor(DecodedContent content,
892- SeqRange anchorRange,
893- SeqRange contentRange)
894- throws HtmlParseException{
895- this.converter.append(this.eventContent, content, contentRange);
896- return;
897- }
898-
899- /**
900- * {@inheritDoc}
901- * @throws HtmlParseException {@inheritDoc}
902- */
903- @Override
904- public void sysEventContentBreak() throws HtmlParseException{
905- this.eventContent.append('\n');
906- return;
907- }
908-
909- /**
910- * {@inheritDoc}
911- * @param content {@inheritDoc}
912- * @param entryNo {@inheritDoc}
913- * @param avatarRange {@inheritDoc}
914- * @throws HtmlParseException {@inheritDoc}
915- */
916- @Override
917- public void sysEventOnStage(DecodedContent content,
918- int entryNo,
919- SeqRange avatarRange)
920- throws HtmlParseException{
921- Avatar newAvatar = toAvatar(content, avatarRange);
922- this.integerList.add(entryNo);
923- this.avatarList.add(newAvatar);
924- return;
925- }
926-
927- /**
928- * {@inheritDoc}
929- * @param role {@inheritDoc}
930- * @param num {@inheritDoc}
931- * @throws HtmlParseException {@inheritDoc}
932- */
933- @Override
934- public void sysEventOpenRole(GameRole role, int num)
935- throws HtmlParseException{
936- this.roleList.add(role);
937- this.integerList.add(num);
938- return;
939- }
940-
941- /**
942- * {@inheritDoc}
943- * @param content {@inheritDoc}
944- * @param avatarRange {@inheritDoc}
945- * @throws HtmlParseException {@inheritDoc}
946- */
947- @Override
948- public void sysEventMurdered(DecodedContent content,
949- SeqRange avatarRange)
950- throws HtmlParseException{
951- Avatar murdered = toAvatar(content, avatarRange);
952- this.avatarList.add(murdered);
953- return;
954- }
955-
956- /**
957- * {@inheritDoc}
958- * @param content {@inheritDoc}
959- * @param avatarRange {@inheritDoc}
960- * @throws HtmlParseException {@inheritDoc}
961- */
962- @Override
963- public void sysEventSurvivor(DecodedContent content,
964- SeqRange avatarRange)
965- throws HtmlParseException{
966- Avatar survivor = toAvatar(content, avatarRange);
967- this.avatarList.add(survivor);
968- return;
969- }
970-
971- /**
972- * {@inheritDoc}
973- * @param content {@inheritDoc}
974- * @param voteByRange {@inheritDoc}
975- * @param voteToRange {@inheritDoc}
976- * @throws HtmlParseException {@inheritDoc}
977- */
978- @Override
979- public void sysEventCounting(DecodedContent content,
980- SeqRange voteByRange,
981- SeqRange voteToRange)
982- throws HtmlParseException{
983- if(voteByRange.isValid()){
984- Avatar voteBy = toAvatar(content, voteByRange);
985- this.avatarList.add(voteBy);
986- }
987- Avatar voteTo = toAvatar(content, voteToRange);
988- this.avatarList.add(voteTo);
989- return;
990- }
991-
992- /**
993- * {@inheritDoc}
994- * @param content {@inheritDoc}
995- * @param voteByRange {@inheritDoc}
996- * @param voteToRange {@inheritDoc}
997- * @throws HtmlParseException {@inheritDoc}
998- */
999- @Override
1000- public void sysEventCounting2(DecodedContent content,
1001- SeqRange voteByRange,
1002- SeqRange voteToRange)
1003- throws HtmlParseException{
1004- sysEventCounting(content, voteByRange, voteToRange);
1005- return;
1006- }
1007-
1008- /**
1009- * {@inheritDoc}
1010- * @param content {@inheritDoc}
1011- * @param avatarRange {@inheritDoc}
1012- * @throws HtmlParseException {@inheritDoc}
1013- */
1014- @Override
1015- public void sysEventSuddenDeath(DecodedContent content,
1016- SeqRange avatarRange)
1017- throws HtmlParseException{
1018- Avatar suddenDeath = toAvatar(content, avatarRange);
1019- this.avatarList.add(suddenDeath);
1020- return;
1021- }
1022-
1023- /**
1024- * {@inheritDoc}
1025- * @param content {@inheritDoc}
1026- * @param avatarRange {@inheritDoc}
1027- * @param anchorRange {@inheritDoc}
1028- * @param loginRange {@inheritDoc}
1029- * @param isLiving {@inheritDoc}
1030- * @param role {@inheritDoc}
1031- * @throws HtmlParseException {@inheritDoc}
1032- */
1033- @Override
1034- public void sysEventPlayerList(DecodedContent content,
1035- SeqRange avatarRange,
1036- SeqRange anchorRange,
1037- SeqRange loginRange,
1038- boolean isLiving,
1039- GameRole role )
1040- throws HtmlParseException{
1041- Avatar who = toAvatar(content, avatarRange);
1042-
1043- CharSequence anchor;
1044- if(anchorRange.isValid()){
1045- anchor = this.converter.convert(content, anchorRange);
1046- }else{
1047- anchor = "";
1048- }
1049- CharSequence account = this.converter
1050- .convert(content, loginRange);
1051-
1052- Integer liveOrDead;
1053- if(isLiving) liveOrDead = 1;
1054- else liveOrDead = 0;
1055-
1056- this.avatarList.add(who);
1057- this.charseqList.add(anchor);
1058- this.charseqList.add(account);
1059- this.integerList.add(liveOrDead);
1060- this.roleList.add(role);
1061-
1062- return;
1063- }
1064-
1065- /**
1066- * {@inheritDoc}
1067- * @param content {@inheritDoc}
1068- * @param avatarRange {@inheritDoc}
1069- * @param votes {@inheritDoc}
1070- * @throws HtmlParseException {@inheritDoc}
1071- */
1072- @Override
1073- public void sysEventExecution(DecodedContent content,
1074- SeqRange avatarRange,
1075- int votes )
1076- throws HtmlParseException{
1077- Avatar who = toAvatar(content, avatarRange);
1078-
1079- this.avatarList.add(who);
1080- this.integerList.add(votes);
1081-
1082- return;
1083- }
1084-
1085- /**
1086- * {@inheritDoc}
1087- * @param hour {@inheritDoc}
1088- * @param minute {@inheritDoc}
1089- * @param minLimit {@inheritDoc}
1090- * @param maxLimit {@inheritDoc}
1091- * @throws HtmlParseException {@inheritDoc}
1092- */
1093- @Override
1094- public void sysEventAskEntry(int hour, int minute,
1095- int minLimit, int maxLimit)
1096- throws HtmlParseException{
1097- this.integerList.add(hour * 60 + minute);
1098- this.integerList.add(minLimit);
1099- this.integerList.add(maxLimit);
1100- return;
1101- }
1102-
1103- /**
1104- * {@inheritDoc}
1105- * @param hour {@inheritDoc}
1106- * @param minute {@inheritDoc}
1107- * @throws HtmlParseException {@inheritDoc}
1108- */
1109- @Override
1110- public void sysEventAskCommit(int hour, int minute)
1111- throws HtmlParseException{
1112- this.integerList.add(hour * 60 + minute);
1113- return;
1114- }
1115-
1116- /**
1117- * {@inheritDoc}
1118- * @param content {@inheritDoc}
1119- * @param avatarRange {@inheritDoc}
1120- * @throws HtmlParseException {@inheritDoc}
1121- */
1122- @Override
1123- public void sysEventNoComment(DecodedContent content,
1124- SeqRange avatarRange)
1125- throws HtmlParseException{
1126- Avatar noComAvatar = toAvatar(content, avatarRange);
1127- this.avatarList.add(noComAvatar);
1128- return;
1129- }
1130-
1131- /**
1132- * {@inheritDoc}
1133- * @param winner {@inheritDoc}
1134- * @param hour {@inheritDoc}
1135- * @param minute {@inheritDoc}
1136- * @throws HtmlParseException {@inheritDoc}
1137- */
1138- @Override
1139- public void sysEventStayEpilogue(Team winner, int hour, int minute)
1140- throws HtmlParseException{
1141- GameRole role = null;
1142-
1143- switch(winner){
1144- case VILLAGE: role = GameRole.INNOCENT; break;
1145- case WOLF: role = GameRole.WOLF; break;
1146- case HAMSTER: role = GameRole.HAMSTER; break;
1147- default: assert false; break;
1148- }
1149-
1150- this.roleList.add(role);
1151- this.integerList.add(hour * 60 + minute);
1152-
1153- return;
1154- }
1155-
1156- /**
1157- * {@inheritDoc}
1158- * @param content {@inheritDoc}
1159- * @param guardByRange {@inheritDoc}
1160- * @param guardToRange {@inheritDoc}
1161- * @throws HtmlParseException {@inheritDoc}
1162- */
1163- @Override
1164- public void sysEventGuard(DecodedContent content,
1165- SeqRange guardByRange,
1166- SeqRange guardToRange)
1167- throws HtmlParseException{
1168- Avatar guardBy = toAvatar(content, guardByRange);
1169- Avatar guardTo = toAvatar(content, guardToRange);
1170- this.avatarList.add(guardBy);
1171- this.avatarList.add(guardTo);
1172- return;
1173- }
1174-
1175- /**
1176- * {@inheritDoc}
1177- * @param content {@inheritDoc}
1178- * @param judgeByRange {@inheritDoc}
1179- * @param judgeToRange {@inheritDoc}
1180- * @throws HtmlParseException {@inheritDoc}
1181- */
1182- @Override
1183- public void sysEventJudge(DecodedContent content,
1184- SeqRange judgeByRange,
1185- SeqRange judgeToRange)
1186- throws HtmlParseException{
1187- Avatar judgeBy = toAvatar(content, judgeByRange);
1188- Avatar judgeTo = toAvatar(content, judgeToRange);
1189- this.avatarList.add(judgeBy);
1190- this.avatarList.add(judgeTo);
1191- return;
1192- }
1193-
1194- /**
1195- * {@inheritDoc}
1196- * @throws HtmlParseException {@inheritDoc}
1197- */
1198- @Override
1199- public void endSysEvent() throws HtmlParseException{
1200- SysEvent event = new SysEvent();
1201- event.setEventFamily(this.eventFamily);
1202- event.setSysEventType(this.sysEventType);
1203- event.setContent(this.eventContent);
1204- event.addAvatarList(this.avatarList);
1205- event.addRoleList(this.roleList);
1206- event.addIntegerList(this.integerList);
1207- event.addCharSequenceList(this.charseqList);
1208-
1209- this.period.addTopic(event);
1210-
1211- if( this.sysEventType == SysEventType.MURDERED
1212- || this.sysEventType == SysEventType.NOMURDER ){
1213- for(Topic topic : this.period.topicList){
1214- if( ! (topic instanceof Talk) ) continue;
1215- Talk talk = (Talk) topic;
1216- if(talk.getTalkType() != TalkType.WOLFONLY) continue;
1217- if( ! StringUtils
1218- .isTerminated(talk.getDialog(),
1219- "!\u0020今日がお前の命日だ!") ){
1220- continue;
1221- }
1222- talk.setCount(-1);
1223- this.countMap.clear();
1224- }
1225- }
1226-
1227- this.eventFamily = null;
1228- this.sysEventType = null;
1229- this.eventContent = null;
1230- this.avatarList.clear();
1231- this.roleList.clear();
1232- this.integerList.clear();
1233- this.charseqList.clear();
1234-
1235- return;
1236- }
1237-
1238- /**
1239- * {@inheritDoc}
1240- * @throws HtmlParseException {@inheritDoc}
1241- */
1242- @Override
1243- public void endParse() throws HtmlParseException{
1244- return;
1245- }
1246-
1247- // TODO 村名のチェックは不要か?
1248- }
1249-
1250490 }
--- a/src/main/java/jp/sfjp/jindolf/data/Talk.java
+++ b/src/main/java/jp/sfjp/jindolf/data/Talk.java
@@ -7,6 +7,7 @@
77
88 package jp.sfjp.jindolf.data;
99
10+import jp.sfjp.jindolf.util.StringUtils;
1011 import jp.sourceforge.jindolf.corelib.TalkType;
1112
1213 /**
@@ -14,6 +15,9 @@ import jp.sourceforge.jindolf.corelib.TalkType;
1415 */
1516 public class Talk implements Topic{
1617
18+ private static final String MEINICHI_LAST = "!\u0020今日がお前の命日だ!";
19+
20+
1721 private final Period homePeriod;
1822 private final TalkType talkType;
1923 private final Avatar avatar;
@@ -28,6 +32,7 @@ public class Talk implements Topic{
2832
2933 /**
3034 * Talkの生成。
35+ *
3136 * @param homePeriod 発言元Period
3237 * @param talkType 発言種別
3338 * @param avatar Avatar
@@ -72,6 +77,7 @@ public class Talk implements Topic{
7277
7378 /**
7479 * 会話種別から色名への変換を行う。
80+ *
7581 * @param type 会話種別
7682 * @return 色名
7783 */
@@ -93,6 +99,7 @@ public class Talk implements Topic{
9399
94100 /**
95101 * 発言が交わされたPeriodを返す。
102+ *
96103 * @return Period
97104 */
98105 public Period getPeriod(){
@@ -101,6 +108,7 @@ public class Talk implements Topic{
101108
102109 /**
103110 * 発言種別を得る。
111+ *
104112 * @return 種別
105113 */
106114 public TalkType getTalkType(){
@@ -109,6 +117,7 @@ public class Talk implements Topic{
109117
110118 /**
111119 * 墓下発言か否か判定する。
120+ *
112121 * @return 墓下発言ならtrue
113122 */
114123 public boolean isGrave(){
@@ -116,8 +125,10 @@ public class Talk implements Topic{
116125 }
117126
118127 /**
119- * 発言種別ごとにその日(Period)の累積発言回数を返す。
120- * 1から始まる。
128+ * 各Avatarの発言種別ごとにその日(Period)の累積発言回数を返す。
129+ *
130+ * <p>システム生成の襲撃予告の場合は負の値となる。
131+ *
121132 * @return 累積発言回数。
122133 */
123134 public int getTalkCount(){
@@ -126,7 +137,9 @@ public class Talk implements Topic{
126137
127138 /**
128139 * 発言文字数を返す。
129- * 改行(\n)は1文字。
140+ *
141+ * <p>改行(\n)は1文字。
142+ *
130143 * @return 文字数
131144 */
132145 public int getTotalChars(){
@@ -135,6 +148,7 @@ public class Talk implements Topic{
135148
136149 /**
137150 * 発言元Avatarを得る。
151+ *
138152 * @return 発言元Avatar
139153 */
140154 public Avatar getAvatar(){
@@ -143,7 +157,9 @@ public class Talk implements Topic{
143157
144158 /**
145159 * 公開発言番号を取得する。
146- * 公開発言番号が割り振られてなければ0以下の値を返す。
160+ *
161+ * <p>公開発言番号が割り振られてなければ0以下の値を返す。
162+ *
147163 * @return 公開発言番号
148164 */
149165 public int getTalkNo(){
@@ -152,6 +168,7 @@ public class Talk implements Topic{
152168
153169 /**
154170 * 公開発言番号の有無を返す。
171+ *
155172 * @return 公開発言番号が割り当てられているならtrueを返す。
156173 */
157174 public boolean hasTalkNo(){
@@ -161,6 +178,7 @@ public class Talk implements Topic{
161178
162179 /**
163180 * メッセージIDを取得する。
181+ *
164182 * @return メッセージID
165183 */
166184 public String getMessageID(){
@@ -169,6 +187,7 @@ public class Talk implements Topic{
169187
170188 /**
171189 * メッセージIDからエポック秒(ms)に変換する。
190+ *
172191 * @return GMT 1970-01-01 00:00:00 からのエポック秒(ms)
173192 */
174193 public long getTimeFromID(){
@@ -179,6 +198,7 @@ public class Talk implements Topic{
179198
180199 /**
181200 * 発言時を取得する。
201+ *
182202 * @return 発言時
183203 */
184204 public int getHour(){
@@ -187,6 +207,7 @@ public class Talk implements Topic{
187207
188208 /**
189209 * 発言分を取得する。
210+ *
190211 * @return 発言分
191212 */
192213 public int getMinute(){
@@ -195,6 +216,7 @@ public class Talk implements Topic{
195216
196217 /**
197218 * 会話データを取得する。
219+ *
198220 * @return 会話データ
199221 */
200222 public CharSequence getDialog(){
@@ -203,6 +225,9 @@ public class Talk implements Topic{
203225
204226 /**
205227 * 発言種別ごとの発言回数を設定する。
228+ *
229+ * <p>システム生成の襲撃予告では負の値を入れれば良い。
230+ *
206231 * @param count 発言回数
207232 */
208233 public void setCount(int count){
@@ -212,7 +237,9 @@ public class Talk implements Topic{
212237
213238 /**
214239 * この会話を識別するためのアンカー文字列を生成する。
215- * 例えば「3d09:56」など。
240+ *
241+ * <p>例えば「3d09:56」など。
242+ *
216243 * @return アンカー文字列
217244 */
218245 public String getAnchorNotation(){
@@ -228,7 +255,9 @@ public class Talk implements Topic{
228255
229256 /**
230257 * この会話を識別するためのG国用アンカー文字列を発言番号から生成する。
231- * 例えば「{@literal >>172}」など。
258+ *
259+ * <p>例えば「{@literal >>172}」など。
260+ *
232261 * @return アンカー文字列。発言番号がなければ空文字列。
233262 */
234263 public String getAnchorNotation_G(){
@@ -237,6 +266,35 @@ public class Talk implements Topic{
237266 }
238267
239268 /**
269+ * 会話テキスト本文が襲撃予告たりうるか判定する。
270+ *
271+ * <p>Period開始時の襲撃予告の文面はシステムが生成する文書であり、
272+ * 狼プレイヤーの投稿に由来しない。
273+ *
274+ * <p>「! 今日がお前の命日だ!」で終わる赤ログは
275+ * 襲撃予告の可能性がある。
276+ *
277+ * <p>
278+ * {@link jp.sourceforge.jindolf.corelib.SysEventType#MURDERED}
279+ * もしくは
280+ * {@link jp.sourceforge.jindolf.corelib.SysEventType#NOMURDER}
281+ * の前に該当する赤ログが出現すれば、それは襲撃予告と断定して良い。
282+ *
283+ * @return 襲撃予告のテキストの可能性があるならtrue
284+ */
285+ public boolean isMurderNotice(){
286+ boolean isWolf;
287+ isWolf = this.talkType == TalkType.WOLFONLY;
288+ if( ! isWolf) return false;
289+
290+ boolean meinichida;
291+ meinichida = StringUtils.isTerminated(getDialog(), MEINICHI_LAST);
292+ if( ! meinichida) return false;
293+
294+ return true;
295+ }
296+
297+ /**
240298 * {@inheritDoc}
241299 * 会話のString表現を返す。
242300 * 実体参照やHTMLタグも含まれる。
--- a/src/main/java/jp/sfjp/jindolf/data/Village.java
+++ b/src/main/java/jp/sfjp/jindolf/data/Village.java
@@ -16,20 +16,9 @@ import java.util.HashMap;
1616 import java.util.LinkedList;
1717 import java.util.List;
1818 import java.util.Map;
19-import java.util.logging.Level;
20-import java.util.logging.Logger;
21-import jp.osdn.jindolf.parser.HtmlAdapter;
22-import jp.osdn.jindolf.parser.HtmlParseException;
23-import jp.osdn.jindolf.parser.HtmlParser;
24-import jp.osdn.jindolf.parser.PageType;
25-import jp.osdn.jindolf.parser.SeqRange;
26-import jp.osdn.jindolf.parser.content.DecodedContent;
27-import jp.sfjp.jindolf.net.HtmlSequence;
28-import jp.sfjp.jindolf.net.ServerAccess;
19+import jp.sfjp.jindolf.data.html.PeriodLoader;
2920 import jp.sfjp.jindolf.util.GUIUtils;
3021 import jp.sourceforge.jindolf.corelib.LandDef;
31-import jp.sourceforge.jindolf.corelib.LandState;
32-import jp.sourceforge.jindolf.corelib.PeriodType;
3322 import jp.sourceforge.jindolf.corelib.VillageState;
3423
3524 /**
@@ -42,18 +31,6 @@ public class Village implements Comparable<Village> {
4231 private static final Comparator<Village> VILLAGE_COMPARATOR =
4332 new VillageComparator();
4433
45- private static final HtmlParser PARSER = new HtmlParser();
46- private static final VillageHeadHandler HANDLER =
47- new VillageHeadHandler();
48-
49- private static final Logger LOGGER = Logger.getAnonymousLogger();
50-
51- static{
52- PARSER.setBasicHandler (HANDLER);
53- PARSER.setSysEventHandler(HANDLER);
54- PARSER.setTalkHandler (HANDLER);
55- }
56-
5734
5835 private final Land parentLand;
5936 private final String villageID;
@@ -113,36 +90,6 @@ public class Village implements Comparable<Village> {
11390 return VILLAGE_COMPARATOR;
11491 }
11592
116- /**
117- * 人狼BBSサーバからPeriod一覧情報が含まれたHTMLを取得し、
118- * Periodリストを更新する。
119- * @param village 村
120- * @throws java.io.IOException ネットワーク入出力の異常
121- */
122- public static synchronized void updateVillage(Village village)
123- throws IOException{
124- Land land = village.getParentLand();
125- LandDef landDef = land.getLandDef();
126- LandState landState = landDef.getLandState();
127- ServerAccess server = land.getServerAccess();
128-
129- HtmlSequence html;
130- if(landState == LandState.ACTIVE){
131- html = server.getHTMLBoneHead(village);
132- }else{
133- html = server.getHTMLVillage(village);
134- }
135-
136- DecodedContent content = html.getContent();
137- HANDLER.setVillage(village);
138- try{
139- PARSER.parseAutomatic(content);
140- }catch(HtmlParseException e){
141- LOGGER.log(Level.WARNING, "村の状態が不明", e);
142- }
143-
144- return;
145- }
14693
14794 /**
14895 * 所属する国を返す。
@@ -465,6 +412,22 @@ public class Village implements Comparable<Village> {
465412 }
466413
467414 /**
415+ * 次回更新時を設定する。
416+ *
417+ * @param month 月
418+ * @param day 日
419+ * @param hour 時
420+ * @param minute 分
421+ */
422+ public void setLimit(int month, int day, int hour, int minute){
423+ this.limitMonth = month;
424+ this.limitDay = day;
425+ this.limitHour = hour;
426+ this.limitMinute = minute;
427+ return;
428+ }
429+
430+ /**
468431 * 次回更新月を返す。
469432 * @return 更新月(1-12)
470433 */
@@ -512,7 +475,7 @@ public class Village implements Comparable<Village> {
512475 * @param period 上書きするPeriod
513476 * @throws java.lang.IndexOutOfBoundsException インデックスの指定がおかしい
514477 */
515- private void setPeriod(int index, Period period)
478+ public void setPeriod(int index, Period period)
516479 throws IndexOutOfBoundsException{
517480 int listSize = this.periodList.size();
518481 if(index == listSize){
@@ -549,7 +512,7 @@ public class Village implements Comparable<Village> {
549512 Period anchorPeriod = getPeriod(anchor);
550513 if(anchorPeriod == null) return result;
551514
552- Period.parsePeriod(anchorPeriod, false);
515+ PeriodLoader.parsePeriod(anchorPeriod, false);
553516
554517 for(Topic topic : anchorPeriod.getTopicList()){
555518 if( ! (topic instanceof Talk) ) continue;
@@ -627,244 +590,6 @@ public class Village implements Comparable<Village> {
627590
628591
629592 /**
630- * Period一覧取得用ハンドラ。
631- */
632- private static class VillageHeadHandler extends HtmlAdapter{
633-
634- private Village village = null;
635-
636- private boolean hasPrologue;
637- private boolean hasProgress;
638- private boolean hasEpilogue;
639- private boolean hasDone;
640- private int maxProgress;
641-
642- /**
643- * コンストラクタ。
644- */
645- public VillageHeadHandler(){
646- super();
647- return;
648- }
649-
650- /**
651- * 更新対象の村を設定する。
652- * @param village 村
653- */
654- public void setVillage(Village village){
655- this.village = village;
656- return;
657- }
658-
659- /**
660- * リセットを行う。
661- */
662- public void reset(){
663- this.hasPrologue = false;
664- this.hasProgress = false;
665- this.hasEpilogue = false;
666- this.hasDone = false;
667- this.maxProgress = 0;
668- return;
669- }
670-
671- /**
672- * パース結果から村の状態を算出する。
673- * @return 村の状態
674- */
675- public VillageState getVillageState(){
676- if(this.hasDone){
677- return VillageState.GAMEOVER;
678- }else if(this.hasEpilogue){
679- return VillageState.EPILOGUE;
680- }else if(this.hasProgress){
681- return VillageState.PROGRESS;
682- }else if(this.hasPrologue){
683- return VillageState.PROLOGUE;
684- }
685-
686- return VillageState.UNKNOWN;
687- }
688-
689- /**
690- * {@inheritDoc}
691- * @param content {@inheritDoc}
692- * @throws HtmlParseException {@inheritDoc}
693- */
694- @Override
695- public void startParse(DecodedContent content)
696- throws HtmlParseException{
697- reset();
698- return;
699- }
700-
701- /**
702- * {@inheritDoc}
703- * 自動判定の結果が日ページでなければ例外を投げる。
704- * @param type {@inheritDoc}
705- * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
706- */
707- @Override
708- public void pageType(PageType type) throws HtmlParseException{
709- if(type != PageType.PERIOD_PAGE){
710- throw new HtmlParseException(
711- "日ページが必要です。");
712- }
713- return;
714- }
715-
716- /**
717- * {@inheritDoc}
718- * @param month {@inheritDoc}
719- * @param day {@inheritDoc}
720- * @param hour {@inheritDoc}
721- * @param minute {@inheritDoc}
722- * @throws HtmlParseException {@inheritDoc}
723- */
724- @Override
725- public void commitTime(int month, int day,
726- int hour, int minute)
727- throws HtmlParseException{
728- this.village.limitMonth = month;
729- this.village.limitDay = day;
730- this.village.limitHour = hour;
731- this.village.limitMinute = minute;
732-
733- return;
734- }
735-
736- /**
737- * {@inheritDoc}
738- * @param content {@inheritDoc}
739- * @param anchorRange {@inheritDoc}
740- * @param periodType {@inheritDoc}
741- * @param day {@inheritDoc}
742- * @throws HtmlParseException {@inheritDoc}
743- */
744- @Override
745- public void periodLink(DecodedContent content,
746- SeqRange anchorRange,
747- PeriodType periodType,
748- int day)
749- throws HtmlParseException{
750- if(periodType == null){
751- this.hasDone = true;
752- return;
753- }
754-
755- switch(periodType){
756- case PROLOGUE:
757- this.hasPrologue = true;
758- break;
759- case PROGRESS:
760- this.hasProgress = true;
761- this.maxProgress = day;
762- break;
763- case EPILOGUE:
764- this.hasEpilogue = true;
765- break;
766- default:
767- assert false;
768- break;
769- }
770-
771- return;
772- }
773-
774- /**
775- * {@inheritDoc}
776- * @throws HtmlParseException {@inheritDoc}
777- */
778- @Override
779- public void endParse() throws HtmlParseException{
780- Land land = this.village.getParentLand();
781- LandDef landDef = land.getLandDef();
782- LandState landState = landDef.getLandState();
783-
784- VillageState villageState = getVillageState();
785- if(villageState == VillageState.UNKNOWN){
786- this.village.setState(villageState);
787- this.village.periodList.clear();
788- LOGGER.warning("村の状況を読み取れません");
789- return;
790- }
791-
792- if(landState == LandState.ACTIVE){
793- this.village.setState(villageState);
794- }else{
795- this.village.setState(VillageState.GAMEOVER);
796- }
797-
798- modifyPeriodList();
799-
800- return;
801- }
802-
803- /**
804- * 抽出したリンク情報に伴いPeriodリストを更新する。
805- * まだPeriodデータのロードは行われない。
806- * ゲーム進行中の村で更新時刻をまたいで更新が行われた場合、
807- * 既存のPeriodリストが伸張する場合がある。
808- */
809- private void modifyPeriodList(){
810- Period lastPeriod = null;
811-
812- if(this.hasPrologue){
813- Period prologue = this.village.getPrologue();
814- if(prologue == null){
815- lastPeriod = new Period(this.village,
816- PeriodType.PROLOGUE, 0);
817- this.village.setPeriod(0, lastPeriod);
818- }else{
819- lastPeriod = prologue;
820- }
821- }
822-
823- if(this.hasProgress){
824- for(int day = 1; day <= this.maxProgress; day++){
825- Period progress = this.village.getProgress(day);
826- if(progress == null){
827- lastPeriod = new Period(this.village,
828- PeriodType.PROGRESS, day);
829- this.village.setPeriod(day, lastPeriod);
830- }else{
831- lastPeriod = progress;
832- }
833- }
834- }
835-
836- if(this.hasEpilogue){
837- Period epilogue = this.village.getEpilogue();
838- if(epilogue == null){
839- lastPeriod = new Period(this.village,
840- PeriodType.EPILOGUE,
841- this.maxProgress +1);
842- this.village.setPeriod(this.maxProgress +1, lastPeriod);
843- }else{
844- lastPeriod = epilogue;
845- }
846- }
847-
848- assert this.village.getPeriodSize() > 0;
849- assert lastPeriod != null;
850-
851- // 念のためチョップ。
852- // リロードで村が縮むわけないじゃん。みんな大げさだなあ
853- while(this.village.periodList.getLast() != lastPeriod){
854- this.village.periodList.removeLast();
855- }
856-
857- if(this.village.getState() != VillageState.GAMEOVER){
858- lastPeriod.setHot(true);
859- }
860-
861- return;
862- }
863-
864- }
865-
866-
867- /**
868593 * 村同士を比較するためのComparator。
869594 */
870595 private static class VillageComparator implements Comparator<Village> {
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/data/html/PeriodHandler.java
@@ -0,0 +1,924 @@
1+/*
2+ * period handler
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2020 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf.data.html;
9+
10+import java.util.HashMap;
11+import java.util.LinkedList;
12+import java.util.List;
13+import java.util.Map;
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;
32+
33+
34+/**
35+ * 各日(Period)のHTMLをパースし、
36+ * 会話やイベントの通知を受け取るためのハンドラ。
37+ *
38+ * <p>パース終了時には、
39+ * あらかじめ指定したPeriodインスタンスに
40+ * 会話やイベントのリストが適切に更新される。
41+ *
42+ * <p>各種ビューが対応するまでの間、Unicodeの非BMP面文字には代替文字で対処。
43+ *
44+ * <p>※ 人狼BBS:G国におけるG2087村のエピローグが終了した段階で、
45+ * 人狼BBSは過去ログの提供しか行っていない。
46+ * だがこのクラスには進行中の村の各日をパースするための
47+ * 冗長な処理が若干残っている。
48+ */
49+class PeriodHandler extends HtmlAdapter {
50+
51+ private static final int TALKTYPE_NUM = TalkType.values().length;
52+
53+ private final EntityConverter converter =
54+ new EntityConverter(true);
55+ // TODO: 非BMP面文字に対応するまでの暫定措置
56+
57+ /** 非別、Avatar別、会話種別の会話通し番号。 */
58+ private final Map<Avatar, int[]> countMap =
59+ new HashMap<>();
60+
61+ private Period period = null;
62+
63+ private TalkType talkType;
64+ private Avatar avatar;
65+ private int talkNo;
66+ private String anchorId;
67+ private int talkHour;
68+ private int talkMinute;
69+ private DecodedContent talkContent = null;
70+
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 =
78+ new LinkedList<>();
79+
80+
81+ /**
82+ * コンストラクタ。
83+ */
84+ PeriodHandler(){
85+ super();
86+ return;
87+ }
88+
89+
90+ /**
91+ * 更新対象のPeriodを設定する。
92+ *
93+ * @param period Period
94+ */
95+ void setPeriod(Period period){
96+ this.period = period;
97+ return;
98+ }
99+
100+ /**
101+ * フルネーム文字列からAvatarインスタンスを得る。
102+ *
103+ * <p>村に未登録のAvatarであればついでに登録される。
104+ *
105+ * @param content 文字列
106+ * @param range 文字列内のAvatarフルネームを示す領域
107+ * @return Avatar
108+ */
109+ private Avatar toAvatar(DecodedContent content, SeqRange range){
110+ Village village = this.period.getVillage();
111+ String fullName = this.converter
112+ .convert(content, range)
113+ .toString();
114+ Avatar result = village.getAvatar(fullName);
115+ if(result == null){
116+ result = new Avatar(fullName);
117+ village.addAvatar(result);
118+ }
119+
120+ return result;
121+ }
122+
123+ /**
124+ * パース中の各種コンテキストをリセットする。
125+ */
126+ void reset(){
127+ this.countMap.clear();
128+
129+ resetTalkContext();
130+ resetEventContext();
131+
132+ return;
133+ }
134+
135+ /**
136+ * パース中の会話コンテキストをリセットする。
137+ */
138+ private void resetTalkContext(){
139+ this.talkType = null;
140+ this.avatar = null;
141+ this.talkNo = -1;
142+ this.anchorId = null;
143+ this.talkHour = -1;
144+ this.talkMinute = -1;
145+ this.talkContent = null;
146+ return;
147+ }
148+
149+ /**
150+ * パース中のイベントコンテキストをリセットする。
151+ */
152+ private void resetEventContext(){
153+ this.eventFamily = null;
154+ this.sysEventType = null;
155+ this.eventContent = null;
156+ this.avatarList.clear();
157+ this.roleList.clear();
158+ this.integerList.clear();
159+ this.charseqList.clear();
160+ return;
161+ }
162+
163+ /**
164+ * {@inheritDoc}
165+ *
166+ * @param content {@inheritDoc}
167+ * @throws HtmlParseException {@inheritDoc}
168+ */
169+ @Override
170+ public void startParse(DecodedContent content)
171+ throws HtmlParseException{
172+ reset();
173+
174+ this.period.setLoginName(null);
175+ this.period.clearTopicList();
176+
177+ return;
178+ }
179+
180+ /**
181+ * {@inheritDoc}
182+ *
183+ * <p>各PeriodのHTML上部にあるログイン名が通知されたのなら、
184+ * それはPOSTやCookieを使ってのログインに成功したと言うこと。
185+ *
186+ * <p>ログイン名中の文字実体参照は展開される。
187+ *
188+ * <p>※ 2020-02現在、人狼BBS各国へのログインは無意味。
189+ *
190+ * @param content {@inheritDoc}
191+ * @param loginRange {@inheritDoc}
192+ * @throws HtmlParseException {@inheritDoc}
193+ */
194+ @Override
195+ public void loginName(DecodedContent content, SeqRange loginRange)
196+ throws HtmlParseException{
197+ DecodedContent loginName =
198+ this.converter.convert(content, loginRange);
199+
200+ this.period.setLoginName(loginName.toString());
201+
202+ return;
203+ }
204+
205+ /**
206+ * {@inheritDoc}
207+ *
208+ * <p>受信したHTMLがPeriodページでないのならパースを中止する。
209+ *
210+ * @param type {@inheritDoc}
211+ * @throws HtmlParseException {@inheritDoc}
212+ */
213+ @Override
214+ public void pageType(PageType type) throws HtmlParseException{
215+ if(type != PageType.PERIOD_PAGE){
216+ throw new HtmlParseException(
217+ "意図しないページを読み込もうとしました。");
218+ }
219+ return;
220+ }
221+
222+ /**
223+ * {@inheritDoc}
224+ *
225+ * <p>月日の通知は無視される。
226+ *
227+ * @param month {@inheritDoc}
228+ * @param day {@inheritDoc}
229+ * @param hour {@inheritDoc}
230+ * @param minute {@inheritDoc}
231+ * @throws HtmlParseException {@inheritDoc}
232+ */
233+ @Override
234+ public void commitTime(int month, int day, int hour, int minute)
235+ throws HtmlParseException{
236+ this.period.setLimit(hour, minute);
237+ return;
238+ }
239+
240+ /**
241+ * {@inheritDoc}
242+ *
243+ * <p>このPeriodが進行中(Hot!)か否か判定する。
244+ *
245+ * <p>PeriodのHTML内に自分自身へのリンクが無いかチェックする。
246+ * 自分へのリンクが見つかればこのPeriodを非Hotにする。
247+ * 自分へのリンクがあるということは、
248+ * 今受信しているHTMLは別のPeriodから辿るために書かれたものということ。
249+ *
250+ * <p>原因としては、HotだったPeriodがゲーム進行に従い
251+ * Hotでなくなったことなどが考えられる。
252+ *
253+ * <p>各Periodの種別と日は、
254+ * 村情報受信を通じて事前に設定されていなければならない。
255+ *
256+ * <p>※ 2020-02現在、HotなPeriodを受信する機会はないはず。
257+ *
258+ * @param content {@inheritDoc}
259+ * @param anchorRange {@inheritDoc}
260+ * @param periodType {@inheritDoc}
261+ * @param day {@inheritDoc}
262+ * @throws HtmlParseException {@inheritDoc}
263+ */
264+ @Override
265+ public void periodLink(DecodedContent content,
266+ SeqRange anchorRange,
267+ PeriodType periodType,
268+ int day )
269+ throws HtmlParseException{
270+ if(this.period.getType() != periodType) return;
271+
272+ boolean isProgress = periodType == PeriodType.PROGRESS;
273+ boolean dayMatch = this.period.getDay() == day;
274+ if(isProgress && ! dayMatch){
275+ return;
276+ }
277+
278+ if( ! anchorRange.isValid() ) return;
279+
280+ this.period.setHot(false);
281+
282+ return;
283+ }
284+
285+ /**
286+ * {@inheritDoc}
287+ *
288+ * @throws HtmlParseException {@inheritDoc}
289+ */
290+ @Override
291+ public void startTalk() throws HtmlParseException{
292+ resetTalkContext();
293+ this.talkContent = new DecodedContent(100 + 1);
294+ return;
295+ }
296+
297+ /**
298+ * {@inheritDoc}
299+ *
300+ * @param type {@inheritDoc}
301+ * @throws HtmlParseException {@inheritDoc}
302+ */
303+ @Override
304+ public void talkType(TalkType type)
305+ throws HtmlParseException{
306+ this.talkType = type;
307+ return;
308+ }
309+
310+ /**
311+ * {@inheritDoc}
312+ *
313+ * @param content {@inheritDoc}
314+ * @param avatarRange {@inheritDoc}
315+ * @throws HtmlParseException {@inheritDoc}
316+ */
317+ @Override
318+ public void talkAvatar(DecodedContent content, SeqRange avatarRange)
319+ throws HtmlParseException{
320+ this.avatar = toAvatar(content, avatarRange);
321+ return;
322+ }
323+
324+ /**
325+ * {@inheritDoc}
326+ *
327+ * @param hour {@inheritDoc}
328+ * @param minute {@inheritDoc}
329+ * @throws HtmlParseException {@inheritDoc}
330+ */
331+ @Override
332+ public void talkTime(int hour, int minute)
333+ throws HtmlParseException{
334+ this.talkHour = hour;
335+ this.talkMinute = minute;
336+ return;
337+ }
338+
339+ /**
340+ * {@inheritDoc}
341+ *
342+ * @param tno {@inheritDoc}
343+ * @throws HtmlParseException {@inheritDoc}
344+ */
345+ @Override
346+ public void talkNo(int tno) throws HtmlParseException{
347+ this.talkNo = tno;
348+ return;
349+ }
350+
351+ /**
352+ * {@inheritDoc}
353+ *
354+ * @param content {@inheritDoc}
355+ * @param idRange {@inheritDoc}
356+ * @throws HtmlParseException {@inheritDoc}
357+ */
358+ @Override
359+ public void talkId(DecodedContent content, SeqRange idRange)
360+ throws HtmlParseException{
361+ this.anchorId = content.subSequence(idRange.getStartPos(),
362+ idRange.getEndPos() )
363+ .toString();
364+ return;
365+ }
366+
367+ /**
368+ * {@inheritDoc}
369+ *
370+ * <p>会話中の文字実体参照は展開される。
371+ *
372+ * @param content {@inheritDoc}
373+ * @param textRange {@inheritDoc}
374+ * @throws HtmlParseException {@inheritDoc}
375+ */
376+ @Override
377+ public void talkText(DecodedContent content, SeqRange textRange)
378+ throws HtmlParseException{
379+ this.converter.append(this.talkContent, content, textRange);
380+ return;
381+ }
382+
383+ /**
384+ * {@inheritDoc}
385+ *
386+ * @throws HtmlParseException {@inheritDoc}
387+ */
388+ @Override
389+ public void talkBreak()
390+ throws HtmlParseException{
391+ this.talkContent.append('\n');
392+ return;
393+ }
394+
395+ /**
396+ * 日別、Avatar別、会話種ごとに発言回数をインクリメントする。
397+ *
398+ * @param targetAvatar 対象Avatar
399+ * @param targetType 対象会話種
400+ * @return 現時点でのカウント数
401+ */
402+ private int countUp(Avatar targetAvatar, TalkType targetType){
403+ int[] countArray = this.countMap.get(targetAvatar);
404+ if(countArray == null){
405+ countArray = new int[TALKTYPE_NUM];
406+ this.countMap.put(targetAvatar, countArray);
407+ }
408+
409+ int typeIdx = targetType.ordinal();
410+ int count = ++countArray[typeIdx];
411+ return count;
412+ }
413+
414+ /**
415+ * {@inheritDoc}
416+ *
417+ * <p>パース中の各種コンテキストから会話を組み立て、
418+ * Periodに追加する。
419+ *
420+ * @throws HtmlParseException {@inheritDoc}
421+ */
422+ @Override
423+ public void endTalk() throws HtmlParseException{
424+ Talk talk = new Talk(this.period,
425+ this.talkType,
426+ this.avatar,
427+ this.talkNo,
428+ this.anchorId,
429+ this.talkHour, this.talkMinute,
430+ this.talkContent );
431+
432+ int count = countUp(this.avatar, this.talkType);
433+ talk.setCount(count);
434+
435+ this.period.addTopic(talk);
436+
437+ resetTalkContext();
438+
439+ return;
440+ }
441+
442+ /**
443+ * {@inheritDoc}
444+ *
445+ * @param family {@inheritDoc}
446+ * @throws HtmlParseException {@inheritDoc}
447+ */
448+ @Override
449+ public void startSysEvent(EventFamily family)
450+ throws HtmlParseException{
451+ resetEventContext();
452+
453+ this.eventFamily = family;
454+ this.eventContent = new DecodedContent();
455+
456+ return;
457+ }
458+
459+ /**
460+ * {@inheritDoc}
461+ *
462+ * @param type {@inheritDoc}
463+ * @throws HtmlParseException {@inheritDoc}
464+ */
465+ @Override
466+ public void sysEventType(SysEventType type)
467+ throws HtmlParseException{
468+ this.sysEventType = type;
469+ return;
470+ }
471+
472+ /**
473+ * {@inheritDoc}
474+ *
475+ * <p>イベント文字列中の文字実体参照は展開される。
476+ *
477+ * @param content {@inheritDoc}
478+ * @param contentRange {@inheritDoc}
479+ * @throws HtmlParseException {@inheritDoc}
480+ */
481+ @Override
482+ public void sysEventContent(DecodedContent content,
483+ SeqRange contentRange)
484+ throws HtmlParseException{
485+ this.converter.append(this.eventContent, content, contentRange);
486+ return;
487+ }
488+
489+ /**
490+ * {@inheritDoc}
491+ *
492+ * <p>イベント文内Aタグ内容の文字実体参照は展開される。
493+ * HREF属性値は無視される
494+ *
495+ * @param content {@inheritDoc}
496+ * @param anchorRange {@inheritDoc}
497+ * @param contentRange {@inheritDoc}
498+ * @throws HtmlParseException {@inheritDoc}
499+ */
500+ @Override
501+ public void sysEventContentAnchor(DecodedContent content,
502+ SeqRange anchorRange,
503+ SeqRange contentRange)
504+ throws HtmlParseException{
505+ this.converter.append(this.eventContent, content, contentRange);
506+ return;
507+ }
508+
509+ /**
510+ * {@inheritDoc}
511+ *
512+ * @throws HtmlParseException {@inheritDoc}
513+ */
514+ @Override
515+ public void sysEventContentBreak() throws HtmlParseException{
516+ this.eventContent.append('\n');
517+ return;
518+ }
519+
520+ /**
521+ * {@inheritDoc}
522+ *
523+ * <p>Avatarリストの先頭にAvatarが、
524+ * intリストの先頭にエントリー番号が入る。
525+ *
526+ * @param content {@inheritDoc}
527+ * @param entryNo {@inheritDoc}
528+ * @param avatarRange {@inheritDoc}
529+ * @throws HtmlParseException {@inheritDoc}
530+ */
531+ @Override
532+ public void sysEventOnStage(DecodedContent content,
533+ int entryNo,
534+ SeqRange avatarRange)
535+ throws HtmlParseException{
536+ Avatar newAvatar = toAvatar(content, avatarRange);
537+ this.integerList.add(entryNo);
538+ this.avatarList.add(newAvatar);
539+ return;
540+ }
541+
542+ /**
543+ * {@inheritDoc}
544+ *
545+ * <p>役職者数開示に伴い役職リストとintリストに一件ずつ追加される。
546+ *
547+ * @param role {@inheritDoc}
548+ * @param num {@inheritDoc}
549+ * @throws HtmlParseException {@inheritDoc}
550+ */
551+ @Override
552+ public void sysEventOpenRole(GameRole role, int num)
553+ throws HtmlParseException{
554+ this.roleList.add(role);
555+ this.integerList.add(num);
556+ return;
557+ }
558+
559+ /**
560+ * {@inheritDoc}
561+ *
562+ * <p>噛み及びハム溶けに伴いAvatarリストに1件ずつ追加される。
563+ *
564+ * @param content {@inheritDoc}
565+ * @param avatarRange {@inheritDoc}
566+ * @throws HtmlParseException {@inheritDoc}
567+ */
568+ @Override
569+ public void sysEventMurdered(DecodedContent content,
570+ SeqRange avatarRange)
571+ throws HtmlParseException{
572+ Avatar murdered = toAvatar(content, avatarRange);
573+ this.avatarList.add(murdered);
574+ return;
575+ }
576+
577+ /**
578+ * {@inheritDoc}
579+ *
580+ * <p>生存者表示に伴いAvatarリストに1件ずつ追加される。
581+ *
582+ * @param content {@inheritDoc}
583+ * @param avatarRange {@inheritDoc}
584+ * @throws HtmlParseException {@inheritDoc}
585+ */
586+ @Override
587+ public void sysEventSurvivor(DecodedContent content,
588+ SeqRange avatarRange)
589+ throws HtmlParseException{
590+ Avatar survivor = toAvatar(content, avatarRange);
591+ this.avatarList.add(survivor);
592+ return;
593+ }
594+
595+ /**
596+ * {@inheritDoc}
597+ *
598+ * <p>G国以外での処刑に伴い、
599+ * 投票元と投票先の順でAvatarリストに追加される。
600+ *
601+ * <p>被処刑者がいればAvatarリストの最後に追加される。
602+ *
603+ * @param content {@inheritDoc}
604+ * @param voteByRange {@inheritDoc}
605+ * @param voteToRange {@inheritDoc}
606+ * @throws HtmlParseException {@inheritDoc}
607+ */
608+ @Override
609+ public void sysEventCounting(DecodedContent content,
610+ SeqRange voteByRange,
611+ SeqRange voteToRange)
612+ throws HtmlParseException{
613+ if(voteByRange.isValid()){
614+ Avatar voteBy = toAvatar(content, voteByRange);
615+ this.avatarList.add(voteBy);
616+ }
617+ Avatar voteTo = toAvatar(content, voteToRange);
618+ this.avatarList.add(voteTo);
619+ return;
620+ }
621+
622+ /**
623+ * {@inheritDoc}
624+ *
625+ * <p>G国処刑に伴い、
626+ * 投票元と投票先の順でAvatarリストに追加される。
627+ *
628+ * @param content {@inheritDoc}
629+ * @param voteByRange {@inheritDoc}
630+ * @param voteToRange {@inheritDoc}
631+ * @throws HtmlParseException {@inheritDoc}
632+ */
633+ @Override
634+ public void sysEventCounting2(DecodedContent content,
635+ SeqRange voteByRange,
636+ SeqRange voteToRange)
637+ throws HtmlParseException{
638+ sysEventCounting(content, voteByRange, voteToRange);
639+ return;
640+ }
641+
642+ /**
643+ * {@inheritDoc}
644+ *
645+ * <p>Avatarリストの先頭に突然死者が入る。
646+ *
647+ * @param content {@inheritDoc}
648+ * @param avatarRange {@inheritDoc}
649+ * @throws HtmlParseException {@inheritDoc}
650+ */
651+ @Override
652+ public void sysEventSuddenDeath(DecodedContent content,
653+ SeqRange avatarRange)
654+ throws HtmlParseException{
655+ Avatar suddenDeath = toAvatar(content, avatarRange);
656+ this.avatarList.add(suddenDeath);
657+ return;
658+ }
659+
660+ /**
661+ * {@inheritDoc}
662+ *
663+ * <p>プレイヤー情報開示に伴い、
664+ * Avatarリストに1件、
665+ * 文字列リストにURLとプレイヤー名の2件、
666+ * intリストに生死(1or0)が1件、
667+ * Roleリストに役職が1件追加される。
668+ *
669+ * @param content {@inheritDoc}
670+ * @param avatarRange {@inheritDoc}
671+ * @param anchorRange {@inheritDoc}
672+ * @param loginRange {@inheritDoc}
673+ * @param isLiving {@inheritDoc}
674+ * @param role {@inheritDoc}
675+ * @throws HtmlParseException {@inheritDoc}
676+ */
677+ @Override
678+ public void sysEventPlayerList(DecodedContent content,
679+ SeqRange avatarRange,
680+ SeqRange anchorRange,
681+ SeqRange loginRange,
682+ boolean isLiving,
683+ GameRole role )
684+ throws HtmlParseException{
685+ Avatar who = toAvatar(content, avatarRange);
686+
687+ CharSequence anchor;
688+ if(anchorRange.isValid()){
689+ anchor = this.converter.convert(content, anchorRange);
690+ }else{
691+ anchor = "";
692+ }
693+ CharSequence account = this.converter
694+ .convert(content, loginRange);
695+
696+ Integer liveOrDead;
697+ if(isLiving) liveOrDead = 1;
698+ else liveOrDead = 0;
699+
700+ this.avatarList.add(who);
701+ this.charseqList.add(anchor);
702+ this.charseqList.add(account);
703+ this.integerList.add(liveOrDead);
704+ this.roleList.add(role);
705+
706+ return;
707+ }
708+
709+ /**
710+ * {@inheritDoc}
711+ *
712+ * <p>G国処刑に伴い、Avatarリストに投票先が1件、
713+ * intリストに得票数が1件追加される。
714+ * 最後に被処刑者がAvatarリストに1件、負の値がintリストに1件追加される。
715+ *
716+ * @param content {@inheritDoc}
717+ * @param avatarRange {@inheritDoc}
718+ * @param votes {@inheritDoc}
719+ * @throws HtmlParseException {@inheritDoc}
720+ */
721+ @Override
722+ public void sysEventExecution(DecodedContent content,
723+ SeqRange avatarRange,
724+ int votes )
725+ throws HtmlParseException{
726+ Avatar who = toAvatar(content, avatarRange);
727+
728+ this.avatarList.add(who);
729+ this.integerList.add(votes);
730+
731+ return;
732+ }
733+
734+ /**
735+ * {@inheritDoc}
736+ *
737+ * <p>エントリー促しに伴い、
738+ * intリストに分数、最小メンバ数、最大メンバ数の3件が設定される。
739+ *
740+ * @param hour {@inheritDoc}
741+ * @param minute {@inheritDoc}
742+ * @param minLimit {@inheritDoc}
743+ * @param maxLimit {@inheritDoc}
744+ * @throws HtmlParseException {@inheritDoc}
745+ */
746+ @Override
747+ public void sysEventAskEntry(int hour, int minute,
748+ int minLimit, int maxLimit)
749+ throws HtmlParseException{
750+ this.integerList.add(hour * 60 + minute);
751+ this.integerList.add(minLimit);
752+ this.integerList.add(maxLimit);
753+ return;
754+ }
755+
756+ /**
757+ * {@inheritDoc}
758+ *
759+ * <p>エントリー完了に伴い、分数をintリストに設定する。
760+ *
761+ * @param hour {@inheritDoc}
762+ * @param minute {@inheritDoc}
763+ * @throws HtmlParseException {@inheritDoc}
764+ */
765+ @Override
766+ public void sysEventAskCommit(int hour, int minute)
767+ throws HtmlParseException{
768+ this.integerList.add(hour * 60 + minute);
769+ return;
770+ }
771+
772+ /**
773+ * {@inheritDoc}
774+ *
775+ * <p>未発言者一覧に伴い、
776+ * 未発言者はAvatarリストへ1件ずつ追加される。
777+ *
778+ * @param content {@inheritDoc}
779+ * @param avatarRange {@inheritDoc}
780+ * @throws HtmlParseException {@inheritDoc}
781+ */
782+ @Override
783+ public void sysEventNoComment(DecodedContent content,
784+ SeqRange avatarRange)
785+ throws HtmlParseException{
786+ Avatar noComAvatar = toAvatar(content, avatarRange);
787+ this.avatarList.add(noComAvatar);
788+ return;
789+ }
790+
791+ /**
792+ * {@inheritDoc}
793+ *
794+ * <p>決着発表に伴い、
795+ * Roleリストに勝者が1件、intリスト分数が1件設定される。
796+ *
797+ * <p>村勝利の場合は素村役職が用いられる。
798+ *
799+ * @param winner {@inheritDoc}
800+ * @param hour {@inheritDoc}
801+ * @param minute {@inheritDoc}
802+ * @throws HtmlParseException {@inheritDoc}
803+ */
804+ @Override
805+ public void sysEventStayEpilogue(Team winner, int hour, int minute)
806+ throws HtmlParseException{
807+ GameRole role = null;
808+
809+ switch(winner){
810+ case VILLAGE: role = GameRole.INNOCENT; break;
811+ case WOLF: role = GameRole.WOLF; break;
812+ case HAMSTER: role = GameRole.HAMSTER; break;
813+ default: assert false; break;
814+ }
815+
816+ this.roleList.add(role);
817+ this.integerList.add(hour * 60 + minute);
818+
819+ return;
820+ }
821+
822+ /**
823+ * {@inheritDoc}
824+ *
825+ * <p>護衛に伴い、Avatarリストに護衛元1件と護衛先1件が設定される。
826+ *
827+ * @param content {@inheritDoc}
828+ * @param guardByRange {@inheritDoc}
829+ * @param guardToRange {@inheritDoc}
830+ * @throws HtmlParseException {@inheritDoc}
831+ */
832+ @Override
833+ public void sysEventGuard(DecodedContent content,
834+ SeqRange guardByRange,
835+ SeqRange guardToRange)
836+ throws HtmlParseException{
837+ Avatar guardBy = toAvatar(content, guardByRange);
838+ Avatar guardTo = toAvatar(content, guardToRange);
839+ this.avatarList.add(guardBy);
840+ this.avatarList.add(guardTo);
841+ return;
842+ }
843+
844+ /**
845+ * {@inheritDoc}
846+ *
847+ * <p>占いに伴い、
848+ * 占い元が1件、占い先が1件Avatarリストに設定される。
849+ *
850+ * @param content {@inheritDoc}
851+ * @param judgeByRange {@inheritDoc}
852+ * @param judgeToRange {@inheritDoc}
853+ * @throws HtmlParseException {@inheritDoc}
854+ */
855+ @Override
856+ public void sysEventJudge(DecodedContent content,
857+ SeqRange judgeByRange,
858+ SeqRange judgeToRange)
859+ throws HtmlParseException{
860+ Avatar judgeBy = toAvatar(content, judgeByRange);
861+ Avatar judgeTo = toAvatar(content, judgeToRange);
862+ this.avatarList.add(judgeBy);
863+ this.avatarList.add(judgeTo);
864+ return;
865+ }
866+
867+ /**
868+ * {@inheritDoc}
869+ *
870+ * <p>パースの完了した1件のイベントインスタンスを
871+ * Periodに追加する。
872+ *
873+ * <p>襲撃もしくは襲撃なしのイベントの前に、
874+ * 「今日がお前の命日だ!」で終わる赤ログが出現した場合、
875+ * 赤カウントに含めない。
876+ *
877+ * @throws HtmlParseException {@inheritDoc}
878+ */
879+ @Override
880+ public void endSysEvent() throws HtmlParseException{
881+ SysEvent event = new SysEvent();
882+ event.setEventFamily(this.eventFamily);
883+ event.setSysEventType(this.sysEventType);
884+ event.setContent(this.eventContent);
885+ event.addAvatarList(this.avatarList);
886+ event.addRoleList(this.roleList);
887+ event.addIntegerList(this.integerList);
888+ event.addCharSequenceList(this.charseqList);
889+
890+ this.period.addTopic(event);
891+
892+ boolean isMurderResult =
893+ this.sysEventType == SysEventType.MURDERED
894+ || this.sysEventType == SysEventType.NOMURDER;
895+
896+ if(isMurderResult){
897+ for(Topic topic : this.period.getTopicList()){
898+ if( ! (topic instanceof Talk) ) continue;
899+ Talk talk = (Talk) topic;
900+ if(talk.isMurderNotice()){
901+ talk.setCount(-1);
902+ this.countMap.clear();
903+ break;
904+ }
905+ }
906+ }
907+
908+ resetEventContext();
909+
910+ return;
911+ }
912+
913+ /**
914+ * {@inheritDoc}
915+ *
916+ * @throws HtmlParseException {@inheritDoc}
917+ */
918+ @Override
919+ public void endParse() throws HtmlParseException{
920+ reset();
921+ return;
922+ }
923+
924+}
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/data/html/PeriodLoader.java
@@ -0,0 +1,122 @@
1+/*
2+ * period loader
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2020 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf.data.html;
9+
10+import java.io.IOException;
11+import java.util.logging.Level;
12+import java.util.logging.Logger;
13+import jp.osdn.jindolf.parser.HtmlParseException;
14+import jp.osdn.jindolf.parser.HtmlParser;
15+import jp.osdn.jindolf.parser.content.DecodedContent;
16+import jp.sfjp.jindolf.data.Land;
17+import jp.sfjp.jindolf.data.Period;
18+import jp.sfjp.jindolf.data.Village;
19+import jp.sfjp.jindolf.net.HtmlSequence;
20+import jp.sfjp.jindolf.net.ServerAccess;
21+import jp.sourceforge.jindolf.corelib.PeriodType;
22+import jp.sourceforge.jindolf.corelib.VillageState;
23+
24+/**
25+ * 人狼各国のHTTPサーバから各村の個別の日(Period)をHTMLで取得する。
26+ *
27+ * <p>Periodには、プレイヤー同士の会話や
28+ * システムが自動生成するメッセージが正しい順序で納められる。
29+ *
30+ * <p>※ 人狼BBS:G国におけるG2087村のエピローグが終了した段階で、
31+ * 人狼BBSは過去ログの提供しか行っていない。
32+ * だがこのクラスには進行中の村の各日をパースするための
33+ * 冗長な処理(Hot判定、fullopen判定etc.)が若干残っている。
34+ */
35+public final class PeriodLoader {
36+
37+ private static final Logger LOGGER = Logger.getAnonymousLogger();
38+
39+ private static final HtmlParser PARSER;
40+ private static final PeriodHandler HANDLER;
41+
42+ static{
43+ PARSER = new HtmlParser();
44+ HANDLER = new PeriodHandler();
45+ PARSER.setBasicHandler (HANDLER);
46+ PARSER.setSysEventHandler(HANDLER);
47+ PARSER.setTalkHandler (HANDLER);
48+ }
49+
50+
51+ /**
52+ * hidden constructor.
53+ */
54+ private PeriodLoader(){
55+ assert false;
56+ }
57+
58+
59+ /**
60+ * Periodを更新する。Topicのリストが更新される。
61+ *
62+ * @param period 日
63+ * @param force trueなら強制再読み込み。
64+ * falseならまだ読み込んで無い時のみ読み込み。
65+ * @throws IOException ネットワーク入力エラー
66+ */
67+ public static void parsePeriod(Period period, boolean force)
68+ throws IOException{
69+ if( ! force && period.hasLoaded() ) return;
70+
71+ Village village = period.getVillage();
72+
73+ /*
74+ プレイ中の村でプロローグでもエピローグでもない日は
75+ 灰ログetc.の非開示情報が含まれる。
76+ ※ 2020-02の時点で非開示情報の含まれるPeriodは存在しない。
77+ (常にFullOpen)
78+ */
79+ boolean isOpen = true;
80+ if( village.getState() == VillageState.PROGRESS
81+ && period.getType() == PeriodType.PROGRESS ){
82+ isOpen = false;
83+ }
84+ period.setFullOpen(isOpen);
85+
86+ Land land = village.getParentLand();
87+ ServerAccess server = land.getServerAccess();
88+
89+ HtmlSequence html = server.getHTMLPeriod(period);
90+ DecodedContent content = html.getContent();
91+
92+ // 2020-02の時点でHotなPeriodは存在しない。
93+ boolean wasHot = period.isHot();
94+
95+ period.clearTopicList();
96+
97+ PARSER.reset();
98+ HANDLER.reset();
99+
100+ HANDLER.setPeriod(period);
101+ try{
102+ PARSER.parseAutomatic(content);
103+ }catch(HtmlParseException e){
104+ LOGGER.log(Level.WARNING, "発言抽出に失敗", e);
105+ }
106+
107+ PARSER.reset();
108+ HANDLER.reset();
109+
110+ /*
111+ 2020-02の時点で、
112+ 日付更新によるリロードを必要とするHotなPeriodは存在しない。
113+ */
114+ if(wasHot && ! period.isHot() ){
115+ parsePeriod(period, true);
116+ return;
117+ }
118+
119+ return;
120+ }
121+
122+}
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/data/html/VillageInfoHandler.java
@@ -0,0 +1,292 @@
1+/*
2+ * village info handler
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2020 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf.data.html;
9+
10+import java.util.logging.Logger;
11+import jp.osdn.jindolf.parser.HtmlAdapter;
12+import jp.osdn.jindolf.parser.HtmlParseException;
13+import jp.osdn.jindolf.parser.PageType;
14+import jp.osdn.jindolf.parser.SeqRange;
15+import jp.osdn.jindolf.parser.content.DecodedContent;
16+import jp.sfjp.jindolf.data.Land;
17+import jp.sfjp.jindolf.data.Period;
18+import jp.sfjp.jindolf.data.Village;
19+import jp.sourceforge.jindolf.corelib.LandDef;
20+import jp.sourceforge.jindolf.corelib.LandState;
21+import jp.sourceforge.jindolf.corelib.PeriodType;
22+import jp.sourceforge.jindolf.corelib.VillageState;
23+
24+
25+/**
26+ * 各村のHTMLをパースし、村情報や日程の通知を受け取るためのハンドラ。
27+ *
28+ * <p>パース終了時には、
29+ * あらかじめ指定したVillageインスタンスに
30+ * 更新時刻などの村情報が適切に更新される。
31+ *
32+ * <p>日程は空Periodのリストに反映されるが各Periodのロードはまだ行われない。
33+ *
34+ * <p>※人狼BBS:G国におけるG2087村のエピローグが終了した段階で、
35+ * 人狼BBSは過去ログの提供しか行っていない。
36+ * だがこのクラスには進行中の村をパースするための冗長な処理が若干残っている。
37+ */
38+class VillageInfoHandler extends HtmlAdapter {
39+
40+ private static final Logger LOGGER = Logger.getAnonymousLogger();
41+
42+
43+ private Village village = null;
44+
45+ private boolean hasPrologue;
46+ private boolean hasProgress;
47+ private boolean hasEpilogue;
48+
49+ private boolean hasDone;
50+ private int maxProgress;
51+
52+
53+ /**
54+ * コンストラクタ。
55+ */
56+ VillageInfoHandler(){
57+ super();
58+ return;
59+ }
60+
61+
62+ /**
63+ * 更新対象の村インスタンスを設定する。
64+ *
65+ * @param village 村インスタンス
66+ */
67+ void setVillage(Village village){
68+ this.village = village;
69+ reset();
70+ return;
71+ }
72+
73+ /**
74+ * 各種進行コンテキストのリセットを行う。
75+ */
76+ void reset() {
77+ this.hasPrologue = false;
78+ this.hasProgress = false;
79+ this.hasEpilogue = false;
80+ this.hasDone = false;
81+ this.maxProgress = 0;
82+ return;
83+ }
84+
85+ /**
86+ * パース結果から村の状態を算出する。
87+ *
88+ * @return 村の状態
89+ */
90+ private VillageState getVillageState() {
91+ if(this.hasDone){
92+ return VillageState.GAMEOVER;
93+ }else if(this.hasEpilogue){
94+ return VillageState.EPILOGUE;
95+ }else if(this.hasProgress){
96+ return VillageState.PROGRESS;
97+ }else if(this.hasPrologue){
98+ return VillageState.PROLOGUE;
99+ }
100+
101+ return VillageState.UNKNOWN;
102+ }
103+
104+ /**
105+ * {@inheritDoc}
106+ *
107+ * @param content {@inheritDoc}
108+ * @throws HtmlParseException {@inheritDoc}
109+ */
110+ @Override
111+ public void startParse(DecodedContent content)
112+ throws HtmlParseException {
113+ reset();
114+ return;
115+ }
116+
117+ /**
118+ * {@inheritDoc}
119+ *
120+ * <p>HTML自動判定の結果が村の日程ページでなければ例外を投げ、
121+ * パースを中止する。
122+ *
123+ * @param type {@inheritDoc}
124+ * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
125+ */
126+ @Override
127+ public void pageType(PageType type) throws HtmlParseException {
128+ if(type != PageType.PERIOD_PAGE){
129+ throw new HtmlParseException("日ページが必要です。");
130+ }
131+ return;
132+ }
133+
134+ /**
135+ * {@inheritDoc}
136+ *
137+ * <p>更新時刻の通知を受け取る。
138+ * 更新時刻はVillageインスタンスへ反映される。
139+ *
140+ * @param month {@inheritDoc}
141+ * @param day {@inheritDoc}
142+ * @param hour {@inheritDoc}
143+ * @param minute {@inheritDoc}
144+ * @throws HtmlParseException {@inheritDoc}
145+ */
146+ @Override
147+ public void commitTime(int month, int day, int hour, int minute)
148+ throws HtmlParseException {
149+ this.village.setLimit(month, day, hour, minute);
150+ return;
151+ }
152+
153+ /**
154+ * {@inheritDoc}
155+ *
156+ * <p>日程ページから各Period(日)へのリンクHTML出現の通知を受け取る。
157+ * Villageインスタンスの進行状況へ反映される。
158+ *
159+ * @param content {@inheritDoc}
160+ * @param anchorRange {@inheritDoc}
161+ * @param periodType {@inheritDoc}
162+ * @param day {@inheritDoc}
163+ * @throws HtmlParseException {@inheritDoc}
164+ */
165+ @Override
166+ public void periodLink(DecodedContent content,
167+ SeqRange anchorRange,
168+ PeriodType periodType,
169+ int day)
170+ throws HtmlParseException {
171+ if(periodType == null){
172+ this.hasDone = true;
173+ return;
174+ }
175+
176+ switch(periodType){
177+ case PROLOGUE:
178+ this.hasPrologue = true;
179+ break;
180+ case PROGRESS:
181+ this.hasProgress = true;
182+ this.maxProgress = day;
183+ break;
184+ case EPILOGUE:
185+ this.hasEpilogue = true;
186+ break;
187+ default:
188+ assert false;
189+ break;
190+ }
191+
192+ return;
193+ }
194+
195+ /**
196+ * {@inheritDoc}
197+ *
198+ * <p>パース終了時の処理を行う。
199+ *
200+ * <p>村としての体裁に矛盾が検出されると、
201+ * 例外を投げパースを中断する。
202+ *
203+ * <p>村の進行に従い空Periodのリストを生成する。
204+ *
205+ * @throws HtmlParseException {@inheritDoc}
206+ */
207+ @Override
208+ public void endParse() throws HtmlParseException {
209+ VillageState villageState = getVillageState();
210+ if(villageState == VillageState.UNKNOWN){
211+ this.village.setState(villageState);
212+ LOGGER.warning("村の状況を読み取れません");
213+ return;
214+ }
215+
216+ Land land = this.village.getParentLand();
217+ LandDef landDef = land.getLandDef();
218+ LandState landState = landDef.getLandState();
219+
220+ if(landState == LandState.ACTIVE){
221+ this.village.setState(villageState);
222+ }else{
223+ this.village.setState(VillageState.GAMEOVER);
224+ }
225+
226+ modifyPeriodList();
227+
228+ return;
229+ }
230+
231+ /**
232+ * 抽出したPeriod別リンク情報に伴い空Periodリストを準備する。
233+ *
234+ * <p>まだPeriodデータのロードは行われない。
235+ *
236+ * <p>ゲーム進行中の村で更新時刻をまたいで更新が行われた場合、
237+ * 既存のPeriodリストが伸張する場合がある。
238+ */
239+ private void modifyPeriodList() {
240+ Period lastPeriod = null;
241+ if(this.hasPrologue){
242+ Period prologue = this.village.getPrologue();
243+ if(prologue == null){
244+ lastPeriod =
245+ new Period(this.village,
246+ PeriodType.PROLOGUE,
247+ 0 );
248+ this.village.setPeriod(0, lastPeriod);
249+ }else{
250+ lastPeriod = prologue;
251+ }
252+ }
253+
254+ if(this.hasProgress){
255+ for(int day = 1; day <= this.maxProgress; day++){
256+ Period progress = this.village.getProgress(day);
257+ if(progress == null){
258+ lastPeriod =
259+ new Period(this.village,
260+ PeriodType.PROGRESS,
261+ day );
262+ this.village.setPeriod(day, lastPeriod);
263+ }else{
264+ lastPeriod = progress;
265+ }
266+ }
267+ }
268+
269+ if(this.hasEpilogue){
270+ Period epilogue = this.village.getEpilogue();
271+ if(epilogue == null){
272+ lastPeriod =
273+ new Period(this.village,
274+ PeriodType.EPILOGUE,
275+ this.maxProgress + 1 );
276+ this.village.setPeriod(this.maxProgress + 1, lastPeriod);
277+ } else {
278+ lastPeriod = epilogue;
279+ }
280+ }
281+
282+ assert this.village.getPeriodSize() > 0;
283+ assert lastPeriod != null;
284+
285+ if(this.village.getState() != VillageState.GAMEOVER){
286+ lastPeriod.setHot(true);
287+ }
288+
289+ return;
290+ }
291+
292+}
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/data/html/VillageInfoLoader.java
@@ -0,0 +1,120 @@
1+/*
2+ * village information loader
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2008 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf.data.html;
9+
10+import java.io.IOException;
11+import java.util.logging.Level;
12+import java.util.logging.Logger;
13+import jp.osdn.jindolf.parser.HtmlParseException;
14+import jp.osdn.jindolf.parser.HtmlParser;
15+import jp.osdn.jindolf.parser.content.DecodedContent;
16+import jp.sfjp.jindolf.data.Land;
17+import jp.sfjp.jindolf.data.Village;
18+import jp.sfjp.jindolf.net.HtmlSequence;
19+import jp.sfjp.jindolf.net.ServerAccess;
20+import jp.sourceforge.jindolf.corelib.LandDef;
21+import jp.sourceforge.jindolf.corelib.LandState;
22+
23+/**
24+ * 人狼各国のHTTPサーバから個別の村の村情報をHTMLで取得する。
25+ *
26+ * <p>村情報には村毎の更新時刻、日程、進行状況などが含まれる。
27+ *
28+ * <p>各Periodの会話はまだロードされない。
29+ */
30+public final class VillageInfoLoader {
31+
32+ private static final Logger LOGGER = Logger.getAnonymousLogger();
33+
34+ private static final HtmlParser PARSER;
35+ private static final VillageInfoHandler HANDLER;
36+
37+ static{
38+ PARSER = new HtmlParser();
39+ HANDLER = new VillageInfoHandler();
40+ PARSER.setBasicHandler (HANDLER);
41+ PARSER.setSysEventHandler(HANDLER);
42+ PARSER.setTalkHandler (HANDLER);
43+ }
44+
45+
46+ /**
47+ * Hidden constructor.
48+ */
49+ private VillageInfoLoader() {
50+ assert false;
51+ }
52+
53+
54+ /**
55+ * 人狼BBSサーバから
56+ * HTMLで記述された各村の村情報ページをダウンロードする。
57+ *
58+ * <p>村情報ページのURLは各国の状態及び村の進行状況により異なる。
59+ *
60+ * <p>※ G国HISTORICAL運用移行に伴い、
61+ * 2020-02の時点で進行中の村はもはや存在しないため、
62+ * 若干の冗長なコードが残存する。
63+ *
64+ * <p>例: G1000村(エピローグ終了状態)の村情報ページは
65+ * <a href="http://www.wolfg.x0.com/index.rb?vid=1000">
66+ * http://www.wolfg.x0.com/index.rb?vid=1000</a>
67+ *
68+ * @param village 村
69+ * @return HTML文書
70+ * @throws IOException 入出力エラー
71+ */
72+ private static DecodedContent loadVillageInfo(Village village)
73+ throws IOException{
74+ Land land = village.getParentLand();
75+ ServerAccess server = land.getServerAccess();
76+ LandDef landDef = land.getLandDef();
77+ LandState landState = landDef.getLandState();
78+
79+ HtmlSequence html;
80+ if(landState == LandState.ACTIVE){
81+ html = server.getHTMLBoneHead(village);
82+ }else{
83+ html = server.getHTMLVillage(village);
84+ }
85+
86+ DecodedContent content = html.getContent();
87+
88+ return content;
89+ }
90+
91+ /**
92+ * 人狼BBSサーバから各村のPeriod一覧情報が含まれたHTML(村情報)を取得し、
93+ * 更新時刻や日程、空PeriodのリストをVillageインスタンスに設定する。
94+ *
95+ * @param village 村
96+ * @throws java.io.IOException ネットワーク入出力の異常
97+ */
98+ public static void updateVillageInfo(Village village)
99+ throws IOException{
100+ DecodedContent content = loadVillageInfo(village);
101+
102+ synchronized(PARSER){
103+ PARSER.reset();
104+ HANDLER.reset();
105+
106+ HANDLER.setVillage(village);
107+ try{
108+ PARSER.parseAutomatic(content);
109+ }catch(HtmlParseException e){
110+ LOGGER.log(Level.WARNING, "村の状態が不明", e);
111+ }
112+
113+ PARSER.reset();
114+ HANDLER.reset();
115+ }
116+
117+ return;
118+ }
119+
120+}
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/data/html/VillageListHandler.java
@@ -0,0 +1,160 @@
1+/*
2+ * village list handler
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2008 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf.data.html;
9+
10+import java.util.LinkedList;
11+import java.util.List;
12+import java.util.logging.Level;
13+import java.util.logging.Logger;
14+import java.util.regex.Matcher;
15+import java.util.regex.Pattern;
16+import jp.osdn.jindolf.parser.HtmlAdapter;
17+import jp.osdn.jindolf.parser.HtmlParseException;
18+import jp.osdn.jindolf.parser.PageType;
19+import jp.osdn.jindolf.parser.SeqRange;
20+import jp.osdn.jindolf.parser.content.DecodedContent;
21+import jp.sourceforge.jindolf.corelib.VillageState;
22+
23+/**
24+ * 各国の村一覧HTMLをパースし、村一覧通知を受け取るためのハンドラ。
25+ *
26+ * <p>パース終了時には村一覧リストが完成する。
27+ */
28+class VillageListHandler extends HtmlAdapter{
29+
30+ private static final Logger LOGGER = Logger.getAnonymousLogger();
31+
32+ private static final String ERR_ILLEGALPAGE =
33+ "トップページか村一覧ページが必要です。";
34+ private static final String ERR_URI =
35+ "認識できないURL[{0}]に遭遇しました。";
36+
37+ private static final Pattern REG_VID = Pattern.compile(
38+ "\\Qindex.rb?vid=\\E" + "([1-9][0-9]*)" + "\\Q&amp;\\E");
39+
40+
41+ private final List<VillageRecord> villageRecords = new LinkedList<>();
42+
43+
44+ /**
45+ * コンストラクタ。
46+ */
47+ VillageListHandler() {
48+ super();
49+ return;
50+ }
51+
52+
53+ /**
54+ * HTMLのAタグ内HREF属性値から村IDを得る。
55+ *
56+ * @param hrefValue HREF属性値
57+ * @return 村ID。見つからなければnull。
58+ */
59+ static String parseVidFromHref(CharSequence hrefValue){
60+ Matcher matcher = REG_VID.matcher(hrefValue);
61+ boolean match = matcher.lookingAt();
62+ if(!match) return null;
63+
64+ String result = matcher.group(1);
65+ return result;
66+ }
67+
68+
69+ /**
70+ * パース結果の村一覧を返す。
71+ *
72+ * @return 村一覧
73+ */
74+ List<VillageRecord> getVillageRecords(){
75+ return this.villageRecords;
76+ }
77+
78+ /**
79+ * リセットを行う。
80+ *
81+ * <p>村一覧リストは空になる。
82+ */
83+ void reset() {
84+ this.villageRecords.clear();
85+ return;
86+ }
87+
88+ /**
89+ * {@inheritDoc}
90+ *
91+ * <p>パース開始通知を受け、村一覧リストを初期化する。
92+ *
93+ * @param content {@inheritDoc}
94+ * @throws HtmlParseException {@inheritDoc}
95+ */
96+ @Override
97+ public void startParse(DecodedContent content) throws HtmlParseException {
98+ reset();
99+ return;
100+ }
101+
102+ /**
103+ * {@inheritDoc}
104+ *
105+ * <p>ページ自動判定の結果の通知を受け、
106+ * パース対象HTMLがトップページでも村一覧ページでもなければ
107+ * 例外を投げパースを中止させる。
108+ *
109+ * @param type {@inheritDoc}
110+ * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
111+ */
112+ @Override
113+ public void pageType(PageType type) throws HtmlParseException {
114+ if( type != PageType.VILLAGELIST_PAGE
115+ && type != PageType.TOP_PAGE ){
116+ throw new HtmlParseException(ERR_ILLEGALPAGE);
117+ }
118+ return;
119+ }
120+
121+ /**
122+ * {@inheritDoc}
123+ *
124+ * <p>村URL出現の通知を受け、村一覧リストに村を追加する。
125+ *
126+ * @param content {@inheritDoc}
127+ * @param anchorRange {@inheritDoc}
128+ * @param villageRange {@inheritDoc}
129+ * @param hour {@inheritDoc}
130+ * @param minute {@inheritDoc}
131+ * @param villageState {@inheritDoc}
132+ * @throws HtmlParseException {@inheritDoc}
133+ */
134+ @Override
135+ public void villageRecord(DecodedContent content,
136+ SeqRange anchorRange,
137+ SeqRange villageRange,
138+ int hour, int minute,
139+ VillageState villageState)
140+ throws HtmlParseException {
141+ CharSequence href = anchorRange.sliceSequence(content);
142+ String villageID = parseVidFromHref(href);
143+ if(villageID == null || villageID.length() <= 0){
144+ LOGGER.log(Level.WARNING, ERR_URI, href);
145+ return;
146+ }
147+
148+ CharSequence fullVillageName = villageRange.sliceSequence(content);
149+
150+ VillageRecord record =
151+ new VillageRecord(villageID,
152+ fullVillageName.toString(),
153+ villageState );
154+
155+ this.villageRecords.add(record);
156+
157+ return;
158+ }
159+
160+}
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/data/html/VillageListLoader.java
@@ -0,0 +1,177 @@
1+/*
2+ * village list loader
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2008 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf.data.html;
9+
10+import java.io.IOException;
11+import java.util.ArrayList;
12+import java.util.Collections;
13+import java.util.LinkedList;
14+import java.util.List;
15+import java.util.SortedSet;
16+import java.util.TreeSet;
17+import java.util.logging.Level;
18+import java.util.logging.Logger;
19+import jp.osdn.jindolf.parser.HtmlParseException;
20+import jp.osdn.jindolf.parser.HtmlParser;
21+import jp.osdn.jindolf.parser.content.DecodedContent;
22+import jp.sfjp.jindolf.data.Land;
23+import jp.sfjp.jindolf.data.Village;
24+import jp.sfjp.jindolf.net.HtmlSequence;
25+import jp.sfjp.jindolf.net.ServerAccess;
26+import jp.sourceforge.jindolf.corelib.LandDef;
27+import jp.sourceforge.jindolf.corelib.LandState;
28+import jp.sourceforge.jindolf.corelib.VillageState;
29+
30+/**
31+ * 人狼各国のHTTPサーバから村一覧リストを取得する。
32+ */
33+public final class VillageListLoader {
34+
35+ private static final Logger LOGGER = Logger.getAnonymousLogger();
36+
37+ // 古国ID
38+ private static final String ID_VANILLAWOLF = "wolf";
39+
40+ private static final List<VillageRecord> EMPTY_LIST =
41+ Collections.emptyList();
42+
43+
44+ /**
45+ * Hidden constructor.
46+ */
47+ private VillageListLoader() {
48+ assert false;
49+ }
50+
51+
52+ /**
53+ * 村一覧リストをサーバからダウンロードする。
54+ *
55+ * <p>リスト元情報は国のトップページと村一覧ページ。
56+ *
57+ * <p>古国(wolf)の場合は村一覧にアクセスせずトップページのみ。
58+ * 古国以外で村建てをやめた国はトップページにアクセスしない。
59+ *
60+ * <p>戻される村一覧リストはソート済みで重複がない。
61+ *
62+ * @param land 国
63+ * @return 村一覧リスト
64+ * @throws java.io.IOException ネットワーク入出力の異常
65+ */
66+ public static List<Village> loadVillageList(Land land)
67+ throws IOException{
68+ List<VillageRecord> records = loadVillageRecords(land);
69+
70+ LandDef landDef = land.getLandDef();
71+ LandState landState = landDef.getLandState();
72+ boolean isHistorical = landState == LandState.HISTORICAL;
73+
74+ List<Village> vList = new ArrayList<>(records.size());
75+
76+ for(VillageRecord record : records){
77+ String id = record.getVillageId();
78+ String fullVillageName = record.getFullVillageName();
79+
80+ VillageState status;
81+ if(isHistorical){
82+ status = VillageState.GAMEOVER;
83+ }else{
84+ status = record.getVillageStatus();
85+ }
86+
87+ Village village = new Village(land, id, fullVillageName);
88+ village.setState(status);
89+
90+ vList.add(village);
91+ }
92+
93+ // たまに同じ村が複数回出現するので注意!
94+ SortedSet<Village> uniq = new TreeSet<>(vList);
95+ List<Village> result = new ArrayList<>(uniq);
96+
97+ return result;
98+ }
99+
100+ /**
101+ * 村一覧リストをサーバからダウンロードする。
102+ *
103+ * <p>リスト元情報は国のトップページと村一覧ページ。
104+ *
105+ * <p>古国(wolf)の場合は村一覧にアクセスせずトップページのみ。
106+ * 古国以外で村建てをやめた国はトップページにアクセスしない。
107+ *
108+ * <p>戻される村一覧リストは順不同で重複もありうる。
109+ *
110+ * @param land 国
111+ * @return 村一覧リスト
112+ * @throws java.io.IOException ネットワーク入出力の異常
113+ */
114+ private static List<VillageRecord> loadVillageRecords(Land land)
115+ throws IOException{
116+ List<VillageRecord> totalList = new LinkedList<>();
117+ List<VillageRecord> recList;
118+
119+ LandDef landDef = land.getLandDef();
120+ boolean isVanillaWolf = landDef.getLandId().equals(ID_VANILLAWOLF);
121+ LandState state = landDef.getLandState();
122+
123+ boolean needTopPage =
124+ state.equals(LandState.ACTIVE) || isVanillaWolf;
125+ boolean hasVillageList = ! isVanillaWolf;
126+
127+ ServerAccess server = land.getServerAccess();
128+
129+ // トップページ
130+ if(needTopPage){
131+ recList = EMPTY_LIST;
132+ HtmlSequence html = server.getHTMLTopPage();
133+ try{
134+ recList = parseVillageRecords(html);
135+ }catch(HtmlParseException e){
136+ LOGGER.log(Level.WARNING, "トップページを認識できない", e);
137+ }
138+ totalList.addAll(recList);
139+ }
140+
141+ // 村一覧ページ
142+ if(hasVillageList){
143+ recList = EMPTY_LIST;
144+ HtmlSequence html = server.getHTMLLandList();
145+ try{
146+ recList = parseVillageRecords(html);
147+ }catch(HtmlParseException e){
148+ LOGGER.log(Level.WARNING, "村一覧ページを認識できない", e);
149+ }
150+ totalList.addAll(recList);
151+ }
152+
153+ return totalList;
154+ }
155+
156+ /**
157+ * HTMLをパースし村一覧リストを返す。
158+ *
159+ * @param html HTML文書
160+ * @return 村一覧リスト
161+ * @throws HtmlParseException HTMLパースエラーによるパース停止
162+ */
163+ private static List<VillageRecord> parseVillageRecords(HtmlSequence html)
164+ throws HtmlParseException{
165+ HtmlParser parser = new HtmlParser();
166+ VillageListHandler handler = new VillageListHandler();
167+ parser.setBasicHandler(handler);
168+
169+ DecodedContent content = html.getContent();
170+ parser.parseAutomatic(content);
171+
172+ List<VillageRecord> result = handler.getVillageRecords();
173+
174+ return result;
175+ }
176+
177+}
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/data/html/VillageRecord.java
@@ -0,0 +1,67 @@
1+/*
2+ * village record
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2020 olyutorskii
6+ */
7+
8+package jp.sfjp.jindolf.data.html;
9+
10+import jp.sourceforge.jindolf.corelib.VillageState;
11+
12+/**
13+ * Village record on HTML.
14+ */
15+class VillageRecord {
16+
17+ private final String villageId;
18+ private final String fullVillageName;
19+ private final VillageState villageStatus;
20+
21+ /**
22+ * Constructor.
23+ *
24+ * @param villageId village id on CGI query
25+ * @param fullVillageName full village name
26+ * @param villageStatus village status
27+ */
28+ VillageRecord(String villageId,
29+ String fullVillageName,
30+ VillageState villageStatus ){
31+ super();
32+
33+ this.villageId = villageId;
34+ this.fullVillageName = fullVillageName;
35+ this.villageStatus = villageStatus;
36+
37+ return;
38+ }
39+
40+ /**
41+ * return village id on CGI query.
42+ *
43+ * @return village id
44+ */
45+ String getVillageId(){
46+ return this.villageId;
47+ }
48+
49+ /**
50+ * return long village name.
51+ *
52+ * @return long village name
53+ */
54+ String getFullVillageName(){
55+ return this.fullVillageName;
56+ }
57+
58+ /**
59+ * return village status.
60+ *
61+ * @return village status
62+ */
63+ VillageState getVillageStatus(){
64+ return this.villageStatus;
65+ }
66+
67+}
--- /dev/null
+++ b/src/main/java/jp/sfjp/jindolf/data/html/package-info.java
@@ -0,0 +1,15 @@
1+/*
2+ * package info
3+ *
4+ * License : The MIT License
5+ * Copyright(c) 2020 olyutorskii
6+ */
7+
8+/**
9+ * 人狼BBSサーバから受信したHTMLデータから、
10+ * JinParserなどを用いて各種データモデルを生成するクラス群。
11+ */
12+
13+package jp.sfjp.jindolf.data.html;
14+
15+/* EOF */
Show on old repository browser