OSDN Git Service

スタートアップ処理の改善
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / data / Village.java
1 /*
2  * Village
3  *
4  * License : The MIT License
5  * Copyright(c) 2008 olyutorskii
6  */
7
8 package jp.sfjp.jindolf.data;
9
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;
18 import java.util.Map;
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;
34
35 /**
36  * いわゆる「村」。
37  */
38 public class Village implements Comparable<Village> {
39
40     /**
41      * 村同士を比較するためのComparator。
42      */
43     private static class VillageComparator implements Comparator<Village> {
44
45         /**
46          * コンストラクタ。
47          */
48         public VillageComparator(){
49             super();
50             return;
51         }
52
53         /**
54          * {@inheritDoc}
55          * @param v1 {@inheritDoc}
56          * @param v2 {@inheritDoc}
57          * @return {@inheritDoc}
58          */
59         @Override
60         public int compare(Village v1, Village v2){
61             int v1Num;
62             if(v1 == null) v1Num = Integer.MIN_VALUE;
63             else           v1Num = v1.getVillageIDNum();
64
65             int v2Num;
66             if(v2 == null) v2Num = Integer.MIN_VALUE;
67             else           v2Num = v2.getVillageIDNum();
68
69             return v1Num - v2Num;
70         }
71
72     }
73
74     private static final Comparator<Village> VILLAGE_COMPARATOR =
75             new VillageComparator();
76
77     private static final HtmlParser PARSER = new HtmlParser();
78     private static final VillageHeadHandler HANDLER =
79             new VillageHeadHandler();
80
81     private static final Logger LOGGER = Logger.getAnonymousLogger();
82
83     static{
84         PARSER.setBasicHandler   (HANDLER);
85         PARSER.setSysEventHandler(HANDLER);
86         PARSER.setTalkHandler    (HANDLER);
87     }
88
89
90     private final Land parentLand;
91     private final String villageID;
92     private final int villageIDNum;
93     private final String villageName;
94
95     private final boolean isValid;
96
97     private int limitMonth;
98     private int limitDay;
99     private int limitHour;
100     private int limitMinute;
101
102     private VillageState state = VillageState.UNKNOWN;
103
104     private final LinkedList<Period> periodList = new LinkedList<Period>();
105     private final List<Period> unmodList =
106             Collections.unmodifiableList(this.periodList);
107
108     private final Map<String, Avatar> avatarMap =
109             new HashMap<String, Avatar>();
110
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>();
119
120
121     /**
122      * Villageを生成する。
123      * @param parentLand Villageの所属する国
124      * @param villageID 村のID
125      * @param villageName 村の名前
126      */
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();
132
133         this.isValid = this.parentLand.getLandDef()
134                        .isValidVillageId(this.villageIDNum);
135
136         return;
137     }
138
139
140     /**
141      * 村同士を比較するためのComparatorを返す。
142      * @return Comparatorインスタンス
143      */
144     public static Comparator<Village> comparator(){
145         return VILLAGE_COMPARATOR;
146     }
147
148     /**
149      * 人狼BBSサーバからPeriod一覧情報が含まれたHTMLを取得し、
150      * Periodリストを更新する。
151      * @param village 村
152      * @throws java.io.IOException ネットワーク入出力の異常
153      */
154     public static synchronized void updateVillage(Village village)
155             throws IOException{
156         Land land = village.getParentLand();
157         LandDef landDef = land.getLandDef();
158         LandState landState = landDef.getLandState();
159         ServerAccess server = land.getServerAccess();
160
161         HtmlSequence html;
162         if(landState == LandState.ACTIVE){
163             html = server.getHTMLBoneHead(village);
164         }else{
165             html = server.getHTMLVillage(village);
166         }
167
168         DecodedContent content = html.getContent();
169         HANDLER.setVillage(village);
170         try{
171             PARSER.parseAutomatic(content);
172         }catch(HtmlParseException e){
173             LOGGER.log(Level.WARNING, "村の状態が不明", e);
174         }
175
176         return;
177     }
178
179     /**
180      * 所属する国を返す。
181      * @return 村の所属する国(Land)
182      */
183     public Land getParentLand(){
184         return this.parentLand;
185     }
186
187     /**
188      * 村のID文字列を返す。
189      * @return 村ID
190      */
191     public String getVillageID(){
192         return this.villageID;
193     }
194
195     /**
196      * 村のID数値を返す。
197      * @return 村ID
198      */
199     public int getVillageIDNum(){
200         return this.villageIDNum;
201     }
202
203     /**
204      * 村の名前を返す。
205      * @return 村の名前
206      */
207     public String getVillageName(){
208         return this.parentLand.getLandDef().getLandPrefix() + getVillageID();
209     }
210
211     /**
212      * 村の長い名前を返す。
213      * @return 村の長い名前
214      */
215     public String getVillageFullName(){
216         return this.villageName;
217     }
218
219     /**
220      * 村の状態を返す。
221      * @return 村の状態
222      */
223     public VillageState getState(){
224         return this.state;
225     }
226
227     /**
228      * 村の状態を設定する。
229      * @param state 村の状態
230      */
231     public void setState(VillageState state){
232         this.state = state;
233         return;
234     }
235
236     /**
237      * プロローグを返す。
238      * @return プロローグ
239      */
240     public Period getPrologue(){
241         for(Period period : this.periodList){
242             if(period.isPrologue()) return period;
243         }
244         return null;
245     }
246
247     /**
248      * エピローグを返す。
249      * @return エピローグ
250      */
251     public Period getEpilogue(){
252         for(Period period : this.periodList){
253             if(period.isEpilogue()) return period;
254         }
255         return null;
256     }
257
258     /**
259      * 指定された日付の進行日を返す。
260      * @param day 日付
261      * @return Period
262      */
263     public Period getProgress(int day){
264         for(Period period : this.periodList){
265             if(   period.isProgress()
266                && period.getDay() == day ) return period;
267         }
268         return null;
269     }
270
271     /**
272      * PROGRESS状態のPeriodの総数を返す。
273      * @return PROGRESS状態のPeriod総数
274      */
275     public int getProgressDays(){
276         int result = 0;
277         for(Period period : this.periodList){
278             if(period.isProgress()) result++;
279         }
280         return result;
281     }
282
283     /**
284      * 指定されたPeriodインデックスのPeriodを返す。
285      * プロローグやエピローグへのアクセスも可能。
286      * @param day Periodインデックス
287      * @return Period
288      */
289     public Period getPeriod(int day){
290         return this.periodList.get(day);
291     }
292
293     /**
294      * 指定されたアンカーの対象のPeriodを返す。
295      * @param anchor アンカー
296      * @return Period
297      */
298     public Period getPeriod(Anchor anchor){
299         Period anchorPeriod;
300
301         if(anchor.isEpilogueDay()){
302             anchorPeriod = getEpilogue();
303             return anchorPeriod;
304         }
305
306         int anchorDay = anchor.getDay();
307         anchorPeriod = getPeriod(anchorDay);
308
309         return anchorPeriod;
310     }
311
312     /**
313      * Period総数を返す。
314      * @return Period総数
315      */
316     public int getPeriodSize(){
317         return this.periodList.size();
318     }
319
320     /**
321      * Periodへのリストを返す。
322      * @return Periodのリスト。
323      */
324     public List<Period> getPeriodList(){
325         return this.unmodList;
326     }
327
328     /**
329      * 指定した名前で村に登録されているAvatarを返す。
330      * @param fullName Avatarの名前
331      * @return Avatar
332      */
333     public Avatar getAvatar(String fullName){
334         // TODO CharSequenceにできない?
335         Avatar avatar;
336
337         avatar = Avatar.getPredefinedAvatar(fullName);
338         if( avatar != null ){
339             preloadAvatarFace(avatar);
340             return avatar;
341         }
342
343         avatar = this.avatarMap.get(fullName);
344         if( avatar != null ){
345             preloadAvatarFace(avatar);
346             return avatar;
347         }
348
349         return null;
350     }
351
352     /**
353      * Avatarの顔画像を事前にロードする。
354      * @param avatar Avatar
355      */
356     private void preloadAvatarFace(Avatar avatar){
357         if(this.faceImageMap.get(avatar) != null) return;
358
359         Land land = getParentLand();
360         LandDef landDef = land.getLandDef();
361
362         String template = landDef.getFaceURITemplate();
363         int serialNo = avatar.getIdNum();
364         String uri = MessageFormat.format(template, serialNo);
365
366         BufferedImage image = land.downloadImage(uri);
367         if(image == null) image = GUIUtils.getNoImage();
368
369         this.faceImageMap.put(avatar, image);
370
371         return;
372     }
373
374     /**
375      * Avatarを村に登録する。
376      * @param avatar Avatar
377      */
378     // 未知のAvatar出現時の処理が不完全
379     public void addAvatar(Avatar avatar){
380         if(avatar == null) return;
381         String fullName = avatar.getFullName();
382         this.avatarMap.put(fullName, avatar);
383
384         preloadAvatarFace(avatar);
385
386         return;
387     }
388
389     /**
390      * 村に登録されたAvatarの顔イメージを返す。
391      * @param avatar Avatar
392      * @return 顔イメージ
393      */
394     // TODO 失敗したらプロローグを強制読み込みして再トライしたい
395     public BufferedImage getAvatarFaceImage(Avatar avatar){
396         return this.faceImageMap.get(avatar);
397     }
398
399     /**
400      * 村に登録されたAvatarの全身像イメージを返す。
401      * @param avatar Avatar
402      * @return 全身イメージ
403      */
404     public BufferedImage getAvatarBodyImage(Avatar avatar){
405         BufferedImage result;
406         result = this.bodyImageMap.get(avatar);
407         if(result != null) return result;
408
409         Land land = getParentLand();
410         LandDef landDef = land.getLandDef();
411
412         String template = landDef.getBodyURITemplate();
413         int serialNo = avatar.getIdNum();
414         String uri = MessageFormat.format(template, serialNo);
415
416         result = land.downloadImage(uri);
417         if(result == null) result = GUIUtils.getNoImage();
418
419         this.bodyImageMap.put(avatar, result);
420
421         return result;
422     }
423
424     /**
425      * 村に登録されたAvatarのモノクロ顔イメージを返す。
426      * @param avatar Avatar
427      * @return 顔イメージ
428      */
429     public BufferedImage getAvatarFaceMonoImage(Avatar avatar){
430         BufferedImage result;
431         result = this.faceMonoImageMap.get(avatar);
432         if(result == null){
433             result = getAvatarFaceImage(avatar);
434             result = GUIUtils.createMonoImage(result);
435             this.faceMonoImageMap.put(avatar, result);
436         }
437         return result;
438     }
439
440     /**
441      * 村に登録されたAvatarの全身像イメージを返す。
442      * @param avatar Avatar
443      * @return 全身イメージ
444      */
445     public BufferedImage getAvatarBodyMonoImage(Avatar avatar){
446         BufferedImage result;
447         result = this.bodyMonoImageMap.get(avatar);
448         if(result == null){
449             result = getAvatarBodyImage(avatar);
450             result = GUIUtils.createMonoImage(result);
451             this.bodyMonoImageMap.put(avatar, result);
452         }
453         return result;
454     }
455
456     /**
457      * 国に登録された墓イメージを返す。
458      * @return 墓イメージ
459      */
460     public BufferedImage getGraveImage(){
461         BufferedImage result = getParentLand().getGraveIconImage();
462         return result;
463     }
464
465     /**
466      * 国に登録された墓イメージ(大)を返す。
467      * @return 墓イメージ(大)
468      */
469     public BufferedImage getGraveBodyImage(){
470         BufferedImage result = getParentLand().getGraveBodyImage();
471         return result;
472     }
473
474     /**
475      * 村にアクセスするためのCGIクエリーを返す。
476      * @return CGIクエリー
477      */
478     public CharSequence getCGIQuery(){
479         StringBuilder result = new StringBuilder();
480         result.append("?vid=").append(getVillageID());
481         return result;
482     }
483
484     /**
485      * 次回更新月を返す。
486      * @return 更新月(1-12)
487      */
488     public int getLimitMonth(){
489         return this.limitMonth;
490     }
491
492     /**
493      * 次回更新日を返す。
494      * @return 更新日(1-31)
495      */
496     public int getLimitDay(){
497         return this.limitDay;
498     }
499
500     /**
501      * 次回更新時を返す。
502      * @return 更新時(0-23)
503      */
504     public int getLimitHour(){
505         return this.limitHour;
506     }
507
508     /**
509      * 次回更新分を返す。
510      * @return 更新分(0-59)
511      */
512     public int getLimitMinute(){
513         return this.limitMinute;
514     }
515
516     /**
517      * 有効な村か否か判定する。
518      * @return 無効な村ならfalse
519      */
520     public boolean isValid(){
521         return this.isValid;
522     }
523
524     /**
525      * Periodリストの指定したインデックスにPeriodを上書きする。
526      * リストのサイズと同じインデックスを指定する事が許される。
527      * その場合の動作はList.addと同じ。
528      * @param index Periodリストのインデックス。
529      * @param period 上書きするPeriod
530      * @throws java.lang.IndexOutOfBoundsException インデックスの指定がおかしい
531      */
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);
539         }else{
540             throw new IndexOutOfBoundsException();
541         }
542         return;
543     }
544
545     /**
546      * アンカーに一致する会話(Talk)のリストを取得する。
547      * @param anchor アンカー
548      * @return Talkのリスト
549      * @throws java.io.IOException おそらくネットワークエラー
550      */
551     public List<Talk> getTalkListFromAnchor(Anchor anchor)
552             throws IOException{
553         List<Talk> result = new LinkedList<Talk>();
554
555         /* G国アンカー対応 */
556         if(anchor.hasTalkNo()){
557             // 事前に全Periodがロードされているのが前提
558             for(Period period : this.periodList){
559                 Talk talk = period.getNumberedTalk(anchor.getTalkNo());
560                 if(talk == null) continue;
561                 result.add(talk);
562             }
563             return result;
564         }
565
566         Period anchorPeriod = getPeriod(anchor);
567         if(anchorPeriod == null) return result;
568
569         Period.parsePeriod(anchorPeriod, false);
570
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;
576             result.add(talk);
577         }
578         return result;
579     }
580
581     /**
582      * 全Periodの発言データをアンロードする。
583      */
584     public void unloadPeriods(){
585         for(Period period : this.periodList){
586             period.unload();
587         }
588         return;
589     }
590
591     /**
592      * {@inheritDoc}
593      * 二つの村を順序付ける。
594      * @param village {@inheritDoc}
595      * @return {@inheritDoc}
596      */
597     @Override
598     public int compareTo(Village village){
599         int cmpResult = VILLAGE_COMPARATOR.compare(this, village);
600         return cmpResult;
601     }
602
603     /**
604      * {@inheritDoc}
605      * 二つの村が等しいか調べる。
606      * @param obj {@inheritDoc}
607      * @return {@inheritDoc}
608      */
609     @Override
610     public boolean equals(Object obj){
611         if(obj == null) return false;
612         if( ! (obj instanceof Village) ) return false;
613         Village village = (Village) obj;
614
615         if( getParentLand() != village.getParentLand() ) return false;
616
617         int cmpResult = compareTo(village);
618         if(cmpResult == 0) return true;
619         return false;
620     }
621
622     /**
623      * {@inheritDoc}
624      * @return {@inheritDoc}
625      */
626     @Override
627     public int hashCode(){
628         int homeHash = getParentLand().hashCode();
629         int vidHash = getVillageID().hashCode();
630         int result = homeHash ^ vidHash;
631         return result;
632     }
633
634     /**
635      * {@inheritDoc}
636      * 村の文字列表現を返す。
637      * 村の名前と等しい。
638      * @return 村の名前
639      */
640     @Override
641     public String toString(){
642         return getVillageFullName();
643     }
644
645     /**
646      * Period一覧取得用ハンドラ。
647      */
648     private static class VillageHeadHandler extends HtmlAdapter{
649
650         private Village village = null;
651
652         private boolean hasPrologue;
653         private boolean hasProgress;
654         private boolean hasEpilogue;
655         private boolean hasDone;
656         private int maxProgress;
657
658         /**
659          * コンストラクタ。
660          */
661         public VillageHeadHandler(){
662             super();
663             return;
664         }
665
666         /**
667          * 更新対象の村を設定する。
668          * @param village 村
669          */
670         public void setVillage(Village village){
671             this.village = village;
672             return;
673         }
674
675         /**
676          * リセットを行う。
677          */
678         public void reset(){
679             this.hasPrologue = false;
680             this.hasProgress = false;
681             this.hasEpilogue = false;
682             this.hasDone = false;
683             this.maxProgress = 0;
684             return;
685         }
686
687         /**
688          * パース結果から村の状態を算出する。
689          * @return 村の状態
690          */
691         public VillageState getVillageState(){
692             if(this.hasDone){
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;
700             }
701
702             return VillageState.UNKNOWN;
703         }
704
705         /**
706          * {@inheritDoc}
707          * @param content {@inheritDoc}
708          * @throws HtmlParseException {@inheritDoc}
709          */
710         @Override
711         public void startParse(DecodedContent content)
712                 throws HtmlParseException{
713             reset();
714             return;
715         }
716
717         /**
718          * {@inheritDoc}
719          * 自動判定の結果が日ページでなければ例外を投げる。
720          * @param type {@inheritDoc}
721          * @throws HtmlParseException {@inheritDoc} 意図しないページが来た。
722          */
723         @Override
724         public void pageType(PageType type) throws HtmlParseException{
725             if(type != PageType.PERIOD_PAGE){
726                 throw new HtmlParseException(
727                         "日ページが必要です。");
728             }
729             return;
730         }
731
732         /**
733          * {@inheritDoc}
734          * @param month {@inheritDoc}
735          * @param day {@inheritDoc}
736          * @param hour {@inheritDoc}
737          * @param minute {@inheritDoc}
738          * @throws HtmlParseException {@inheritDoc}
739          */
740         @Override
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;
748
749             return;
750         }
751
752         /**
753          * {@inheritDoc}
754          * @param content {@inheritDoc}
755          * @param anchorRange {@inheritDoc}
756          * @param periodType {@inheritDoc}
757          * @param day {@inheritDoc}
758          * @throws HtmlParseException {@inheritDoc}
759          */
760         @Override
761         public void periodLink(DecodedContent content,
762                                 SeqRange anchorRange,
763                                 PeriodType periodType,
764                                 int day)
765                 throws HtmlParseException{
766             if(periodType == null){
767                 this.hasDone = true;
768                 return;
769             }
770
771             switch(periodType){
772             case PROLOGUE:
773                 this.hasPrologue = true;
774                 break;
775             case PROGRESS:
776                 this.hasProgress = true;
777                 this.maxProgress = day;
778                 break;
779             case EPILOGUE:
780                 this.hasEpilogue = true;
781                 break;
782             default:
783                 assert false;
784                 break;
785             }
786
787             return;
788         }
789
790         /**
791          * {@inheritDoc}
792          * @throws HtmlParseException {@inheritDoc}
793          */
794         @Override
795         public void endParse() throws HtmlParseException{
796             Land land = this.village.getParentLand();
797             LandDef landDef = land.getLandDef();
798             LandState landState = landDef.getLandState();
799
800             VillageState villageState = getVillageState();
801             if(villageState == VillageState.UNKNOWN){
802                 this.village.setState(villageState);
803                 this.village.periodList.clear();
804                 LOGGER.warning("村の状況を読み取れません");
805                 return;
806             }
807
808             if(landState == LandState.ACTIVE){
809                 this.village.setState(villageState);
810             }else{
811                 this.village.setState(VillageState.GAMEOVER);
812             }
813
814             modifyPeriodList();
815
816             return;
817         }
818
819         /**
820          * 抽出したリンク情報に伴いPeriodリストを更新する。
821          * まだPeriodデータのロードは行われない。
822          * ゲーム進行中の村で更新時刻をまたいで更新が行われた場合、
823          * 既存のPeriodリストが伸張する場合がある。
824          */
825         private void modifyPeriodList(){
826             Period lastPeriod = null;
827
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);
834                 }else{
835                     lastPeriod = prologue;
836                 }
837             }
838
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);
846                     }else{
847                         lastPeriod = progress;
848                     }
849                 }
850             }
851
852             if(this.hasEpilogue){
853                 Period epilogue = this.village.getEpilogue();
854                 if(epilogue == null){
855                     lastPeriod = new Period(this.village,
856                                             PeriodType.EPILOGUE,
857                                             this.maxProgress +1);
858                     this.village.setPeriod(this.maxProgress +1, lastPeriod);
859                 }else{
860                     lastPeriod = epilogue;
861                 }
862             }
863
864             assert this.village.getPeriodSize() > 0;
865             assert lastPeriod != null;
866
867             // 念のためチョップ。
868             // リロードで村が縮むわけないじゃん。みんな大げさだなあ
869             while(this.village.periodList.getLast() != lastPeriod){
870                 this.village.periodList.removeLast();
871             }
872
873             if(this.village.getState() != VillageState.GAMEOVER){
874                 lastPeriod.setHot(true);
875             }
876
877             return;
878         }
879
880     }
881
882 }