-/*\r
- * daily period in village\r
- *\r
- * Copyright(c) 2008 olyutorskii\r
- * $Id: Period.java 1028 2010-05-13 10:15:11Z olyutorskii $\r
- */\r
-\r
-package jp.sourceforge.jindolf;\r
-\r
-import java.io.IOException;\r
-import java.util.Collections;\r
-import java.util.HashMap;\r
-import java.util.HashSet;\r
-import java.util.LinkedList;\r
-import java.util.List;\r
-import java.util.Map;\r
-import java.util.Set;\r
-import jp.sourceforge.jindolf.corelib.EventFamily;\r
-import jp.sourceforge.jindolf.corelib.GameRole;\r
-import jp.sourceforge.jindolf.corelib.LandDef;\r
-import jp.sourceforge.jindolf.corelib.PeriodType;\r
-import jp.sourceforge.jindolf.corelib.SysEventType;\r
-import jp.sourceforge.jindolf.corelib.TalkType;\r
-import jp.sourceforge.jindolf.corelib.Team;\r
-import jp.sourceforge.jindolf.corelib.VillageState;\r
-import jp.sourceforge.jindolf.parser.DecodedContent;\r
-import jp.sourceforge.jindolf.parser.EntityConverter;\r
-import jp.sourceforge.jindolf.parser.HtmlAdapter;\r
-import jp.sourceforge.jindolf.parser.HtmlParseException;\r
-import jp.sourceforge.jindolf.parser.HtmlParser;\r
-import jp.sourceforge.jindolf.parser.PageType;\r
-import jp.sourceforge.jindolf.parser.SeqRange;\r
-\r
-/**\r
- * いわゆる「日」。\r
- * 村の進行の一区切り。プロローグやエピローグも含まれる。\r
- *\r
- * 将来、24時間更新でなくなる可能性の考慮が必要。\r
- * 人気のないプロローグなどで、\r
- * 24時間以上の期間を持つPeriodが生成される可能性の考慮が必要。\r
- */\r
-public class Period{\r
- // TODO Comparable も implement する?\r
-\r
- private static final HtmlParser PARSER = new HtmlParser();\r
- private static final PeriodHandler HANDLER =\r
- new PeriodHandler();\r
-\r
- static{\r
- PARSER.setBasicHandler (HANDLER);\r
- PARSER.setSysEventHandler(HANDLER);\r
- PARSER.setTalkHandler (HANDLER);\r
- }\r
-\r
- /**\r
- * Periodを更新する。Topicのリストが更新される。\r
- * @param period 日\r
- * @param force trueなら強制再読み込み。\r
- * falseならまだ読み込んで無い時のみ読み込み。\r
- * @throws IOException ネットワーク入力エラー\r
- */\r
- public static void parsePeriod(Period period, boolean force)\r
- throws IOException{\r
- if( ! force && period.hasLoaded() ) return;\r
-\r
- Village village = period.getVillage();\r
- Land land = village.getParentLand();\r
- ServerAccess server = land.getServerAccess();\r
-\r
- if(village.getState() != VillageState.PROGRESS){\r
- period.isFullOpen = true;\r
- }else if(period.getType() != PeriodType.PROGRESS){\r
- period.isFullOpen = true;\r
- }else{\r
- period.isFullOpen = false;\r
- }\r
-\r
- HtmlSequence html = server.getHTMLPeriod(period);\r
-\r
- period.topicList.clear();\r
-\r
- boolean wasHot = period.isHot();\r
-\r
- HANDLER.setPeriod(period);\r
- DecodedContent content = html.getContent();\r
- try{\r
- PARSER.parseAutomatic(content);\r
- }catch(HtmlParseException e){\r
- Jindolf.logger().warn("発言抽出に失敗", e);\r
- }\r
-\r
- if(wasHot && ! period.isHot() ){\r
- parsePeriod(period, true);\r
- return;\r
- }\r
-\r
- return;\r
- }\r
-\r
- private final Village homeVillage;\r
- private final PeriodType periodType;\r
- private final int day;\r
- private int limitHour;\r
- private int limitMinute;\r
- // TODO 更新月日も入れるべきか。\r
- private String loginName;\r
- private boolean isFullOpen = false;\r
-\r
- private final List<Topic> topicList = new LinkedList<Topic>();\r
- private final List<Topic> unmodList =\r
- Collections.unmodifiableList(this.topicList);\r
-\r
- /**\r
- * この Period が進行中の村の最新日で、\r
- * 今まさに次々と発言が蓄積されているときは\r
- * true になる。\r
- * ※重要: Hot な Period は meslog クエリーを使ってダウンロードできない。\r
- */\r
- private boolean isHot;\r
-\r
- /**\r
- * Periodを生成する。\r
- * この段階では発言データのロードは行われない。\r
- * デフォルトで非Hot状態。\r
- * @param homeVillage 所属するVillage\r
- * @param periodType Period種別\r
- * @param day Period通番\r
- * @throws java.lang.NullPointerException 引数にnullが渡された場合。\r
- */\r
- public Period(Village homeVillage,\r
- PeriodType periodType,\r
- int day)\r
- throws NullPointerException{\r
- this(homeVillage, periodType, day, false);\r
- return;\r
- }\r
-\r
- /**\r
- * Periodを生成する。\r
- * この段階では発言データのロードは行われない。\r
- * @param homeVillage 所属するVillage\r
- * @param periodType Period種別\r
- * @param day Period通番\r
- * @param isHot Hotか否か\r
- * @throws java.lang.NullPointerException 引数にnullが渡された場合。\r
- */\r
- private Period(Village homeVillage,\r
- PeriodType periodType,\r
- int day,\r
- boolean isHot)\r
- throws NullPointerException{\r
- if( homeVillage == null\r
- || periodType == null ) throw new NullPointerException();\r
- if(day < 0){\r
- throw new IllegalArgumentException("Period day is too small !");\r
- }\r
- switch(periodType){\r
- case PROLOGUE:\r
- assert day == 0;\r
- break;\r
- case PROGRESS:\r
- case EPILOGUE:\r
- assert day > 0;\r
- break;\r
- default:\r
- assert false;\r
- break;\r
- }\r
-\r
- this.homeVillage = homeVillage;\r
- this.periodType = periodType;\r
- this.day = day;\r
-\r
- unload();\r
-\r
- this.isHot = isHot;\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 所属する村を返す。\r
- * @return 村\r
- */\r
- public Village getVillage(){\r
- return this.homeVillage;\r
- }\r
-\r
- /**\r
- * Period種別を返す。\r
- * @return 種別\r
- */\r
- public PeriodType getType(){\r
- return this.periodType;\r
- }\r
-\r
- /**\r
- * Period通番を返す。\r
- * プロローグは常に0番。\r
- * n日目のゲーム進行日はn番\r
- * エピローグは最後のゲーム進行日+1番\r
- * @return Period通番\r
- */\r
- public int getDay(){\r
- return this.day;\r
- }\r
-\r
- /**\r
- * 更新時刻の文字表記を返す。\r
- * @return 更新時刻の文字表記\r
- */\r
- public String getLimit(){\r
- StringBuilder result = new StringBuilder();\r
-\r
- if(this.limitHour < 10) result.append('0');\r
- result.append(this.limitHour).append(':');\r
-\r
- if(this.limitMinute < 10) result.append('0');\r
- result.append(this.limitMinute);\r
-\r
- return result.toString();\r
- }\r
-\r
- /**\r
- * Hotか否か返す。\r
- * @return Hotか否か\r
- */\r
- public boolean isHot(){\r
- return this.isHot;\r
- }\r
-\r
- /**\r
- * Hotか否か設定する。\r
- * @param isHotArg Hot指定\r
- */\r
- public void setHot(boolean isHotArg){\r
- this.isHot = isHotArg;\r
- }\r
-\r
- /**\r
- * プロローグか否か判定する。\r
- * @return プロローグならtrue\r
- */\r
- public boolean isPrologue(){\r
- if(getType() == PeriodType.PROLOGUE) return true;\r
- return false;\r
- }\r
-\r
- /**\r
- * エピローグか否か判定する。\r
- * @return エピローグならtrue\r
- */\r
- public boolean isEpilogue(){\r
- if(getType() == PeriodType.EPILOGUE) return true;\r
- return false;\r
- }\r
-\r
- /**\r
- * 進行日か否か判定する。\r
- * @return 進行日ならtrue\r
- */\r
- public boolean isProgress(){\r
- if(getType() == PeriodType.PROGRESS) return true;\r
- return false;\r
- }\r
-\r
- /**\r
- * このPeriodにアクセスするためのクエリーを生成する。\r
- * @return CGIに渡すクエリー\r
- */\r
- public String getCGIQuery(){\r
- StringBuilder result = new StringBuilder();\r
-\r
- Village village = getVillage();\r
- result.append(village.getCGIQuery());\r
-\r
- if(isHot()){\r
- result.append("&mes=all"); // 全表示指定\r
- return result.toString();\r
- }\r
-\r
- Land land = village.getParentLand();\r
- LandDef ldef = land.getLandDef();\r
-\r
- if(ldef.getLandId().equals("wolfg")){\r
- result.append("&meslog=");\r
- String dnum = "000" + (getDay() - 1);\r
- dnum = dnum.substring(dnum.length() - 3);\r
- switch(getType()){\r
- case PROLOGUE:\r
- result.append("000_ready");\r
- break;\r
- case PROGRESS:\r
- result.append(dnum).append("_progress");\r
- break;\r
- case EPILOGUE:\r
- result.append(dnum).append("_party");\r
- break;\r
- default:\r
- assert false;\r
- return null;\r
- }\r
- }else{\r
- result.append("&meslog=").append(village.getVillageID());\r
- switch(getType()){\r
- case PROLOGUE:\r
- result.append("_ready_0");\r
- break;\r
- case PROGRESS:\r
- result.append("_progress_").append(getDay() - 1);\r
- break;\r
- case EPILOGUE:\r
- result.append("_party_").append(getDay() - 1);\r
- break;\r
- default:\r
- assert false;\r
- return null;\r
- }\r
- }\r
-\r
-\r
- result.append("&mes=all");\r
-\r
- return result.toString();\r
- }\r
-\r
- /**\r
- * Periodに含まれるTopicのリストを返す。\r
- * このリストは上書き操作不能。\r
- * @return Topicのリスト\r
- */\r
- public List<Topic> getTopicList(){\r
- return this.unmodList;\r
- }\r
-\r
- /**\r
- * Periodに含まれるTopicの総数を返す。\r
- * @return Topic総数\r
- */\r
- public int getTopics(){\r
- return this.topicList.size();\r
- }\r
-\r
- /**\r
- * Topicを追加する。\r
- * @param topic Topic\r
- * @throws java.lang.NullPointerException nullが渡された場合。\r
- */\r
- protected void addTopic(Topic topic) throws NullPointerException{\r
- if(topic == null) throw new NullPointerException();\r
- this.topicList.add(topic);\r
- return;\r
- }\r
-\r
- /**\r
- * Periodのキャプション文字列を返す。\r
- * 主な用途はタブ画面の耳のラベルなど。\r
- * @return キャプション文字列\r
- */\r
- public String getCaption(){\r
- String result;\r
-\r
- switch(getType()){\r
- case PROLOGUE:\r
- result = "プロローグ";\r
- break;\r
- case PROGRESS:\r
- result = getDay() + "日目";\r
- break;\r
- case EPILOGUE:\r
- result = "エピローグ";\r
- break;\r
- default:\r
- assert false;\r
- result = null;\r
- break;\r
- }\r
-\r
- return result;\r
- }\r
-\r
- /**\r
- * このPeriodをダウンロードしたときのログイン名を返す。\r
- * @return ログイン名。ログアウト中はnull。\r
- */\r
- public String getLoginName(){\r
- return this.loginName;\r
- }\r
-\r
- /**\r
- * 公開発言番号にマッチする発言を返す。\r
- * @param talkNo 公開発言番号\r
- * @return 発言。見つからなければnull\r
- */\r
- public Talk getNumberedTalk(int talkNo){\r
- if(talkNo <= 0) throw new IllegalArgumentException();\r
-\r
- for(Topic topic : this.topicList){\r
- if( ! (topic instanceof Talk) ) continue;\r
- Talk talk = (Talk) topic;\r
- if(talkNo == talk.getTalkNo()) return talk;\r
- }\r
-\r
- return null;\r
- }\r
-\r
- /**\r
- * このPeriodの内容にゲーム進行上隠された部分がある可能性を判定する。\r
- * @return 隠れた要素がありうるならfalse\r
- */\r
- public boolean isFullOpen(){\r
- return this.isFullOpen;\r
- }\r
-\r
- /**\r
- * ロード済みか否かチェックする。\r
- * @return ロード済みならtrue\r
- */\r
- public boolean hasLoaded(){\r
- return getTopics() > 0;\r
- }\r
-\r
- /**\r
- * 発言データをアンロードする。\r
- */\r
- public void unload(){\r
- this.limitHour = 0;\r
- this.limitMinute = 0;\r
- this.loginName = null;\r
- this.isFullOpen = false;\r
-\r
- this.isHot = false;\r
-\r
- this.topicList.clear();\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * 襲撃メッセージの有無を判定する。\r
- * 決着が付くまで非狼陣営には見えない。\r
- * 偽装GJでは狼にも見えない。\r
- * @return 襲撃メッセージがあればtrue\r
- */\r
- public boolean hasAssaultTried(){\r
- for(Topic topic : this.topicList){\r
- if(topic instanceof Talk){\r
- Talk talk = (Talk) topic;\r
- if(talk.getTalkCount() <= 0) return true;\r
- }else if(topic instanceof SysEvent){\r
- SysEvent sysEvent = (SysEvent) topic;\r
- SysEventType type = sysEvent.getSysEventType();\r
- if(type == SysEventType.ASSAULT) return true;\r
- }\r
- }\r
-\r
- return false;\r
- }\r
-\r
- /**\r
- * 処刑されたAvatarを返す。\r
- * @return 処刑されたAvatar。突然死などなんらかの理由でいない場合はnull\r
- */\r
- public Avatar getExecutedAvatar(){\r
- Avatar result = null;\r
-\r
- for(Topic topic : getTopicList()){\r
- if( ! (topic instanceof SysEvent) ) continue;\r
- SysEvent event = (SysEvent) topic;\r
- result = event.getExecutedAvatar();\r
- if(result != null) break;\r
- }\r
-\r
- return result;\r
- }\r
-\r
- /**\r
- * 投票に参加したAvatarの集合を返す。\r
- * @return 投票に参加したAvatarのSet\r
- */\r
- public Set<Avatar> getVoterSet(){\r
- Set<Avatar> result = new HashSet<Avatar>();\r
-\r
- for(Topic topic : getTopicList()){\r
- if( ! (topic instanceof SysEvent) ) continue;\r
- SysEvent event = (SysEvent) topic;\r
- result = event.getVoterSet(result);\r
- }\r
-\r
- return result;\r
- }\r
-\r
- /**\r
- * 任意のタイプのシステムイベントを返す。\r
- * 複数存在する場合、返すのは最初の一つだけ。\r
- * @param type イベントタイプ\r
- * @return システムイベント\r
- */\r
- public SysEvent getTypedSysEvent(SysEventType type){\r
- for(Topic topic : getTopicList()){\r
- if( ! (topic instanceof SysEvent) ) continue;\r
- SysEvent event = (SysEvent) topic;\r
- if(event.getSysEventType() == type) return event;\r
- }\r
-\r
- return null;\r
- }\r
-\r
- /**\r
- * Periodパース用ハンドラ。\r
- */\r
- private static class PeriodHandler extends HtmlAdapter{\r
-\r
- private static final int TALKTYPE_NUM = TalkType.values().length;\r
-\r
- private final EntityConverter converter =\r
- new EntityConverter();\r
-\r
- private final Map<Avatar, int[]> countMap =\r
- new HashMap<Avatar, int[]>();\r
-\r
- private Period period = null;\r
-\r
- private TalkType talkType;\r
- private Avatar avatar;\r
- private int talkNo;\r
- private String anchorId;\r
- private int talkHour;\r
- private int talkMinute;\r
- private DecodedContent talkContent = null;\r
-\r
- private EventFamily eventFamily;\r
- private SysEventType sysEventType;\r
- private DecodedContent eventContent = null;\r
- private final List<Avatar> avatarList = new LinkedList<Avatar>();\r
- private final List<GameRole> roleList = new LinkedList<GameRole>();\r
- private final List<Integer> integerList = new LinkedList<Integer>();\r
- private final List<CharSequence> charseqList =\r
- new LinkedList<CharSequence>();\r
-\r
- /**\r
- * コンストラクタ。\r
- */\r
- public PeriodHandler(){\r
- super();\r
- return;\r
- }\r
-\r
- /**\r
- * パース結果を格納するPeriodを設定する。\r
- * @param period Period\r
- */\r
- public void setPeriod(Period period){\r
- this.period = period;\r
- return;\r
- }\r
-\r
- /**\r
- * 文字列断片からAvatarを得る。\r
- * 村に未登録のAvatarであればついでに登録される。\r
- * @param content 文字列\r
- * @param range 文字列内のAvatarフルネームを示す領域\r
- * @return Avatar\r
- */\r
- private Avatar toAvatar(DecodedContent content, SeqRange range){\r
- Village village = this.period.getVillage();\r
- String fullName = this.converter\r
- .convert(content, range)\r
- .toString();\r
- Avatar result = village.getAvatar(fullName);\r
- if(result == null){\r
- result = new Avatar(fullName);\r
- village.addAvatar(result);\r
- }\r
-\r
- return result;\r
- }\r
-\r
- /**\r
- * Avatar別、会話種ごとに発言回数をカウントする。\r
- * 1から始まる。\r
- * @param targetAvatar 対象Avatar\r
- * @param targetType 対象会話種\r
- * @return カウント数\r
- */\r
- private int countUp(Avatar targetAvatar, TalkType targetType){\r
- int[] countArray = this.countMap.get(targetAvatar);\r
- if(countArray == null){\r
- countArray = new int[TALKTYPE_NUM];\r
- this.countMap.put(targetAvatar, countArray);\r
- }\r
- int count = ++countArray[targetType.ordinal()];\r
- return count;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void startParse(DecodedContent content)\r
- throws HtmlParseException{\r
- this.period.loginName = null;\r
- this.period.topicList.clear();\r
- this.countMap.clear();\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param loginRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void loginName(DecodedContent content, SeqRange loginRange)\r
- throws HtmlParseException{\r
- DecodedContent loginName =\r
- this.converter.convert(content, loginRange);\r
-\r
- this.period.loginName = loginName.toString();\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param type {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void pageType(PageType type) throws HtmlParseException{\r
- if(type != PageType.PERIOD_PAGE){\r
- throw new HtmlParseException(\r
- "意図しないページを読み込もうとしました。");\r
- }\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param month {@inheritDoc}\r
- * @param day {@inheritDoc}\r
- * @param hour {@inheritDoc}\r
- * @param minute {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void commitTime(int month, int day, int hour, int minute)\r
- throws HtmlParseException{\r
- this.period.limitHour = hour;\r
- this.period.limitMinute = minute;\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * 自分へのリンクが無いかチェックする。\r
- * 自分へのリンクが見つかればこのPeriodを非Hotにする。\r
- * 自分へのリンクがあるということは、\r
- * 今読んでるHTMLは別のPeriodのために書かれたものということ。\r
- * 考えられる原因は、HotだったPeriodがゲーム進行に従い\r
- * Hotでなくなったこと。\r
- * @param content {@inheritDoc}\r
- * @param anchorRange {@inheritDoc}\r
- * @param periodType {@inheritDoc}\r
- * @param day {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void periodLink(DecodedContent content,\r
- SeqRange anchorRange,\r
- PeriodType periodType,\r
- int day)\r
- throws HtmlParseException{\r
-\r
- if(this.period.getType() != periodType) return;\r
-\r
- if( periodType == PeriodType.PROGRESS\r
- && this.period.getDay() != day ){\r
- return;\r
- }\r
-\r
- if( ! anchorRange.isValid() ) return;\r
-\r
- this.period.setHot(false);\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void startTalk() throws HtmlParseException{\r
- this.talkType = null;\r
- this.avatar = null;\r
- this.talkNo = -1;\r
- this.anchorId = null;\r
- this.talkHour = -1;\r
- this.talkMinute = -1;\r
- this.talkContent = new DecodedContent(100 + 1);\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param type {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void talkType(TalkType type)\r
- throws HtmlParseException{\r
- this.talkType = type;\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param avatarRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void talkAvatar(DecodedContent content, SeqRange avatarRange)\r
- throws HtmlParseException{\r
- this.avatar = toAvatar(content, avatarRange);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param hour {@inheritDoc}\r
- * @param minute {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void talkTime(int hour, int minute)\r
- throws HtmlParseException{\r
- this.talkHour = hour;\r
- this.talkMinute = minute;\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param tno {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void talkNo(int tno) throws HtmlParseException{\r
- this.talkNo = tno;\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param idRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void talkId(DecodedContent content, SeqRange idRange)\r
- throws HtmlParseException{\r
- this.anchorId = content.subSequence(idRange.getStartPos(),\r
- idRange.getEndPos() )\r
- .toString();\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param textRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void talkText(DecodedContent content, SeqRange textRange)\r
- throws HtmlParseException{\r
- this.converter.append(this.talkContent, content, textRange);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void talkBreak()\r
- throws HtmlParseException{\r
- this.talkContent.append('\n');\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void endTalk() throws HtmlParseException{\r
- Talk talk = new Talk(this.period,\r
- this.talkType,\r
- this.avatar,\r
- this.talkNo,\r
- this.anchorId,\r
- this.talkHour, this.talkMinute,\r
- this.talkContent );\r
-\r
- int count = countUp(this.avatar, this.talkType);\r
- talk.setCount(count);\r
-\r
- this.period.addTopic(talk);\r
-\r
- this.talkType = null;\r
- this.avatar = null;\r
- this.talkNo = -1;\r
- this.anchorId = null;\r
- this.talkHour = -1;\r
- this.talkMinute = -1;\r
- this.talkContent = null;\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param family {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void startSysEvent(EventFamily family)\r
- throws HtmlParseException{\r
- this.eventFamily = family;\r
- this.sysEventType = null;\r
- this.eventContent = new DecodedContent();\r
- this.avatarList.clear();\r
- this.roleList.clear();\r
- this.integerList.clear();\r
- this.charseqList.clear();\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param type {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventType(SysEventType type)\r
- throws HtmlParseException{\r
- this.sysEventType = type;\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param contentRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventContent(DecodedContent content,\r
- SeqRange contentRange)\r
- throws HtmlParseException{\r
- this.converter.append(this.eventContent, content, contentRange);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param anchorRange {@inheritDoc}\r
- * @param contentRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventContentAnchor(DecodedContent content,\r
- SeqRange anchorRange,\r
- SeqRange contentRange)\r
- throws HtmlParseException{\r
- this.converter.append(this.eventContent, content, contentRange);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventContentBreak() throws HtmlParseException{\r
- this.eventContent.append('\n');\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param entryNo {@inheritDoc}\r
- * @param avatarRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventOnStage(DecodedContent content,\r
- int entryNo,\r
- SeqRange avatarRange)\r
- throws HtmlParseException{\r
- Avatar newAvatar = toAvatar(content, avatarRange);\r
- this.integerList.add(entryNo);\r
- this.avatarList.add(newAvatar);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param role {@inheritDoc}\r
- * @param num {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventOpenRole(GameRole role, int num)\r
- throws HtmlParseException{\r
- this.roleList.add(role);\r
- this.integerList.add(num);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param avatarRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventMurdered(DecodedContent content,\r
- SeqRange avatarRange)\r
- throws HtmlParseException{\r
- Avatar murdered = toAvatar(content, avatarRange);\r
- this.avatarList.add(murdered);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param avatarRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventSurvivor(DecodedContent content,\r
- SeqRange avatarRange)\r
- throws HtmlParseException{\r
- Avatar survivor = toAvatar(content, avatarRange);\r
- this.avatarList.add(survivor);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param voteByRange {@inheritDoc}\r
- * @param voteToRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventCounting(DecodedContent content,\r
- SeqRange voteByRange,\r
- SeqRange voteToRange)\r
- throws HtmlParseException{\r
- if(voteByRange.isValid()){\r
- Avatar voteBy = toAvatar(content, voteByRange);\r
- this.avatarList.add(voteBy);\r
- }\r
- Avatar voteTo = toAvatar(content, voteToRange);\r
- this.avatarList.add(voteTo);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param voteByRange {@inheritDoc}\r
- * @param voteToRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventCounting2(DecodedContent content,\r
- SeqRange voteByRange,\r
- SeqRange voteToRange)\r
- throws HtmlParseException{\r
- sysEventCounting(content, voteByRange, voteToRange);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param avatarRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventSuddenDeath(DecodedContent content,\r
- SeqRange avatarRange)\r
- throws HtmlParseException{\r
- Avatar suddenDeath = toAvatar(content, avatarRange);\r
- this.avatarList.add(suddenDeath);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param avatarRange {@inheritDoc}\r
- * @param anchorRange {@inheritDoc}\r
- * @param loginRange {@inheritDoc}\r
- * @param isLiving {@inheritDoc}\r
- * @param role {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventPlayerList(DecodedContent content,\r
- SeqRange avatarRange,\r
- SeqRange anchorRange,\r
- SeqRange loginRange,\r
- boolean isLiving,\r
- GameRole role )\r
- throws HtmlParseException{\r
- Avatar who = toAvatar(content, avatarRange);\r
-\r
- CharSequence anchor;\r
- if(anchorRange.isValid()){\r
- anchor = this.converter.convert(content, anchorRange);\r
- }else{\r
- anchor = "";\r
- }\r
- CharSequence account = this.converter\r
- .convert(content, loginRange);\r
-\r
- Integer liveOrDead;\r
- if(isLiving) liveOrDead = Integer.valueOf(1);\r
- else liveOrDead = Integer.valueOf(0);\r
-\r
- this.avatarList.add(who);\r
- this.charseqList.add(anchor);\r
- this.charseqList.add(account);\r
- this.integerList.add(liveOrDead);\r
- this.roleList.add(role);\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param avatarRange {@inheritDoc}\r
- * @param votes {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventExecution(DecodedContent content,\r
- SeqRange avatarRange,\r
- int votes )\r
- throws HtmlParseException{\r
- Avatar who = toAvatar(content, avatarRange);\r
-\r
- this.avatarList.add(who);\r
- this.integerList.add(votes);\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param hour {@inheritDoc}\r
- * @param minute {@inheritDoc}\r
- * @param minLimit {@inheritDoc}\r
- * @param maxLimit {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventAskEntry(int hour, int minute,\r
- int minLimit, int maxLimit)\r
- throws HtmlParseException{\r
- this.integerList.add(hour * 60 + minute);\r
- this.integerList.add(minLimit);\r
- this.integerList.add(maxLimit);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param hour {@inheritDoc}\r
- * @param minute {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventAskCommit(int hour, int minute)\r
- throws HtmlParseException{\r
- this.integerList.add(hour * 60 + minute);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param avatarRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventNoComment(DecodedContent content,\r
- SeqRange avatarRange)\r
- throws HtmlParseException{\r
- Avatar noComAvatar = toAvatar(content, avatarRange);\r
- this.avatarList.add(noComAvatar);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param winner {@inheritDoc}\r
- * @param hour {@inheritDoc}\r
- * @param minute {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventStayEpilogue(Team winner, int hour, int minute)\r
- throws HtmlParseException{\r
- GameRole role = null;\r
-\r
- switch(winner){\r
- case VILLAGE: role = GameRole.INNOCENT; break;\r
- case WOLF: role = GameRole.WOLF; break;\r
- case HAMSTER: role = GameRole.HAMSTER; break;\r
- default: assert false; break;\r
- }\r
-\r
- this.roleList.add(role);\r
- this.integerList.add(hour * 60 + minute);\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param guardByRange {@inheritDoc}\r
- * @param guardToRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventGuard(DecodedContent content,\r
- SeqRange guardByRange,\r
- SeqRange guardToRange)\r
- throws HtmlParseException{\r
- Avatar guardBy = toAvatar(content, guardByRange);\r
- Avatar guardTo = toAvatar(content, guardToRange);\r
- this.avatarList.add(guardBy);\r
- this.avatarList.add(guardTo);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @param content {@inheritDoc}\r
- * @param judgeByRange {@inheritDoc}\r
- * @param judgeToRange {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void sysEventJudge(DecodedContent content,\r
- SeqRange judgeByRange,\r
- SeqRange judgeToRange)\r
- throws HtmlParseException{\r
- Avatar judgeBy = toAvatar(content, judgeByRange);\r
- Avatar judgeTo = toAvatar(content, judgeToRange);\r
- this.avatarList.add(judgeBy);\r
- this.avatarList.add(judgeTo);\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void endSysEvent() throws HtmlParseException{\r
- SysEvent event = new SysEvent();\r
- event.setEventFamily(this.eventFamily);\r
- event.setSysEventType(this.sysEventType);\r
- event.setContent(this.eventContent);\r
- event.addAvatarList(this.avatarList);\r
- event.addRoleList(this.roleList);\r
- event.addIntegerList(this.integerList);\r
- event.addCharSequenceList(this.charseqList);\r
-\r
- this.period.addTopic(event);\r
-\r
- if( this.sysEventType == SysEventType.MURDERED\r
- || this.sysEventType == SysEventType.NOMURDER ){\r
- for(Topic topic : this.period.topicList){\r
- if( ! (topic instanceof Talk) ) continue;\r
- Talk talk = (Talk) topic;\r
- if(talk.getTalkType() != TalkType.WOLFONLY) continue;\r
- if( ! StringUtils\r
- .isTerminated(talk.getDialog(),\r
- "!\u0020今日がお前の命日だ!") ){\r
- continue;\r
- }\r
- talk.setCount(-1);\r
- this.countMap.clear();\r
- }\r
- }\r
-\r
- this.eventFamily = null;\r
- this.sysEventType = null;\r
- this.eventContent = null;\r
- this.avatarList.clear();\r
- this.roleList.clear();\r
- this.integerList.clear();\r
- this.charseqList.clear();\r
-\r
- return;\r
- }\r
-\r
- /**\r
- * {@inheritDoc}\r
- * @throws HtmlParseException {@inheritDoc}\r
- */\r
- @Override\r
- public void endParse() throws HtmlParseException{\r
- return;\r
- }\r
-\r
- // TODO 村名のチェックは不要か?\r
- }\r
-\r
-}\r
+/*
+ * daily period in village
+ *
+ * License : The MIT License
+ * Copyright(c) 2008 olyutorskii
+ */
+
+package jp.sfjp.jindolf.data;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jp.osdn.jindolf.parser.EntityConverter;
+import jp.osdn.jindolf.parser.HtmlAdapter;
+import jp.osdn.jindolf.parser.HtmlParseException;
+import jp.osdn.jindolf.parser.HtmlParser;
+import jp.osdn.jindolf.parser.PageType;
+import jp.osdn.jindolf.parser.SeqRange;
+import jp.osdn.jindolf.parser.content.DecodedContent;
+import jp.sfjp.jindolf.net.HtmlSequence;
+import jp.sfjp.jindolf.net.ServerAccess;
+import jp.sfjp.jindolf.util.StringUtils;
+import jp.sourceforge.jindolf.corelib.EventFamily;
+import jp.sourceforge.jindolf.corelib.GameRole;
+import jp.sourceforge.jindolf.corelib.LandDef;
+import jp.sourceforge.jindolf.corelib.PeriodType;
+import jp.sourceforge.jindolf.corelib.SysEventType;
+import jp.sourceforge.jindolf.corelib.TalkType;
+import jp.sourceforge.jindolf.corelib.Team;
+import jp.sourceforge.jindolf.corelib.VillageState;
+
+/**
+ * いわゆる「日」。
+ * 村の進行の一区切り。プロローグやエピローグも含まれる。
+ *
+ * <p>将来、24時間更新でなくなる可能性の考慮が必要。
+ * 人気のないプロローグなどで、
+ * 24時間以上の期間を持つPeriodが生成される可能性の考慮が必要。
+ */
+public class Period{
+ // TODO Comparable も implement する?
+
+ private static final HtmlParser PARSER = new HtmlParser();
+ private static final PeriodHandler HANDLER =
+ new PeriodHandler();
+
+ private static final Logger LOGGER = Logger.getAnonymousLogger();
+
+ static{
+ PARSER.setBasicHandler (HANDLER);
+ PARSER.setSysEventHandler(HANDLER);
+ PARSER.setTalkHandler (HANDLER);
+ }
+
+ private final Village homeVillage;
+ private final PeriodType periodType;
+ private final int day;
+ private int limitHour;
+ private int limitMinute;
+ // TODO 更新月日も入れるべきか。
+ private String loginName;
+ private boolean isFullOpen = false;
+
+ private final List<Topic> topicList = new LinkedList<>();
+ private final List<Topic> unmodList =
+ Collections.unmodifiableList(this.topicList);
+
+
+ /**
+ * この Period が進行中の村の最新日で、
+ * 今まさに次々と発言が蓄積されているときは
+ * true になる。
+ * ※重要: Hot な Period は meslog クエリーを使ってダウンロードできない。
+ */
+ private boolean isHot;
+
+
+ /**
+ * Periodを生成する。
+ * この段階では発言データのロードは行われない。
+ * デフォルトで非Hot状態。
+ * @param homeVillage 所属するVillage
+ * @param periodType Period種別
+ * @param day Period通番
+ * @throws java.lang.NullPointerException 引数にnullが渡された場合。
+ */
+ public Period(Village homeVillage,
+ PeriodType periodType,
+ int day)
+ throws NullPointerException{
+ this(homeVillage, periodType, day, false);
+ return;
+ }
+
+ /**
+ * Periodを生成する。
+ * この段階では発言データのロードは行われない。
+ * @param homeVillage 所属するVillage
+ * @param periodType Period種別
+ * @param day Period通番
+ * @param isHot Hotか否か
+ * @throws java.lang.NullPointerException 引数にnullが渡された場合。
+ */
+ private Period(Village homeVillage,
+ PeriodType periodType,
+ int day,
+ boolean isHot)
+ throws NullPointerException{
+ if( homeVillage == null
+ || periodType == null ) throw new NullPointerException();
+ if(day < 0){
+ throw new IllegalArgumentException("Period day is too small !");
+ }
+ switch(periodType){
+ case PROLOGUE:
+ assert day == 0;
+ break;
+ case PROGRESS:
+ case EPILOGUE:
+ assert day > 0;
+ break;
+ default:
+ assert false;
+ break;
+ }
+
+ this.homeVillage = homeVillage;
+ this.periodType = periodType;
+ this.day = day;
+
+ unload();
+
+ this.isHot = isHot;
+
+ return;
+ }
+
+
+ /**
+ * Periodを更新する。Topicのリストが更新される。
+ * @param period 日
+ * @param force trueなら強制再読み込み。
+ * falseならまだ読み込んで無い時のみ読み込み。
+ * @throws IOException ネットワーク入力エラー
+ */
+ public static void parsePeriod(Period period, boolean force)
+ throws IOException{
+ if( ! force && period.hasLoaded() ) return;
+
+ Village village = period.getVillage();
+ Land land = village.getParentLand();
+ ServerAccess server = land.getServerAccess();
+
+ if(village.getState() != VillageState.PROGRESS){
+ period.isFullOpen = true;
+ }else if(period.getType() != PeriodType.PROGRESS){
+ period.isFullOpen = true;
+ }else{
+ period.isFullOpen = false;
+ }
+
+ HtmlSequence html = server.getHTMLPeriod(period);
+
+ period.topicList.clear();
+
+ boolean wasHot = period.isHot();
+
+ HANDLER.setPeriod(period);
+ DecodedContent content = html.getContent();
+ try{
+ PARSER.parseAutomatic(content);
+ }catch(HtmlParseException e){
+ LOGGER.log(Level.WARNING, "発言抽出に失敗", e);
+ }
+
+ if(wasHot && ! period.isHot() ){
+ parsePeriod(period, true);
+ return;
+ }
+
+ return;
+ }
+
+ /**
+ * 所属する村を返す。
+ * @return 村
+ */
+ public Village getVillage(){
+ return this.homeVillage;
+ }
+
+ /**
+ * Period種別を返す。
+ * @return 種別
+ */
+ public PeriodType getType(){
+ return this.periodType;
+ }
+
+ /**
+ * Period通番を返す。
+ * プロローグは常に0番。
+ * n日目のゲーム進行日はn番
+ * エピローグは最後のゲーム進行日+1番
+ * @return Period通番
+ */
+ public int getDay(){
+ return this.day;
+ }
+
+ /**
+ * 更新時刻の文字表記を返す。
+ * @return 更新時刻の文字表記
+ */
+ public String getLimit(){
+ StringBuilder result = new StringBuilder();
+
+ if(this.limitHour < 10) result.append('0');
+ result.append(this.limitHour).append(':');
+
+ if(this.limitMinute < 10) result.append('0');
+ result.append(this.limitMinute);
+
+ return result.toString();
+ }
+
+ /**
+ * Hotか否か返す。
+ * @return Hotか否か
+ */
+ public boolean isHot(){
+ return this.isHot;
+ }
+
+ /**
+ * Hotか否か設定する。
+ * @param isHotArg Hot指定
+ */
+ public void setHot(boolean isHotArg){
+ this.isHot = isHotArg;
+ }
+
+ /**
+ * プロローグか否か判定する。
+ * @return プロローグならtrue
+ */
+ public boolean isPrologue(){
+ if(getType() == PeriodType.PROLOGUE) return true;
+ return false;
+ }
+
+ /**
+ * エピローグか否か判定する。
+ * @return エピローグならtrue
+ */
+ public boolean isEpilogue(){
+ if(getType() == PeriodType.EPILOGUE) return true;
+ return false;
+ }
+
+ /**
+ * 進行日か否か判定する。
+ * @return 進行日ならtrue
+ */
+ public boolean isProgress(){
+ if(getType() == PeriodType.PROGRESS) return true;
+ return false;
+ }
+
+ /**
+ * このPeriodにアクセスするためのクエリーを生成する。
+ * @return CGIに渡すクエリー
+ */
+ public String getCGIQuery(){
+ StringBuilder result = new StringBuilder();
+
+ Village village = getVillage();
+ result.append(village.getCGIQuery());
+
+ if(isHot()){
+ result.append("&mes=all"); // 全表示指定
+ return result.toString();
+ }
+
+ Land land = village.getParentLand();
+ LandDef ldef = land.getLandDef();
+
+ if(ldef.getLandId().equals("wolfg")){
+ result.append("&meslog=");
+ String dnum = "000" + (getDay() - 1);
+ dnum = dnum.substring(dnum.length() - 3);
+ switch(getType()){
+ case PROLOGUE:
+ result.append("000_ready");
+ break;
+ case PROGRESS:
+ result.append(dnum).append("_progress");
+ break;
+ case EPILOGUE:
+ result.append(dnum).append("_party");
+ break;
+ default:
+ assert false;
+ return null;
+ }
+ }else{
+ result.append("&meslog=").append(village.getVillageID());
+ switch(getType()){
+ case PROLOGUE:
+ result.append("_ready_0");
+ break;
+ case PROGRESS:
+ result.append("_progress_").append(getDay() - 1);
+ break;
+ case EPILOGUE:
+ result.append("_party_").append(getDay() - 1);
+ break;
+ default:
+ assert false;
+ return null;
+ }
+ }
+
+
+ result.append("&mes=all");
+
+ return result.toString();
+ }
+
+ /**
+ * Periodに含まれるTopicのリストを返す。
+ * このリストは上書き操作不能。
+ * @return Topicのリスト
+ */
+ public List<Topic> getTopicList(){
+ return this.unmodList;
+ }
+
+ /**
+ * Periodに含まれるTopicの総数を返す。
+ * @return Topic総数
+ */
+ public int getTopics(){
+ return this.topicList.size();
+ }
+
+ /**
+ * Topicを追加する。
+ * @param topic Topic
+ * @throws java.lang.NullPointerException nullが渡された場合。
+ */
+ protected void addTopic(Topic topic) throws NullPointerException{
+ if(topic == null) throw new NullPointerException();
+ this.topicList.add(topic);
+ return;
+ }
+
+ /**
+ * Periodのキャプション文字列を返す。
+ * 主な用途はタブ画面の耳のラベルなど。
+ * @return キャプション文字列
+ */
+ public String getCaption(){
+ String result;
+
+ switch(getType()){
+ case PROLOGUE:
+ result = "プロローグ";
+ break;
+ case PROGRESS:
+ result = getDay() + "日目";
+ break;
+ case EPILOGUE:
+ result = "エピローグ";
+ break;
+ default:
+ assert false;
+ result = null;
+ break;
+ }
+
+ return result;
+ }
+
+ /**
+ * このPeriodをダウンロードしたときのログイン名を返す。
+ * @return ログイン名。ログアウト中はnull。
+ */
+ public String getLoginName(){
+ return this.loginName;
+ }
+
+ /**
+ * 公開発言番号にマッチする発言を返す。
+ * @param talkNo 公開発言番号
+ * @return 発言。見つからなければnull
+ */
+ public Talk getNumberedTalk(int talkNo){
+ if(talkNo <= 0) throw new IllegalArgumentException();
+
+ for(Topic topic : this.topicList){
+ if( ! (topic instanceof Talk) ) continue;
+ Talk talk = (Talk) topic;
+ if(talkNo == talk.getTalkNo()) return talk;
+ }
+
+ return null;
+ }
+
+ /**
+ * このPeriodの内容にゲーム進行上隠された部分がある可能性を判定する。
+ * @return 隠れた要素がありうるならfalse
+ */
+ public boolean isFullOpen(){
+ return this.isFullOpen;
+ }
+
+ /**
+ * ロード済みか否かチェックする。
+ * @return ロード済みならtrue
+ */
+ public boolean hasLoaded(){
+ return getTopics() > 0;
+ }
+
+ /**
+ * 発言データをアンロードする。
+ */
+ public void unload(){
+ this.limitHour = 0;
+ this.limitMinute = 0;
+ this.loginName = null;
+ this.isFullOpen = false;
+
+ this.isHot = false;
+
+ this.topicList.clear();
+
+ return;
+ }
+
+ /**
+ * 襲撃メッセージの有無を判定する。
+ * 決着が付くまで非狼陣営には見えない。
+ * 偽装GJでは狼にも見えない。
+ * @return 襲撃メッセージがあればtrue
+ */
+ public boolean hasAssaultTried(){
+ for(Topic topic : this.topicList){
+ if(topic instanceof Talk){
+ Talk talk = (Talk) topic;
+ if(talk.getTalkCount() <= 0) return true;
+ }else if(topic instanceof SysEvent){
+ SysEvent sysEvent = (SysEvent) topic;
+ SysEventType type = sysEvent.getSysEventType();
+ if(type == SysEventType.ASSAULT) return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 処刑されたAvatarを返す。
+ * @return 処刑されたAvatar。突然死などなんらかの理由でいない場合はnull
+ */
+ public Avatar getExecutedAvatar(){
+ Avatar result = null;
+
+ for(Topic topic : getTopicList()){
+ if( ! (topic instanceof SysEvent) ) continue;
+ SysEvent event = (SysEvent) topic;
+ result = event.getExecutedAvatar();
+ if(result != null) break;
+ }
+
+ return result;
+ }
+
+ /**
+ * 投票に参加したAvatarの集合を返す。
+ * @return 投票に参加したAvatarのSet
+ */
+ public Set<Avatar> getVoterSet(){
+ Set<Avatar> result = new HashSet<>();
+
+ for(Topic topic : getTopicList()){
+ if( ! (topic instanceof SysEvent) ) continue;
+ SysEvent event = (SysEvent) topic;
+ result = event.getVoterSet(result);
+ }
+
+ return result;
+ }
+
+ /**
+ * 任意のタイプのシステムイベントを返す。
+ * 複数存在する場合、返すのは最初の一つだけ。
+ * @param type イベントタイプ
+ * @return システムイベント
+ */
+ public SysEvent getTypedSysEvent(SysEventType type){
+ for(Topic topic : getTopicList()){
+ if( ! (topic instanceof SysEvent) ) continue;
+ SysEvent event = (SysEvent) topic;
+ if(event.getSysEventType() == type) return event;
+ }
+
+ return null;
+ }
+
+ /**
+ * Periodパース用ハンドラ。
+ */
+ private static class PeriodHandler extends HtmlAdapter{
+
+ private static final int TALKTYPE_NUM = TalkType.values().length;
+
+ private final EntityConverter converter =
+ new EntityConverter(true);
+ // TODO: SMP面文字に彩色対応するまでの暫定措置
+
+ private final Map<Avatar, int[]> countMap =
+ new HashMap<>();
+
+ private Period period = null;
+
+ private TalkType talkType;
+ private Avatar avatar;
+ private int talkNo;
+ private String anchorId;
+ private int talkHour;
+ private int talkMinute;
+ private DecodedContent talkContent = null;
+
+ private EventFamily eventFamily;
+ private SysEventType sysEventType;
+ private DecodedContent eventContent = null;
+ private final List<Avatar> avatarList = new LinkedList<>();
+ private final List<GameRole> roleList = new LinkedList<>();
+ private final List<Integer> integerList = new LinkedList<>();
+ private final List<CharSequence> charseqList =
+ new LinkedList<>();
+
+ /**
+ * コンストラクタ。
+ */
+ public PeriodHandler(){
+ super();
+ return;
+ }
+
+ /**
+ * パース結果を格納するPeriodを設定する。
+ * @param period Period
+ */
+ public void setPeriod(Period period){
+ this.period = period;
+ return;
+ }
+
+ /**
+ * 文字列断片からAvatarを得る。
+ * 村に未登録のAvatarであればついでに登録される。
+ * @param content 文字列
+ * @param range 文字列内のAvatarフルネームを示す領域
+ * @return Avatar
+ */
+ private Avatar toAvatar(DecodedContent content, SeqRange range){
+ Village village = this.period.getVillage();
+ String fullName = this.converter
+ .convert(content, range)
+ .toString();
+ Avatar result = village.getAvatar(fullName);
+ if(result == null){
+ result = new Avatar(fullName);
+ village.addAvatar(result);
+ }
+
+ return result;
+ }
+
+ /**
+ * Avatar別、会話種ごとに発言回数をカウントする。
+ * 1から始まる。
+ * @param targetAvatar 対象Avatar
+ * @param targetType 対象会話種
+ * @return カウント数
+ */
+ private int countUp(Avatar targetAvatar, TalkType targetType){
+ int[] countArray = this.countMap.get(targetAvatar);
+ if(countArray == null){
+ countArray = new int[TALKTYPE_NUM];
+ this.countMap.put(targetAvatar, countArray);
+ }
+ int count = ++countArray[targetType.ordinal()];
+ return count;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void startParse(DecodedContent content)
+ throws HtmlParseException{
+ this.period.loginName = null;
+ this.period.topicList.clear();
+ this.countMap.clear();
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param loginRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void loginName(DecodedContent content, SeqRange loginRange)
+ throws HtmlParseException{
+ DecodedContent loginName =
+ this.converter.convert(content, loginRange);
+
+ this.period.loginName = loginName.toString();
+
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param type {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void pageType(PageType type) throws HtmlParseException{
+ if(type != PageType.PERIOD_PAGE){
+ throw new HtmlParseException(
+ "意図しないページを読み込もうとしました。");
+ }
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param month {@inheritDoc}
+ * @param day {@inheritDoc}
+ * @param hour {@inheritDoc}
+ * @param minute {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void commitTime(int month, int day, int hour, int minute)
+ throws HtmlParseException{
+ this.period.limitHour = hour;
+ this.period.limitMinute = minute;
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * 自分へのリンクが無いかチェックする。
+ * 自分へのリンクが見つかればこのPeriodを非Hotにする。
+ * 自分へのリンクがあるということは、
+ * 今読んでるHTMLは別のPeriodのために書かれたものということ。
+ * 考えられる原因は、HotだったPeriodがゲーム進行に従い
+ * Hotでなくなったこと。
+ * @param content {@inheritDoc}
+ * @param anchorRange {@inheritDoc}
+ * @param periodType {@inheritDoc}
+ * @param day {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void periodLink(DecodedContent content,
+ SeqRange anchorRange,
+ PeriodType periodType,
+ int day)
+ throws HtmlParseException{
+
+ if(this.period.getType() != periodType) return;
+
+ if( periodType == PeriodType.PROGRESS
+ && this.period.getDay() != day ){
+ return;
+ }
+
+ if( ! anchorRange.isValid() ) return;
+
+ this.period.setHot(false);
+
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void startTalk() throws HtmlParseException{
+ this.talkType = null;
+ this.avatar = null;
+ this.talkNo = -1;
+ this.anchorId = null;
+ this.talkHour = -1;
+ this.talkMinute = -1;
+ this.talkContent = new DecodedContent(100 + 1);
+
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param type {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void talkType(TalkType type)
+ throws HtmlParseException{
+ this.talkType = type;
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param avatarRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void talkAvatar(DecodedContent content, SeqRange avatarRange)
+ throws HtmlParseException{
+ this.avatar = toAvatar(content, avatarRange);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param hour {@inheritDoc}
+ * @param minute {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void talkTime(int hour, int minute)
+ throws HtmlParseException{
+ this.talkHour = hour;
+ this.talkMinute = minute;
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param tno {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void talkNo(int tno) throws HtmlParseException{
+ this.talkNo = tno;
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param idRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void talkId(DecodedContent content, SeqRange idRange)
+ throws HtmlParseException{
+ this.anchorId = content.subSequence(idRange.getStartPos(),
+ idRange.getEndPos() )
+ .toString();
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param textRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void talkText(DecodedContent content, SeqRange textRange)
+ throws HtmlParseException{
+ this.converter.append(this.talkContent, content, textRange);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void talkBreak()
+ throws HtmlParseException{
+ this.talkContent.append('\n');
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void endTalk() throws HtmlParseException{
+ Talk talk = new Talk(this.period,
+ this.talkType,
+ this.avatar,
+ this.talkNo,
+ this.anchorId,
+ this.talkHour, this.talkMinute,
+ this.talkContent );
+
+ int count = countUp(this.avatar, this.talkType);
+ talk.setCount(count);
+
+ this.period.addTopic(talk);
+
+ this.talkType = null;
+ this.avatar = null;
+ this.talkNo = -1;
+ this.anchorId = null;
+ this.talkHour = -1;
+ this.talkMinute = -1;
+ this.talkContent = null;
+
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param family {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void startSysEvent(EventFamily family)
+ throws HtmlParseException{
+ this.eventFamily = family;
+ this.sysEventType = null;
+ this.eventContent = new DecodedContent();
+ this.avatarList.clear();
+ this.roleList.clear();
+ this.integerList.clear();
+ this.charseqList.clear();
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param type {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventType(SysEventType type)
+ throws HtmlParseException{
+ this.sysEventType = type;
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param contentRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventContent(DecodedContent content,
+ SeqRange contentRange)
+ throws HtmlParseException{
+ this.converter.append(this.eventContent, content, contentRange);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param anchorRange {@inheritDoc}
+ * @param contentRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventContentAnchor(DecodedContent content,
+ SeqRange anchorRange,
+ SeqRange contentRange)
+ throws HtmlParseException{
+ this.converter.append(this.eventContent, content, contentRange);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventContentBreak() throws HtmlParseException{
+ this.eventContent.append('\n');
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param entryNo {@inheritDoc}
+ * @param avatarRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventOnStage(DecodedContent content,
+ int entryNo,
+ SeqRange avatarRange)
+ throws HtmlParseException{
+ Avatar newAvatar = toAvatar(content, avatarRange);
+ this.integerList.add(entryNo);
+ this.avatarList.add(newAvatar);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param role {@inheritDoc}
+ * @param num {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventOpenRole(GameRole role, int num)
+ throws HtmlParseException{
+ this.roleList.add(role);
+ this.integerList.add(num);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param avatarRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventMurdered(DecodedContent content,
+ SeqRange avatarRange)
+ throws HtmlParseException{
+ Avatar murdered = toAvatar(content, avatarRange);
+ this.avatarList.add(murdered);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param avatarRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventSurvivor(DecodedContent content,
+ SeqRange avatarRange)
+ throws HtmlParseException{
+ Avatar survivor = toAvatar(content, avatarRange);
+ this.avatarList.add(survivor);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param voteByRange {@inheritDoc}
+ * @param voteToRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventCounting(DecodedContent content,
+ SeqRange voteByRange,
+ SeqRange voteToRange)
+ throws HtmlParseException{
+ if(voteByRange.isValid()){
+ Avatar voteBy = toAvatar(content, voteByRange);
+ this.avatarList.add(voteBy);
+ }
+ Avatar voteTo = toAvatar(content, voteToRange);
+ this.avatarList.add(voteTo);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param voteByRange {@inheritDoc}
+ * @param voteToRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventCounting2(DecodedContent content,
+ SeqRange voteByRange,
+ SeqRange voteToRange)
+ throws HtmlParseException{
+ sysEventCounting(content, voteByRange, voteToRange);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param avatarRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventSuddenDeath(DecodedContent content,
+ SeqRange avatarRange)
+ throws HtmlParseException{
+ Avatar suddenDeath = toAvatar(content, avatarRange);
+ this.avatarList.add(suddenDeath);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param avatarRange {@inheritDoc}
+ * @param anchorRange {@inheritDoc}
+ * @param loginRange {@inheritDoc}
+ * @param isLiving {@inheritDoc}
+ * @param role {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventPlayerList(DecodedContent content,
+ SeqRange avatarRange,
+ SeqRange anchorRange,
+ SeqRange loginRange,
+ boolean isLiving,
+ GameRole role )
+ throws HtmlParseException{
+ Avatar who = toAvatar(content, avatarRange);
+
+ CharSequence anchor;
+ if(anchorRange.isValid()){
+ anchor = this.converter.convert(content, anchorRange);
+ }else{
+ anchor = "";
+ }
+ CharSequence account = this.converter
+ .convert(content, loginRange);
+
+ Integer liveOrDead;
+ if(isLiving) liveOrDead = 1;
+ else liveOrDead = 0;
+
+ this.avatarList.add(who);
+ this.charseqList.add(anchor);
+ this.charseqList.add(account);
+ this.integerList.add(liveOrDead);
+ this.roleList.add(role);
+
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param avatarRange {@inheritDoc}
+ * @param votes {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventExecution(DecodedContent content,
+ SeqRange avatarRange,
+ int votes )
+ throws HtmlParseException{
+ Avatar who = toAvatar(content, avatarRange);
+
+ this.avatarList.add(who);
+ this.integerList.add(votes);
+
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param hour {@inheritDoc}
+ * @param minute {@inheritDoc}
+ * @param minLimit {@inheritDoc}
+ * @param maxLimit {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventAskEntry(int hour, int minute,
+ int minLimit, int maxLimit)
+ throws HtmlParseException{
+ this.integerList.add(hour * 60 + minute);
+ this.integerList.add(minLimit);
+ this.integerList.add(maxLimit);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param hour {@inheritDoc}
+ * @param minute {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventAskCommit(int hour, int minute)
+ throws HtmlParseException{
+ this.integerList.add(hour * 60 + minute);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param avatarRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventNoComment(DecodedContent content,
+ SeqRange avatarRange)
+ throws HtmlParseException{
+ Avatar noComAvatar = toAvatar(content, avatarRange);
+ this.avatarList.add(noComAvatar);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param winner {@inheritDoc}
+ * @param hour {@inheritDoc}
+ * @param minute {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventStayEpilogue(Team winner, int hour, int minute)
+ throws HtmlParseException{
+ GameRole role = null;
+
+ switch(winner){
+ case VILLAGE: role = GameRole.INNOCENT; break;
+ case WOLF: role = GameRole.WOLF; break;
+ case HAMSTER: role = GameRole.HAMSTER; break;
+ default: assert false; break;
+ }
+
+ this.roleList.add(role);
+ this.integerList.add(hour * 60 + minute);
+
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param guardByRange {@inheritDoc}
+ * @param guardToRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventGuard(DecodedContent content,
+ SeqRange guardByRange,
+ SeqRange guardToRange)
+ throws HtmlParseException{
+ Avatar guardBy = toAvatar(content, guardByRange);
+ Avatar guardTo = toAvatar(content, guardToRange);
+ this.avatarList.add(guardBy);
+ this.avatarList.add(guardTo);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @param content {@inheritDoc}
+ * @param judgeByRange {@inheritDoc}
+ * @param judgeToRange {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void sysEventJudge(DecodedContent content,
+ SeqRange judgeByRange,
+ SeqRange judgeToRange)
+ throws HtmlParseException{
+ Avatar judgeBy = toAvatar(content, judgeByRange);
+ Avatar judgeTo = toAvatar(content, judgeToRange);
+ this.avatarList.add(judgeBy);
+ this.avatarList.add(judgeTo);
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void endSysEvent() throws HtmlParseException{
+ SysEvent event = new SysEvent();
+ event.setEventFamily(this.eventFamily);
+ event.setSysEventType(this.sysEventType);
+ event.setContent(this.eventContent);
+ event.addAvatarList(this.avatarList);
+ event.addRoleList(this.roleList);
+ event.addIntegerList(this.integerList);
+ event.addCharSequenceList(this.charseqList);
+
+ this.period.addTopic(event);
+
+ if( this.sysEventType == SysEventType.MURDERED
+ || this.sysEventType == SysEventType.NOMURDER ){
+ for(Topic topic : this.period.topicList){
+ if( ! (topic instanceof Talk) ) continue;
+ Talk talk = (Talk) topic;
+ if(talk.getTalkType() != TalkType.WOLFONLY) continue;
+ if( ! StringUtils
+ .isTerminated(talk.getDialog(),
+ "!\u0020今日がお前の命日だ!") ){
+ continue;
+ }
+ talk.setCount(-1);
+ this.countMap.clear();
+ }
+ }
+
+ this.eventFamily = null;
+ this.sysEventType = null;
+ this.eventContent = null;
+ this.avatarList.clear();
+ this.roleList.clear();
+ this.integerList.clear();
+ this.charseqList.clear();
+
+ return;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws HtmlParseException {@inheritDoc}
+ */
+ @Override
+ public void endParse() throws HtmlParseException{
+ return;
+ }
+
+ // TODO 村名のチェックは不要か?
+ }
+
+}