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 Comparator<Village> VILLAGE_COMPARATOR =
75 new VillageComparator();
77 private static final HtmlParser PARSER = new HtmlParser();
78 private static final VillageHeadHandler HANDLER =
79 new VillageHeadHandler();
81 private static final Logger LOGGER = Logger.getAnonymousLogger();
84 PARSER.setBasicHandler (HANDLER);
85 PARSER.setSysEventHandler(HANDLER);
86 PARSER.setTalkHandler (HANDLER);
90 private final Land parentLand;
91 private final String villageID;
92 private final int villageIDNum;
93 private final String villageName;
95 private final boolean isValid;
97 private int limitMonth;
99 private int limitHour;
100 private int limitMinute;
102 private VillageState state = VillageState.UNKNOWN;
104 private final LinkedList<Period> periodList = new LinkedList<Period>();
105 private final List<Period> unmodList =
106 Collections.unmodifiableList(this.periodList);
108 private final Map<String, Avatar> avatarMap =
109 new HashMap<String, Avatar>();
111 private final Map<Avatar, BufferedImage> faceImageMap =
112 new HashMap<Avatar, BufferedImage>();
113 private final Map<Avatar, BufferedImage> bodyImageMap =
114 new HashMap<Avatar, BufferedImage>();
115 private final Map<Avatar, BufferedImage> faceMonoImageMap =
116 new HashMap<Avatar, BufferedImage>();
117 private final Map<Avatar, BufferedImage> bodyMonoImageMap =
118 new HashMap<Avatar, BufferedImage>();
123 * @param parentLand Villageの所属する国
124 * @param villageID 村のID
125 * @param villageName 村の名前
127 public Village(Land parentLand, String villageID, String villageName) {
128 this.parentLand = parentLand;
129 this.villageID = villageID.intern();
130 this.villageIDNum = Integer.parseInt(this.villageID);
131 this.villageName = villageName.intern();
133 this.isValid = this.parentLand.getLandDef()
134 .isValidVillageId(this.villageIDNum);
141 * 村同士を比較するためのComparatorを返す。
142 * @return Comparatorインスタンス
144 public static Comparator<Village> comparator(){
145 return VILLAGE_COMPARATOR;
149 * 人狼BBSサーバからPeriod一覧情報が含まれたHTMLを取得し、
152 * @throws java.io.IOException ネットワーク入出力の異常
154 public static synchronized void updateVillage(Village village)
156 Land land = village.getParentLand();
157 LandDef landDef = land.getLandDef();
158 LandState landState = landDef.getLandState();
159 ServerAccess server = land.getServerAccess();
162 if(landState == LandState.ACTIVE){
163 html = server.getHTMLBoneHead(village);
165 html = server.getHTMLVillage(village);
168 DecodedContent content = html.getContent();
169 HANDLER.setVillage(village);
171 PARSER.parseAutomatic(content);
172 }catch(HtmlParseException e){
173 LOGGER.log(Level.WARNING, "村の状態が不明", e);
181 * @return 村の所属する国(Land)
183 public Land getParentLand(){
184 return this.parentLand;
191 public String getVillageID(){
192 return this.villageID;
199 public int getVillageIDNum(){
200 return this.villageIDNum;
207 public String getVillageName(){
208 return this.parentLand.getLandDef().getLandPrefix() + getVillageID();
215 public String getVillageFullName(){
216 return this.villageName;
223 public VillageState getState(){
231 public void setState(VillageState state){
240 public Period getPrologue(){
241 for(Period period : this.periodList){
242 if(period.isPrologue()) return period;
251 public Period getEpilogue(){
252 for(Period period : this.periodList){
253 if(period.isEpilogue()) return period;
263 public Period getProgress(int day){
264 for(Period period : this.periodList){
265 if( period.isProgress()
266 && period.getDay() == day ) return period;
272 * PROGRESS状態のPeriodの総数を返す。
273 * @return PROGRESS状態のPeriod総数
275 public int getProgressDays(){
277 for(Period period : this.periodList){
278 if(period.isProgress()) result++;
284 * 指定されたPeriodインデックスのPeriodを返す。
285 * プロローグやエピローグへのアクセスも可能。
286 * @param day Periodインデックス
289 public Period getPeriod(int day){
290 return this.periodList.get(day);
294 * 指定されたアンカーの対象のPeriodを返す。
298 public Period getPeriod(Anchor anchor){
301 if(anchor.isEpilogueDay()){
302 anchorPeriod = getEpilogue();
306 int anchorDay = anchor.getDay();
307 anchorPeriod = getPeriod(anchorDay);
316 public int getPeriodSize(){
317 return this.periodList.size();
322 * @return Periodのリスト。
324 public List<Period> getPeriodList(){
325 return this.unmodList;
329 * 指定した名前で村に登録されているAvatarを返す。
330 * @param fullName Avatarの名前
333 public Avatar getAvatar(String fullName){
334 // TODO CharSequenceにできない?
337 avatar = Avatar.getPredefinedAvatar(fullName);
338 if( avatar != null ){
339 preloadAvatarFace(avatar);
343 avatar = this.avatarMap.get(fullName);
344 if( avatar != null ){
345 preloadAvatarFace(avatar);
353 * Avatarの顔画像を事前にロードする。
354 * @param avatar Avatar
356 private void preloadAvatarFace(Avatar avatar){
357 if(this.faceImageMap.get(avatar) != null) return;
359 Land land = getParentLand();
360 LandDef landDef = land.getLandDef();
362 String template = landDef.getFaceURITemplate();
363 int serialNo = avatar.getIdNum();
364 String uri = MessageFormat.format(template, serialNo);
366 BufferedImage image = land.downloadImage(uri);
367 if(image == null) image = GUIUtils.getNoImage();
369 this.faceImageMap.put(avatar, image);
376 * @param avatar Avatar
378 // 未知のAvatar出現時の処理が不完全
379 public void addAvatar(Avatar avatar){
380 if(avatar == null) return;
381 String fullName = avatar.getFullName();
382 this.avatarMap.put(fullName, avatar);
384 preloadAvatarFace(avatar);
390 * 村に登録されたAvatarの顔イメージを返す。
391 * @param avatar Avatar
394 // TODO 失敗したらプロローグを強制読み込みして再トライしたい
395 public BufferedImage getAvatarFaceImage(Avatar avatar){
396 return this.faceImageMap.get(avatar);
400 * 村に登録されたAvatarの全身像イメージを返す。
401 * @param avatar Avatar
404 public BufferedImage getAvatarBodyImage(Avatar avatar){
405 BufferedImage result;
406 result = this.bodyImageMap.get(avatar);
407 if(result != null) return result;
409 Land land = getParentLand();
410 LandDef landDef = land.getLandDef();
412 String template = landDef.getBodyURITemplate();
413 int serialNo = avatar.getIdNum();
414 String uri = MessageFormat.format(template, serialNo);
416 result = land.downloadImage(uri);
417 if(result == null) result = GUIUtils.getNoImage();
419 this.bodyImageMap.put(avatar, result);
425 * 村に登録されたAvatarのモノクロ顔イメージを返す。
426 * @param avatar Avatar
429 public BufferedImage getAvatarFaceMonoImage(Avatar avatar){
430 BufferedImage result;
431 result = this.faceMonoImageMap.get(avatar);
433 result = getAvatarFaceImage(avatar);
434 result = GUIUtils.createMonoImage(result);
435 this.faceMonoImageMap.put(avatar, result);
441 * 村に登録されたAvatarの全身像イメージを返す。
442 * @param avatar Avatar
445 public BufferedImage getAvatarBodyMonoImage(Avatar avatar){
446 BufferedImage result;
447 result = this.bodyMonoImageMap.get(avatar);
449 result = getAvatarBodyImage(avatar);
450 result = GUIUtils.createMonoImage(result);
451 this.bodyMonoImageMap.put(avatar, result);
460 public BufferedImage getGraveImage(){
461 BufferedImage result = getParentLand().getGraveIconImage();
466 * 国に登録された墓イメージ(大)を返す。
469 public BufferedImage getGraveBodyImage(){
470 BufferedImage result = getParentLand().getGraveBodyImage();
475 * 村にアクセスするためのCGIクエリーを返す。
478 public CharSequence getCGIQuery(){
479 StringBuilder result = new StringBuilder();
480 result.append("?vid=").append(getVillageID());
488 public int getLimitMonth(){
489 return this.limitMonth;
496 public int getLimitDay(){
497 return this.limitDay;
504 public int getLimitHour(){
505 return this.limitHour;
512 public int getLimitMinute(){
513 return this.limitMinute;
518 * @return 無効な村ならfalse
520 public boolean isValid(){
525 * Periodリストの指定したインデックスにPeriodを上書きする。
526 * リストのサイズと同じインデックスを指定する事が許される。
527 * その場合の動作はList.addと同じ。
528 * @param index Periodリストのインデックス。
529 * @param period 上書きするPeriod
530 * @throws java.lang.IndexOutOfBoundsException インデックスの指定がおかしい
532 private void setPeriod(int index, Period period)
533 throws IndexOutOfBoundsException{
534 int listSize = this.periodList.size();
535 if(index == listSize){
536 this.periodList.add(period);
537 }else if(index < listSize){
538 this.periodList.set(index, period);
540 throw new IndexOutOfBoundsException();
546 * アンカーに一致する会話(Talk)のリストを取得する。
549 * @throws java.io.IOException おそらくネットワークエラー
551 public List<Talk> getTalkListFromAnchor(Anchor anchor)
553 List<Talk> result = new LinkedList<Talk>();
556 if(anchor.hasTalkNo()){
557 // 事前に全Periodがロードされているのが前提
558 for(Period period : this.periodList){
559 Talk talk = period.getNumberedTalk(anchor.getTalkNo());
560 if(talk == null) continue;
566 Period anchorPeriod = getPeriod(anchor);
567 if(anchorPeriod == null) return result;
569 Period.parsePeriod(anchorPeriod, false);
571 for(Topic topic : anchorPeriod.getTopicList()){
572 if( ! (topic instanceof Talk) ) continue;
573 Talk talk = (Talk) topic;
574 if(talk.getHour() != anchor.getHour() ) continue;
575 if(talk.getMinute() != anchor.getMinute()) continue;
582 * 全Periodの発言データをアンロードする。
584 public void unloadPeriods(){
585 for(Period period : this.periodList){
594 * @param village {@inheritDoc}
595 * @return {@inheritDoc}
598 public int compareTo(Village village){
599 int cmpResult = VILLAGE_COMPARATOR.compare(this, village);
606 * @param obj {@inheritDoc}
607 * @return {@inheritDoc}
610 public boolean equals(Object obj){
611 if(obj == null) return false;
612 if( ! (obj instanceof Village) ) return false;
613 Village village = (Village) obj;
615 if( getParentLand() != village.getParentLand() ) return false;
617 int cmpResult = compareTo(village);
618 if(cmpResult == 0) return true;
624 * @return {@inheritDoc}
627 public int hashCode(){
628 int homeHash = getParentLand().hashCode();
629 int vidHash = getVillageID().hashCode();
630 int result = homeHash ^ vidHash;
641 public String toString(){
642 return getVillageFullName();
648 private static class VillageHeadHandler extends HtmlAdapter{
650 private Village village = null;
652 private boolean hasPrologue;
653 private boolean hasProgress;
654 private boolean hasEpilogue;
655 private boolean hasDone;
656 private int maxProgress;
661 public VillageHeadHandler(){
670 public void setVillage(Village village){
671 this.village = village;
679 this.hasPrologue = false;
680 this.hasProgress = false;
681 this.hasEpilogue = false;
682 this.hasDone = false;
683 this.maxProgress = 0;
691 public VillageState getVillageState(){
693 return VillageState.GAMEOVER;
694 }else if(this.hasEpilogue){
695 return VillageState.EPILOGUE;
696 }else if(this.hasProgress){
697 return VillageState.PROGRESS;
698 }else if(this.hasPrologue){
699 return VillageState.PROLOGUE;
702 return VillageState.UNKNOWN;
707 * @param content {@inheritDoc}
708 * @throws HtmlParseException {@inheritDoc}
711 public void startParse(DecodedContent content)
712 throws HtmlParseException{
719 * 自動判定の結果が日ページでなければ例外を投げる。
720 * @param type {@inheritDoc}
721 * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
724 public void pageType(PageType type) throws HtmlParseException{
725 if(type != PageType.PERIOD_PAGE){
726 throw new HtmlParseException(
734 * @param month {@inheritDoc}
735 * @param day {@inheritDoc}
736 * @param hour {@inheritDoc}
737 * @param minute {@inheritDoc}
738 * @throws HtmlParseException {@inheritDoc}
741 public void commitTime(int month, int day,
742 int hour, int minute)
743 throws HtmlParseException{
744 this.village.limitMonth = month;
745 this.village.limitDay = day;
746 this.village.limitHour = hour;
747 this.village.limitMinute = minute;
754 * @param content {@inheritDoc}
755 * @param anchorRange {@inheritDoc}
756 * @param periodType {@inheritDoc}
757 * @param day {@inheritDoc}
758 * @throws HtmlParseException {@inheritDoc}
761 public void periodLink(DecodedContent content,
762 SeqRange anchorRange,
763 PeriodType periodType,
765 throws HtmlParseException{
766 if(periodType == null){
773 this.hasPrologue = true;
776 this.hasProgress = true;
777 this.maxProgress = day;
780 this.hasEpilogue = true;
792 * @throws HtmlParseException {@inheritDoc}
795 public void endParse() throws HtmlParseException{
796 Land land = this.village.getParentLand();
797 LandDef landDef = land.getLandDef();
798 LandState landState = landDef.getLandState();
800 VillageState villageState = getVillageState();
801 if(villageState == VillageState.UNKNOWN){
802 this.village.setState(villageState);
803 this.village.periodList.clear();
804 LOGGER.warning("村の状況を読み取れません");
808 if(landState == LandState.ACTIVE){
809 this.village.setState(villageState);
811 this.village.setState(VillageState.GAMEOVER);
820 * 抽出したリンク情報に伴いPeriodリストを更新する。
821 * まだPeriodデータのロードは行われない。
822 * ゲーム進行中の村で更新時刻をまたいで更新が行われた場合、
823 * 既存のPeriodリストが伸張する場合がある。
825 private void modifyPeriodList(){
826 Period lastPeriod = null;
828 if(this.hasPrologue){
829 Period prologue = this.village.getPrologue();
830 if(prologue == null){
831 lastPeriod = new Period(this.village,
832 PeriodType.PROLOGUE, 0);
833 this.village.setPeriod(0, lastPeriod);
835 lastPeriod = prologue;
839 if(this.hasProgress){
840 for(int day = 1; day <= this.maxProgress; day++){
841 Period progress = this.village.getProgress(day);
842 if(progress == null){
843 lastPeriod = new Period(this.village,
844 PeriodType.PROGRESS, day);
845 this.village.setPeriod(day, lastPeriod);
847 lastPeriod = progress;
852 if(this.hasEpilogue){
853 Period epilogue = this.village.getEpilogue();
854 if(epilogue == null){
855 lastPeriod = new Period(this.village,
857 this.maxProgress +1);
858 this.village.setPeriod(this.maxProgress +1, lastPeriod);
860 lastPeriod = epilogue;
864 assert this.village.getPeriodSize() > 0;
865 assert lastPeriod != null;
868 // リロードで村が縮むわけないじゃん。みんな大げさだなあ
869 while(this.village.periodList.getLast() != lastPeriod){
870 this.village.periodList.removeLast();
873 if(this.village.getState() != VillageState.GAMEOVER){
874 lastPeriod.setHot(true);