4 * License : The MIT License
5 * Copyright(c) 2008 olyutorskii
8 package jp.sfjp.jindolf.data;
10 import java.awt.image.BufferedImage;
11 import java.io.IOException;
12 import java.text.MessageFormat;
13 import java.util.Collections;
14 import java.util.Comparator;
15 import java.util.HashMap;
16 import java.util.LinkedList;
17 import java.util.List;
19 import java.util.logging.Level;
20 import java.util.logging.Logger;
21 import jp.sfjp.jindolf.net.HtmlSequence;
22 import jp.sfjp.jindolf.net.ServerAccess;
23 import jp.sfjp.jindolf.util.GUIUtils;
24 import jp.sourceforge.jindolf.corelib.LandDef;
25 import jp.sourceforge.jindolf.corelib.LandState;
26 import jp.sourceforge.jindolf.corelib.PeriodType;
27 import jp.sourceforge.jindolf.corelib.VillageState;
28 import jp.sourceforge.jindolf.parser.DecodedContent;
29 import jp.sourceforge.jindolf.parser.HtmlAdapter;
30 import jp.sourceforge.jindolf.parser.HtmlParseException;
31 import jp.sourceforge.jindolf.parser.HtmlParser;
32 import jp.sourceforge.jindolf.parser.PageType;
33 import jp.sourceforge.jindolf.parser.SeqRange;
38 public class Village implements Comparable<Village> {
40 private static final int GID_MIN = 3;
42 private static final Comparator<Village> VILLAGE_COMPARATOR =
43 new VillageComparator();
45 private static final HtmlParser PARSER = new HtmlParser();
46 private static final VillageHeadHandler HANDLER =
47 new VillageHeadHandler();
49 private static final Logger LOGGER = Logger.getAnonymousLogger();
52 PARSER.setBasicHandler (HANDLER);
53 PARSER.setSysEventHandler(HANDLER);
54 PARSER.setTalkHandler (HANDLER);
58 private final Land parentLand;
59 private final String villageID;
60 private final int villageIDNum;
61 private final String villageName;
63 private final boolean isValid;
65 private int limitMonth;
67 private int limitHour;
68 private int limitMinute;
70 private VillageState state = VillageState.UNKNOWN;
72 private final LinkedList<Period> periodList = new LinkedList<>();
73 private final List<Period> unmodList =
74 Collections.unmodifiableList(this.periodList);
76 private final Map<String, Avatar> avatarMap =
79 private final Map<Avatar, BufferedImage> faceImageMap =
81 private final Map<Avatar, BufferedImage> bodyImageMap =
83 private final Map<Avatar, BufferedImage> faceMonoImageMap =
85 private final Map<Avatar, BufferedImage> bodyMonoImageMap =
91 * @param parentLand Villageの所属する国
92 * @param villageID 村のID
93 * @param villageName 村の名前
95 public Village(Land parentLand, String villageID, String villageName) {
96 this.parentLand = parentLand;
97 this.villageID = villageID.intern();
98 this.villageIDNum = Integer.parseInt(this.villageID);
99 this.villageName = villageName.intern();
101 this.isValid = this.parentLand.getLandDef()
102 .isValidVillageId(this.villageIDNum);
109 * 村同士を比較するためのComparatorを返す。
110 * @return Comparatorインスタンス
112 public static Comparator<Village> comparator(){
113 return VILLAGE_COMPARATOR;
117 * 人狼BBSサーバからPeriod一覧情報が含まれたHTMLを取得し、
120 * @throws java.io.IOException ネットワーク入出力の異常
122 public static synchronized void updateVillage(Village village)
124 Land land = village.getParentLand();
125 LandDef landDef = land.getLandDef();
126 LandState landState = landDef.getLandState();
127 ServerAccess server = land.getServerAccess();
130 if(landState == LandState.ACTIVE){
131 html = server.getHTMLBoneHead(village);
133 html = server.getHTMLVillage(village);
136 DecodedContent content = html.getContent();
137 HANDLER.setVillage(village);
139 PARSER.parseAutomatic(content);
140 }catch(HtmlParseException e){
141 LOGGER.log(Level.WARNING, "村の状態が不明", e);
149 * @return 村の所属する国(Land)
151 public Land getParentLand(){
152 return this.parentLand;
159 public String getVillageID(){
160 return this.villageID;
167 public int getVillageIDNum(){
168 return this.villageIDNum;
175 public String getVillageName(){
176 StringBuilder name = new StringBuilder();
178 LandDef landDef = this.parentLand.getLandDef();
179 String prefix = landDef.getLandPrefix();
182 StringBuilder id = new StringBuilder(this.villageID);
183 if(landDef.getLandId().equals("wolfg")){
184 while(id.length() < GID_MIN){
190 String result = name.toString();
198 public String getVillageFullName(){
199 return this.villageName;
206 public VillageState getState(){
214 public void setState(VillageState state){
223 public Period getPrologue(){
224 for(Period period : this.periodList){
225 if(period.isPrologue()) return period;
234 public Period getEpilogue(){
235 for(Period period : this.periodList){
236 if(period.isEpilogue()) return period;
246 public Period getProgress(int day){
247 for(Period period : this.periodList){
248 if( period.isProgress()
249 && period.getDay() == day ) return period;
255 * PROGRESS状態のPeriodの総数を返す。
256 * @return PROGRESS状態のPeriod総数
258 public int getProgressDays(){
260 for(Period period : this.periodList){
261 if(period.isProgress()) result++;
267 * 指定されたPeriodインデックスのPeriodを返す。
268 * プロローグやエピローグへのアクセスも可能。
269 * @param day Periodインデックス
272 public Period getPeriod(int day){
273 return this.periodList.get(day);
277 * 指定されたアンカーの対象のPeriodを返す。
281 public Period getPeriod(Anchor anchor){
284 if(anchor.isEpilogueDay()){
285 anchorPeriod = getEpilogue();
289 int anchorDay = anchor.getDay();
290 anchorPeriod = getPeriod(anchorDay);
299 public int getPeriodSize(){
300 return this.periodList.size();
305 * @return Periodのリスト。
307 public List<Period> getPeriodList(){
308 return this.unmodList;
312 * 指定した名前で村に登録されているAvatarを返す。
313 * @param fullName Avatarの名前
316 public Avatar getAvatar(String fullName){
317 // TODO CharSequenceにできない?
320 avatar = Avatar.getPredefinedAvatar(fullName);
321 if( avatar != null ){
322 preloadAvatarFace(avatar);
326 avatar = this.avatarMap.get(fullName);
327 if( avatar != null ){
328 preloadAvatarFace(avatar);
336 * Avatarの顔画像を事前にロードする。
337 * @param avatar Avatar
339 private void preloadAvatarFace(Avatar avatar){
340 if(this.faceImageMap.get(avatar) != null) return;
342 Land land = getParentLand();
343 LandDef landDef = land.getLandDef();
345 String template = landDef.getFaceURITemplate();
346 int serialNo = avatar.getIdNum();
347 String uri = MessageFormat.format(template, serialNo);
349 BufferedImage image = land.downloadImage(uri);
350 if(image == null) image = GUIUtils.getNoImage();
352 this.faceImageMap.put(avatar, image);
359 * @param avatar Avatar
361 // 未知のAvatar出現時の処理が不完全
362 public void addAvatar(Avatar avatar){
363 if(avatar == null) return;
364 String fullName = avatar.getFullName();
365 this.avatarMap.put(fullName, avatar);
367 preloadAvatarFace(avatar);
373 * 村に登録されたAvatarの顔イメージを返す。
374 * @param avatar Avatar
377 // TODO 失敗したらプロローグを強制読み込みして再トライしたい
378 public BufferedImage getAvatarFaceImage(Avatar avatar){
379 return this.faceImageMap.get(avatar);
383 * 村に登録されたAvatarの全身像イメージを返す。
384 * @param avatar Avatar
387 public BufferedImage getAvatarBodyImage(Avatar avatar){
388 BufferedImage result;
389 result = this.bodyImageMap.get(avatar);
390 if(result != null) return result;
392 Land land = getParentLand();
393 LandDef landDef = land.getLandDef();
395 String template = landDef.getBodyURITemplate();
396 int serialNo = avatar.getIdNum();
397 String uri = MessageFormat.format(template, serialNo);
399 result = land.downloadImage(uri);
400 if(result == null) result = GUIUtils.getNoImage();
402 this.bodyImageMap.put(avatar, result);
408 * 村に登録されたAvatarのモノクロ顔イメージを返す。
409 * @param avatar Avatar
412 public BufferedImage getAvatarFaceMonoImage(Avatar avatar){
413 BufferedImage result;
414 result = this.faceMonoImageMap.get(avatar);
416 result = getAvatarFaceImage(avatar);
417 result = GUIUtils.createMonoImage(result);
418 this.faceMonoImageMap.put(avatar, result);
424 * 村に登録されたAvatarの全身像イメージを返す。
425 * @param avatar Avatar
428 public BufferedImage getAvatarBodyMonoImage(Avatar avatar){
429 BufferedImage result;
430 result = this.bodyMonoImageMap.get(avatar);
432 result = getAvatarBodyImage(avatar);
433 result = GUIUtils.createMonoImage(result);
434 this.bodyMonoImageMap.put(avatar, result);
443 public BufferedImage getGraveImage(){
444 BufferedImage result = getParentLand().getGraveIconImage();
449 * 国に登録された墓イメージ(大)を返す。
452 public BufferedImage getGraveBodyImage(){
453 BufferedImage result = getParentLand().getGraveBodyImage();
458 * 村にアクセスするためのCGIクエリーを返す。
461 public CharSequence getCGIQuery(){
462 StringBuilder result = new StringBuilder();
463 result.append("?vid=").append(getVillageID());
471 public int getLimitMonth(){
472 return this.limitMonth;
479 public int getLimitDay(){
480 return this.limitDay;
487 public int getLimitHour(){
488 return this.limitHour;
495 public int getLimitMinute(){
496 return this.limitMinute;
501 * @return 無効な村ならfalse
503 public boolean isValid(){
508 * Periodリストの指定したインデックスにPeriodを上書きする。
509 * リストのサイズと同じインデックスを指定する事が許される。
510 * その場合の動作はList.addと同じ。
511 * @param index Periodリストのインデックス。
512 * @param period 上書きするPeriod
513 * @throws java.lang.IndexOutOfBoundsException インデックスの指定がおかしい
515 private void setPeriod(int index, Period period)
516 throws IndexOutOfBoundsException{
517 int listSize = this.periodList.size();
518 if(index == listSize){
519 this.periodList.add(period);
520 }else if(index < listSize){
521 this.periodList.set(index, period);
523 throw new IndexOutOfBoundsException();
529 * アンカーに一致する会話(Talk)のリストを取得する。
532 * @throws java.io.IOException おそらくネットワークエラー
534 public List<Talk> getTalkListFromAnchor(Anchor anchor)
536 List<Talk> result = new LinkedList<>();
539 if(anchor.hasTalkNo()){
540 // 事前に全Periodがロードされているのが前提
541 for(Period period : this.periodList){
542 Talk talk = period.getNumberedTalk(anchor.getTalkNo());
543 if(talk == null) continue;
549 Period anchorPeriod = getPeriod(anchor);
550 if(anchorPeriod == null) return result;
552 Period.parsePeriod(anchorPeriod, false);
554 for(Topic topic : anchorPeriod.getTopicList()){
555 if( ! (topic instanceof Talk) ) continue;
556 Talk talk = (Talk) topic;
557 if(talk.getHour() != anchor.getHour() ) continue;
558 if(talk.getMinute() != anchor.getMinute()) continue;
565 * 全Periodの発言データをアンロードする。
567 public void unloadPeriods(){
568 for(Period period : this.periodList){
577 * @param village {@inheritDoc}
578 * @return {@inheritDoc}
581 public int compareTo(Village village){
582 int cmpResult = VILLAGE_COMPARATOR.compare(this, village);
589 * @param obj {@inheritDoc}
590 * @return {@inheritDoc}
593 public boolean equals(Object obj){
594 if(obj == null) return false;
595 if( ! (obj instanceof Village) ) return false;
596 Village village = (Village) obj;
598 if( getParentLand() != village.getParentLand() ) return false;
600 int cmpResult = compareTo(village);
601 if(cmpResult == 0) return true;
607 * @return {@inheritDoc}
610 public int hashCode(){
611 int homeHash = getParentLand().hashCode();
612 int vidHash = getVillageID().hashCode();
613 int result = homeHash ^ vidHash;
624 public String toString(){
625 return getVillageFullName();
632 private static class VillageHeadHandler extends HtmlAdapter{
634 private Village village = null;
636 private boolean hasPrologue;
637 private boolean hasProgress;
638 private boolean hasEpilogue;
639 private boolean hasDone;
640 private int maxProgress;
645 public VillageHeadHandler(){
654 public void setVillage(Village village){
655 this.village = village;
663 this.hasPrologue = false;
664 this.hasProgress = false;
665 this.hasEpilogue = false;
666 this.hasDone = false;
667 this.maxProgress = 0;
675 public VillageState getVillageState(){
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;
686 return VillageState.UNKNOWN;
691 * @param content {@inheritDoc}
692 * @throws HtmlParseException {@inheritDoc}
695 public void startParse(DecodedContent content)
696 throws HtmlParseException{
703 * 自動判定の結果が日ページでなければ例外を投げる。
704 * @param type {@inheritDoc}
705 * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
708 public void pageType(PageType type) throws HtmlParseException{
709 if(type != PageType.PERIOD_PAGE){
710 throw new HtmlParseException(
718 * @param month {@inheritDoc}
719 * @param day {@inheritDoc}
720 * @param hour {@inheritDoc}
721 * @param minute {@inheritDoc}
722 * @throws HtmlParseException {@inheritDoc}
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;
738 * @param content {@inheritDoc}
739 * @param anchorRange {@inheritDoc}
740 * @param periodType {@inheritDoc}
741 * @param day {@inheritDoc}
742 * @throws HtmlParseException {@inheritDoc}
745 public void periodLink(DecodedContent content,
746 SeqRange anchorRange,
747 PeriodType periodType,
749 throws HtmlParseException{
750 if(periodType == null){
757 this.hasPrologue = true;
760 this.hasProgress = true;
761 this.maxProgress = day;
764 this.hasEpilogue = true;
776 * @throws HtmlParseException {@inheritDoc}
779 public void endParse() throws HtmlParseException{
780 Land land = this.village.getParentLand();
781 LandDef landDef = land.getLandDef();
782 LandState landState = landDef.getLandState();
784 VillageState villageState = getVillageState();
785 if(villageState == VillageState.UNKNOWN){
786 this.village.setState(villageState);
787 this.village.periodList.clear();
788 LOGGER.warning("村の状況を読み取れません");
792 if(landState == LandState.ACTIVE){
793 this.village.setState(villageState);
795 this.village.setState(VillageState.GAMEOVER);
804 * 抽出したリンク情報に伴いPeriodリストを更新する。
805 * まだPeriodデータのロードは行われない。
806 * ゲーム進行中の村で更新時刻をまたいで更新が行われた場合、
807 * 既存のPeriodリストが伸張する場合がある。
809 private void modifyPeriodList(){
810 Period lastPeriod = null;
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);
819 lastPeriod = prologue;
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);
831 lastPeriod = progress;
836 if(this.hasEpilogue){
837 Period epilogue = this.village.getEpilogue();
838 if(epilogue == null){
839 lastPeriod = new Period(this.village,
841 this.maxProgress +1);
842 this.village.setPeriod(this.maxProgress +1, lastPeriod);
844 lastPeriod = epilogue;
848 assert this.village.getPeriodSize() > 0;
849 assert lastPeriod != null;
852 // リロードで村が縮むわけないじゃん。みんな大げさだなあ
853 while(this.village.periodList.getLast() != lastPeriod){
854 this.village.periodList.removeLast();
857 if(this.village.getState() != VillageState.GAMEOVER){
858 lastPeriod.setHot(true);
868 * 村同士を比較するためのComparator。
870 private static class VillageComparator implements Comparator<Village> {
875 public VillageComparator(){
882 * @param v1 {@inheritDoc}
883 * @param v2 {@inheritDoc}
884 * @return {@inheritDoc}
887 public int compare(Village v1, Village v2){
889 if(v1 == null) v1Num = Integer.MIN_VALUE;
890 else v1Num = v1.getVillageIDNum();
893 if(v2 == null) v2Num = Integer.MIN_VALUE;
894 else v2Num = v2.getVillageIDNum();
896 return v1Num - v2Num;