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> {
41 * 村同士を比較するためのComparator。
43 private static class VillageComparator implements Comparator<Village> {
48 public VillageComparator(){
55 * @param v1 {@inheritDoc}
56 * @param v2 {@inheritDoc}
57 * @return {@inheritDoc}
60 public int compare(Village v1, Village v2){
62 if(v1 == null) v1Num = Integer.MIN_VALUE;
63 else v1Num = v1.getVillageIDNum();
66 if(v2 == null) v2Num = Integer.MIN_VALUE;
67 else v2Num = v2.getVillageIDNum();
74 private static final int GID_MIN = 3;
76 private static final Comparator<Village> VILLAGE_COMPARATOR =
77 new VillageComparator();
79 private static final HtmlParser PARSER = new HtmlParser();
80 private static final VillageHeadHandler HANDLER =
81 new VillageHeadHandler();
83 private static final Logger LOGGER = Logger.getAnonymousLogger();
86 PARSER.setBasicHandler (HANDLER);
87 PARSER.setSysEventHandler(HANDLER);
88 PARSER.setTalkHandler (HANDLER);
92 private final Land parentLand;
93 private final String villageID;
94 private final int villageIDNum;
95 private final String villageName;
97 private final boolean isValid;
99 private int limitMonth;
100 private int limitDay;
101 private int limitHour;
102 private int limitMinute;
104 private VillageState state = VillageState.UNKNOWN;
106 private final LinkedList<Period> periodList = new LinkedList<>();
107 private final List<Period> unmodList =
108 Collections.unmodifiableList(this.periodList);
110 private final Map<String, Avatar> avatarMap =
113 private final Map<Avatar, BufferedImage> faceImageMap =
115 private final Map<Avatar, BufferedImage> bodyImageMap =
117 private final Map<Avatar, BufferedImage> faceMonoImageMap =
119 private final Map<Avatar, BufferedImage> bodyMonoImageMap =
125 * @param parentLand Villageの所属する国
126 * @param villageID 村のID
127 * @param villageName 村の名前
129 public Village(Land parentLand, String villageID, String villageName) {
130 this.parentLand = parentLand;
131 this.villageID = villageID.intern();
132 this.villageIDNum = Integer.parseInt(this.villageID);
133 this.villageName = villageName.intern();
135 this.isValid = this.parentLand.getLandDef()
136 .isValidVillageId(this.villageIDNum);
143 * 村同士を比較するためのComparatorを返す。
144 * @return Comparatorインスタンス
146 public static Comparator<Village> comparator(){
147 return VILLAGE_COMPARATOR;
151 * 人狼BBSサーバからPeriod一覧情報が含まれたHTMLを取得し、
154 * @throws java.io.IOException ネットワーク入出力の異常
156 public static synchronized void updateVillage(Village village)
158 Land land = village.getParentLand();
159 LandDef landDef = land.getLandDef();
160 LandState landState = landDef.getLandState();
161 ServerAccess server = land.getServerAccess();
164 if(landState == LandState.ACTIVE){
165 html = server.getHTMLBoneHead(village);
167 html = server.getHTMLVillage(village);
170 DecodedContent content = html.getContent();
171 HANDLER.setVillage(village);
173 PARSER.parseAutomatic(content);
174 }catch(HtmlParseException e){
175 LOGGER.log(Level.WARNING, "村の状態が不明", e);
183 * @return 村の所属する国(Land)
185 public Land getParentLand(){
186 return this.parentLand;
193 public String getVillageID(){
194 return this.villageID;
201 public int getVillageIDNum(){
202 return this.villageIDNum;
209 public String getVillageName(){
210 StringBuilder name = new StringBuilder();
212 LandDef landDef = this.parentLand.getLandDef();
213 String prefix = landDef.getLandPrefix();
216 StringBuilder id = new StringBuilder(this.villageID);
217 if(landDef.getLandId().equals("wolfg")){
218 while(id.length() < GID_MIN){
224 String result = name.toString();
232 public String getVillageFullName(){
233 return this.villageName;
240 public VillageState getState(){
248 public void setState(VillageState state){
257 public Period getPrologue(){
258 for(Period period : this.periodList){
259 if(period.isPrologue()) return period;
268 public Period getEpilogue(){
269 for(Period period : this.periodList){
270 if(period.isEpilogue()) return period;
280 public Period getProgress(int day){
281 for(Period period : this.periodList){
282 if( period.isProgress()
283 && period.getDay() == day ) return period;
289 * PROGRESS状態のPeriodの総数を返す。
290 * @return PROGRESS状態のPeriod総数
292 public int getProgressDays(){
294 for(Period period : this.periodList){
295 if(period.isProgress()) result++;
301 * 指定されたPeriodインデックスのPeriodを返す。
302 * プロローグやエピローグへのアクセスも可能。
303 * @param day Periodインデックス
306 public Period getPeriod(int day){
307 return this.periodList.get(day);
311 * 指定されたアンカーの対象のPeriodを返す。
315 public Period getPeriod(Anchor anchor){
318 if(anchor.isEpilogueDay()){
319 anchorPeriod = getEpilogue();
323 int anchorDay = anchor.getDay();
324 anchorPeriod = getPeriod(anchorDay);
333 public int getPeriodSize(){
334 return this.periodList.size();
339 * @return Periodのリスト。
341 public List<Period> getPeriodList(){
342 return this.unmodList;
346 * 指定した名前で村に登録されているAvatarを返す。
347 * @param fullName Avatarの名前
350 public Avatar getAvatar(String fullName){
351 // TODO CharSequenceにできない?
354 avatar = Avatar.getPredefinedAvatar(fullName);
355 if( avatar != null ){
356 preloadAvatarFace(avatar);
360 avatar = this.avatarMap.get(fullName);
361 if( avatar != null ){
362 preloadAvatarFace(avatar);
370 * Avatarの顔画像を事前にロードする。
371 * @param avatar Avatar
373 private void preloadAvatarFace(Avatar avatar){
374 if(this.faceImageMap.get(avatar) != null) return;
376 Land land = getParentLand();
377 LandDef landDef = land.getLandDef();
379 String template = landDef.getFaceURITemplate();
380 int serialNo = avatar.getIdNum();
381 String uri = MessageFormat.format(template, serialNo);
383 BufferedImage image = land.downloadImage(uri);
384 if(image == null) image = GUIUtils.getNoImage();
386 this.faceImageMap.put(avatar, image);
393 * @param avatar Avatar
395 // 未知のAvatar出現時の処理が不完全
396 public void addAvatar(Avatar avatar){
397 if(avatar == null) return;
398 String fullName = avatar.getFullName();
399 this.avatarMap.put(fullName, avatar);
401 preloadAvatarFace(avatar);
407 * 村に登録されたAvatarの顔イメージを返す。
408 * @param avatar Avatar
411 // TODO 失敗したらプロローグを強制読み込みして再トライしたい
412 public BufferedImage getAvatarFaceImage(Avatar avatar){
413 return this.faceImageMap.get(avatar);
417 * 村に登録されたAvatarの全身像イメージを返す。
418 * @param avatar Avatar
421 public BufferedImage getAvatarBodyImage(Avatar avatar){
422 BufferedImage result;
423 result = this.bodyImageMap.get(avatar);
424 if(result != null) return result;
426 Land land = getParentLand();
427 LandDef landDef = land.getLandDef();
429 String template = landDef.getBodyURITemplate();
430 int serialNo = avatar.getIdNum();
431 String uri = MessageFormat.format(template, serialNo);
433 result = land.downloadImage(uri);
434 if(result == null) result = GUIUtils.getNoImage();
436 this.bodyImageMap.put(avatar, result);
442 * 村に登録されたAvatarのモノクロ顔イメージを返す。
443 * @param avatar Avatar
446 public BufferedImage getAvatarFaceMonoImage(Avatar avatar){
447 BufferedImage result;
448 result = this.faceMonoImageMap.get(avatar);
450 result = getAvatarFaceImage(avatar);
451 result = GUIUtils.createMonoImage(result);
452 this.faceMonoImageMap.put(avatar, result);
458 * 村に登録されたAvatarの全身像イメージを返す。
459 * @param avatar Avatar
462 public BufferedImage getAvatarBodyMonoImage(Avatar avatar){
463 BufferedImage result;
464 result = this.bodyMonoImageMap.get(avatar);
466 result = getAvatarBodyImage(avatar);
467 result = GUIUtils.createMonoImage(result);
468 this.bodyMonoImageMap.put(avatar, result);
477 public BufferedImage getGraveImage(){
478 BufferedImage result = getParentLand().getGraveIconImage();
483 * 国に登録された墓イメージ(大)を返す。
486 public BufferedImage getGraveBodyImage(){
487 BufferedImage result = getParentLand().getGraveBodyImage();
492 * 村にアクセスするためのCGIクエリーを返す。
495 public CharSequence getCGIQuery(){
496 StringBuilder result = new StringBuilder();
497 result.append("?vid=").append(getVillageID());
505 public int getLimitMonth(){
506 return this.limitMonth;
513 public int getLimitDay(){
514 return this.limitDay;
521 public int getLimitHour(){
522 return this.limitHour;
529 public int getLimitMinute(){
530 return this.limitMinute;
535 * @return 無効な村ならfalse
537 public boolean isValid(){
542 * Periodリストの指定したインデックスにPeriodを上書きする。
543 * リストのサイズと同じインデックスを指定する事が許される。
544 * その場合の動作はList.addと同じ。
545 * @param index Periodリストのインデックス。
546 * @param period 上書きするPeriod
547 * @throws java.lang.IndexOutOfBoundsException インデックスの指定がおかしい
549 private void setPeriod(int index, Period period)
550 throws IndexOutOfBoundsException{
551 int listSize = this.periodList.size();
552 if(index == listSize){
553 this.periodList.add(period);
554 }else if(index < listSize){
555 this.periodList.set(index, period);
557 throw new IndexOutOfBoundsException();
563 * アンカーに一致する会話(Talk)のリストを取得する。
566 * @throws java.io.IOException おそらくネットワークエラー
568 public List<Talk> getTalkListFromAnchor(Anchor anchor)
570 List<Talk> result = new LinkedList<>();
573 if(anchor.hasTalkNo()){
574 // 事前に全Periodがロードされているのが前提
575 for(Period period : this.periodList){
576 Talk talk = period.getNumberedTalk(anchor.getTalkNo());
577 if(talk == null) continue;
583 Period anchorPeriod = getPeriod(anchor);
584 if(anchorPeriod == null) return result;
586 Period.parsePeriod(anchorPeriod, false);
588 for(Topic topic : anchorPeriod.getTopicList()){
589 if( ! (topic instanceof Talk) ) continue;
590 Talk talk = (Talk) topic;
591 if(talk.getHour() != anchor.getHour() ) continue;
592 if(talk.getMinute() != anchor.getMinute()) continue;
599 * 全Periodの発言データをアンロードする。
601 public void unloadPeriods(){
602 for(Period period : this.periodList){
611 * @param village {@inheritDoc}
612 * @return {@inheritDoc}
615 public int compareTo(Village village){
616 int cmpResult = VILLAGE_COMPARATOR.compare(this, village);
623 * @param obj {@inheritDoc}
624 * @return {@inheritDoc}
627 public boolean equals(Object obj){
628 if(obj == null) return false;
629 if( ! (obj instanceof Village) ) return false;
630 Village village = (Village) obj;
632 if( getParentLand() != village.getParentLand() ) return false;
634 int cmpResult = compareTo(village);
635 if(cmpResult == 0) return true;
641 * @return {@inheritDoc}
644 public int hashCode(){
645 int homeHash = getParentLand().hashCode();
646 int vidHash = getVillageID().hashCode();
647 int result = homeHash ^ vidHash;
658 public String toString(){
659 return getVillageFullName();
665 private static class VillageHeadHandler extends HtmlAdapter{
667 private Village village = null;
669 private boolean hasPrologue;
670 private boolean hasProgress;
671 private boolean hasEpilogue;
672 private boolean hasDone;
673 private int maxProgress;
678 public VillageHeadHandler(){
687 public void setVillage(Village village){
688 this.village = village;
696 this.hasPrologue = false;
697 this.hasProgress = false;
698 this.hasEpilogue = false;
699 this.hasDone = false;
700 this.maxProgress = 0;
708 public VillageState getVillageState(){
710 return VillageState.GAMEOVER;
711 }else if(this.hasEpilogue){
712 return VillageState.EPILOGUE;
713 }else if(this.hasProgress){
714 return VillageState.PROGRESS;
715 }else if(this.hasPrologue){
716 return VillageState.PROLOGUE;
719 return VillageState.UNKNOWN;
724 * @param content {@inheritDoc}
725 * @throws HtmlParseException {@inheritDoc}
728 public void startParse(DecodedContent content)
729 throws HtmlParseException{
736 * 自動判定の結果が日ページでなければ例外を投げる。
737 * @param type {@inheritDoc}
738 * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
741 public void pageType(PageType type) throws HtmlParseException{
742 if(type != PageType.PERIOD_PAGE){
743 throw new HtmlParseException(
751 * @param month {@inheritDoc}
752 * @param day {@inheritDoc}
753 * @param hour {@inheritDoc}
754 * @param minute {@inheritDoc}
755 * @throws HtmlParseException {@inheritDoc}
758 public void commitTime(int month, int day,
759 int hour, int minute)
760 throws HtmlParseException{
761 this.village.limitMonth = month;
762 this.village.limitDay = day;
763 this.village.limitHour = hour;
764 this.village.limitMinute = minute;
771 * @param content {@inheritDoc}
772 * @param anchorRange {@inheritDoc}
773 * @param periodType {@inheritDoc}
774 * @param day {@inheritDoc}
775 * @throws HtmlParseException {@inheritDoc}
778 public void periodLink(DecodedContent content,
779 SeqRange anchorRange,
780 PeriodType periodType,
782 throws HtmlParseException{
783 if(periodType == null){
790 this.hasPrologue = true;
793 this.hasProgress = true;
794 this.maxProgress = day;
797 this.hasEpilogue = true;
809 * @throws HtmlParseException {@inheritDoc}
812 public void endParse() throws HtmlParseException{
813 Land land = this.village.getParentLand();
814 LandDef landDef = land.getLandDef();
815 LandState landState = landDef.getLandState();
817 VillageState villageState = getVillageState();
818 if(villageState == VillageState.UNKNOWN){
819 this.village.setState(villageState);
820 this.village.periodList.clear();
821 LOGGER.warning("村の状況を読み取れません");
825 if(landState == LandState.ACTIVE){
826 this.village.setState(villageState);
828 this.village.setState(VillageState.GAMEOVER);
837 * 抽出したリンク情報に伴いPeriodリストを更新する。
838 * まだPeriodデータのロードは行われない。
839 * ゲーム進行中の村で更新時刻をまたいで更新が行われた場合、
840 * 既存のPeriodリストが伸張する場合がある。
842 private void modifyPeriodList(){
843 Period lastPeriod = null;
845 if(this.hasPrologue){
846 Period prologue = this.village.getPrologue();
847 if(prologue == null){
848 lastPeriod = new Period(this.village,
849 PeriodType.PROLOGUE, 0);
850 this.village.setPeriod(0, lastPeriod);
852 lastPeriod = prologue;
856 if(this.hasProgress){
857 for(int day = 1; day <= this.maxProgress; day++){
858 Period progress = this.village.getProgress(day);
859 if(progress == null){
860 lastPeriod = new Period(this.village,
861 PeriodType.PROGRESS, day);
862 this.village.setPeriod(day, lastPeriod);
864 lastPeriod = progress;
869 if(this.hasEpilogue){
870 Period epilogue = this.village.getEpilogue();
871 if(epilogue == null){
872 lastPeriod = new Period(this.village,
874 this.maxProgress +1);
875 this.village.setPeriod(this.maxProgress +1, lastPeriod);
877 lastPeriod = epilogue;
881 assert this.village.getPeriodSize() > 0;
882 assert lastPeriod != null;
885 // リロードで村が縮むわけないじゃん。みんな大げさだなあ
886 while(this.village.periodList.getLast() != lastPeriod){
887 this.village.periodList.removeLast();
890 if(this.village.getState() != VillageState.GAMEOVER){
891 lastPeriod.setHot(true);