OSDN Git Service

キャスト表Wiki出力の見栄えを改善
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / dxchg / WolfBBS.java
1 /*
2  * WolfBBS
3  *
4  * License : The MIT License
5  * Copyright(c) 2009 olyutorskii
6  */
7
8 package jp.sfjp.jindolf.dxchg;
9
10 import java.awt.Color;
11 import java.io.FileNotFoundException;
12 import java.io.IOException;
13 import java.nio.ByteBuffer;
14 import java.nio.CharBuffer;
15 import java.nio.charset.CharacterCodingException;
16 import java.nio.charset.Charset;
17 import java.nio.charset.CharsetEncoder;
18 import java.util.Collections;
19 import java.util.LinkedList;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.Properties;
23 import java.util.Set;
24 import java.util.SortedSet;
25 import java.util.TreeSet;
26 import java.util.logging.Logger;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 import jp.sfjp.jindolf.ResourceManager;
30 import jp.sfjp.jindolf.data.Avatar;
31 import jp.sfjp.jindolf.data.Village;
32 import jp.sourceforge.jindolf.corelib.Destiny;
33 import jp.sourceforge.jindolf.corelib.GameRole;
34
35 /**
36  * まちゅ氏運営のまとめサイト(wolfbbs)に関する諸々。
37  *
38  * PukiWikiベース。
39  *
40  * @see <a href="https://wolfbbs.jp/">まとめサイト</a>
41  * @see <a href="https://pukiwiki.osdn.jp/">PukiWiki</a>
42  */
43 public final class WolfBBS{
44
45     /** PukiWikiコメント行。 */
46     public static final String COMMENTLINE;
47
48     private static final String WIKICHAR = "#&[]()<>+-*:|~/,'%?";
49     private static final Pattern WIKINAME_PATTERN =
50             Pattern.compile("[A-Z][a-z]+([A-Z])[a-z]+");
51
52     private static final String FACEICONSET =
53             "resources/wolfbbs/faceIconSet.properties";
54     private static final String ORDER_PREFIX = "iconset.order.";
55     private static final List<FaceIconSet> FACEICONSET_LIST =
56             new LinkedList<>();
57
58     private static final Charset CHARSET_EUC = Charset.forName("EUC-JP");
59
60     private static final String WOLFBBS_URL = "http://wolfbbs.jp/";
61
62     private static final Color COLOR_INNOCENT = new Color(0xb7bad3);
63     private static final Color COLOR_WOLF     = new Color(0xe0b8b8);
64     private static final Color COLOR_HAMSTER  = new Color(0xb9d0be);
65     private static final Color COLOR_DEAD     = new Color(0xaaaaaa);
66     private static final Color COLOR_ALIVE    = new Color(0xffffff);
67
68     private static final Logger LOGGER = Logger.getAnonymousLogger();
69
70     static{
71         try{
72             loadFaceIconSet();
73         }catch(FileNotFoundException e){
74             throw new ExceptionInInitializerError(e);
75         }
76
77         StringBuilder wikicomment = new StringBuilder();
78         wikicomment.append("// ");
79         while(wikicomment.length() < 72){
80             wikicomment.append('=');
81         }
82         wikicomment.append('\n');
83         COMMENTLINE = wikicomment.toString();
84     }
85
86
87     /**
88      * 隠しコンストラクタ。
89      */
90     private WolfBBS(){
91         assert false;
92         throw new AssertionError();
93     }
94
95
96     /**
97      * アイコンセットのロード。
98      *
99      * @throws FileNotFoundException リソースが不明
100      */
101     private static void loadFaceIconSet() throws FileNotFoundException {
102         Properties properties = ResourceManager.getProperties(FACEICONSET);
103         if(properties == null){
104             LOGGER.severe("顔アイコンセットの読み込みに失敗しました");
105             throw new FileNotFoundException();
106         }
107
108         loadFaceIconSet(properties);
109
110         return;
111     }
112
113     /**
114      * アイコンセットのロード。
115      *
116      * @param properties プロパティ
117      * @throws FileNotFoundException リソースが不明
118      */
119     private static void loadFaceIconSet(Properties properties)
120             throws FileNotFoundException {
121         String codeCheck = properties.getProperty("codeCheck");
122         if(    codeCheck == null
123             || codeCheck.length() != 1
124             || codeCheck.charAt(0) != '\u72fc'){  // 「狼」
125             LOGGER.severe(
126                     "顔アイコンセットプロパティファイルの"
127                     +"文字コードがおかしいようです。"
128                     +"native2ascii は正しく適用しましたか?");
129             throw new FileNotFoundException();
130         }
131
132         Set<Object> keySet = properties.keySet();
133
134         SortedSet<Integer> orderSet = new TreeSet<>();
135         for(Object keyObj : keySet){
136             if(keyObj == null) continue;
137             String key = keyObj.toString();
138             if( ! key.startsWith(ORDER_PREFIX) ) continue;
139             key = key.replace(ORDER_PREFIX, "");
140             Integer order;
141             try{
142                 order = Integer.valueOf(key);
143             }catch(NumberFormatException e){
144                 continue;
145             }
146             orderSet.add(order);
147         }
148
149         for(Integer orderNum : orderSet){
150             String setName = properties.getProperty(ORDER_PREFIX + orderNum);
151             FaceIconSet iconSet = loadFaceIconSet(properties, setName);
152             FACEICONSET_LIST.add(iconSet);
153         }
154
155         return;
156     }
157
158     /**
159      * アイコンセットのロード。
160      *
161      * @param properties プロパティ
162      * @param setName アイコンセット名
163      * @return アイコンセット
164      */
165     private static FaceIconSet loadFaceIconSet(Properties properties,
166                                           String setName){
167         String author  = properties.getProperty(setName + ".author");
168         String caption = properties.getProperty(setName + ".caption");
169         String urlText = properties.getProperty(setName + ".url");
170
171         FaceIconSet iconSet = new FaceIconSet(caption, author, urlText);
172
173         List<Avatar> avatarList = Avatar.getPredefinedAvatarList();
174         for(Avatar avatar : avatarList){
175             String identifier = avatar.getIdentifier();
176             String key = setName + ".iconWiki." + identifier;
177             String wiki = properties.getProperty(key);
178             iconSet.registIconWiki(avatar, wiki);
179         }
180
181         return iconSet;
182     }
183
184     /**
185      * 顔アイコンセットのリストを取得する。
186      *
187      * @return 顔アイコンセットのリスト
188      */
189     public static List<FaceIconSet> getFaceIconSetList(){
190         List<FaceIconSet> result =
191                 Collections.unmodifiableList(FACEICONSET_LIST);
192         return result;
193     }
194
195     /**
196      * 任意の文字がWikiの特殊キャラクタか否か判定する。
197      *
198      * @param ch 文字
199      * @return 特殊キャラクタならtrue
200      */
201     public static boolean isWikiChar(char ch){
202         if(WIKICHAR.indexOf(ch) < 0) return false;
203         return true;
204     }
205
206     /**
207      * Wiki特殊文字を数値参照文字でエスケープする。
208      *
209      * @param seq Wiki特殊文字を含むかもしれない文字列。
210      * @return エスケープされた文字列
211      */
212     public static CharSequence escapeWikiChar(CharSequence seq){
213         StringBuilder result = new StringBuilder();
214
215         int seqLength = seq.length();
216         for(int pos = 0; pos < seqLength; pos++){
217             char ch = seq.charAt(pos);
218             if(isWikiChar(ch)){
219                 try{
220                     appendNumCharRef(result, ch);
221                 }catch(IOException e){
222                     assert false;
223                     return null;
224                 }
225             }else{
226                 result.append(ch);
227             }
228         }
229
230         return result;
231     }
232
233     /**
234      * WikiNameを数値参照文字でエスケープする。
235      *
236      * @param seq WikiNameを含むかもしれない文字列
237      * @return エスケープされた文字列。
238      * @see <a href="https://pukiwiki.osdn.jp/?WikiName">WikiName</a>
239      */
240     public static CharSequence escapeWikiName(CharSequence seq){
241         StringBuilder result = null;
242         Matcher matcher = WIKINAME_PATTERN.matcher(seq);
243
244         int pos = 0;
245         while(matcher.find(pos)){
246             int matchStart = matcher.start();
247             int matchEnd   = matcher.end();
248             int capStart = matcher.start(1);
249             int capEnd   = matcher.end(1);
250
251             if(result == null) result = new StringBuilder();
252             result.append(seq, pos, matchStart);
253             result.append(seq, matchStart, capStart);
254             try{
255                 appendNumCharRef(result, seq.charAt(capStart));
256             }catch(IOException e){
257                 assert false;
258                 return null;
259             }
260             result.append(seq, capEnd, matchEnd);
261
262             pos = matchEnd;
263         }
264
265         if(pos == 0 || result == null) return seq;
266
267         result.append(seq, pos, seq.length());
268
269         return result;
270     }
271
272     /**
273      * 任意の文字列をWiki表記へ変換する。
274      *
275      * @param seq 任意の文字列
276      * @return Wiki用表記
277      */
278     public static CharSequence escapeWikiSyntax(CharSequence seq){
279         CharSequence result = seq;
280         result = escapeWikiChar(result);   // この順番は大事
281         result = escapeWikiName(result);
282         // TODO さらにURLとメールアドレスのエスケープも
283         return result;
284     }
285
286     /**
287      * ブラケットに入れる文字をエスケープする。
288      *
289      * @param seq 文字列。
290      * @return エスケープされた文字列
291      */
292     public static CharSequence escapeWikiBracket(CharSequence seq){
293         StringBuilder result = new StringBuilder();
294
295         int seqLength = seq.length();
296         for(int pos = 0; pos < seqLength; pos++){
297             char ch = seq.charAt(pos);
298
299             switch(ch){
300             case '#': ch = '#'; break;
301             case '&': ch = '&'; break;
302             case '[': ch = '['; break;
303             case ']': ch = ']'; break;
304             case '<': ch = '<'; break;
305             case '>': ch = '>'; break;
306             default: break;
307             }
308
309             result.append(ch);
310         }
311
312         int resultLength;
313
314         while(result.length() > 0 && result.charAt(0) == '/'){
315             result.deleteCharAt(0);
316         }
317
318         resultLength = result.length();
319         for(int pos = resultLength - 1; pos >= 0; pos--){
320             char ch = result.charAt(pos);
321             if(ch != '/') break;
322             result.deleteCharAt(pos);
323         }
324
325         resultLength = result.length();
326         for(int pos = 1; pos < resultLength - 1; pos++){
327             char ch = result.charAt(pos);
328             if(ch == ':'){
329                 result.setCharAt(pos, ':');
330             }
331         }
332
333         resultLength = result.length();
334         if(resultLength == 1 && result.charAt(0) == ':'){
335             result.setCharAt(0, ':');
336         }
337
338         return result;
339     }
340
341     /**
342      * 数値参照文字に変換された文字を追加する。
343      *
344      * 例){@literal 'D' => "&#x44;}"
345      *
346      * @param app 追加対象
347      * @param ch 1文字
348      * @return 引数と同じ
349      * @throws java.io.IOException 入出力エラー。文字列の場合はありえない。
350      */
351     public static Appendable appendNumCharRef(Appendable app, char ch)
352             throws IOException{
353         app.append("&#x");
354
355         int ival = ch;
356         String hex = Integer.toHexString(ival);
357         app.append(hex);
358
359         app.append(';');
360
361         return app;
362     }
363
364     /**
365      * 任意の文字を数値参照文字列に変換する。
366      *
367      * 例){@literal 'D' => "&#x44;"}
368      *
369      * @param ch 文字
370      * @return 変換後の文字列
371      */
372     public static CharSequence toNumCharRef(char ch){
373         StringBuilder result = new StringBuilder(8);
374         try{
375             appendNumCharRef(result, ch);
376         }catch(IOException e){
377             assert false;
378             return null;
379         }
380         return result;
381     }
382
383     /**
384      * ColorのRGB各成分をWikiカラー表記に変換する。
385      *
386      * α成分は無視される。
387      *
388      * @param color 色
389      * @return Wikiカラー表記
390      */
391     public static String cnvWikiColor(Color color){
392         int packRGB = color.getRGB();
393
394         String txtRGB = Integer.toHexString(packRGB);
395         String leadRGB = "00000000" + txtRGB;
396         int chopLen = leadRGB.length() - 6;
397         String fixed = leadRGB.substring(chopLen);
398         String result = "#" + fixed;
399
400         return result;
401     }
402
403     /**
404      * 表の偶数行に色の変化を付ける。
405      *
406      * @param color 色
407      * @return 変化した色
408      */
409     public static Color evenColor(Color color){
410         int red   = color.getRed();
411         int green = color.getGreen();
412         int blue  = color.getBlue();
413
414         float[] hsb = Color.RGBtoHSB(red, green, blue, null);
415         float h = hsb[0];
416         float s = hsb[1];
417         float b = hsb[2];
418
419         if(b < 0.5){
420             b += 0.03;
421         }else{
422             b -= 0.03;
423         }
424
425         Color result = Color.getHSBColor(h, s, b);
426
427         return result;
428     }
429
430     /**
431      * 陣営の色を返す。
432      *
433      * @param role 役職
434      * @return 色
435      */
436     public static Color getTeamColor(GameRole role){
437         Color result;
438
439         switch(role){
440         case INNOCENT:
441         case SEER:
442         case SHAMAN:
443         case HUNTER:
444         case FRATER:
445             result = COLOR_INNOCENT;
446             break;
447         case WOLF:
448         case MADMAN:
449             result = COLOR_WOLF;
450             break;
451         case HAMSTER:
452             result = COLOR_HAMSTER;
453             break;
454         default:
455             assert false;
456             return null;
457         }
458
459         return result;
460     }
461
462     /**
463      * 各役職のアイコンWikiを返す。
464      *
465      * @param role 役職
466      * @return アイコンWiki
467      */
468     public static String getRoleIconWiki(GameRole role){
469         String result;
470
471         switch(role){
472         case INNOCENT:
473             result = "&char(村人,nolink);";
474             break;
475         case WOLF:
476             result = "&char(人狼,nolink);";
477             break;
478         case SEER:
479             result = "&char(占い師,nolink);";
480             break;
481         case SHAMAN:
482             result = "&char(霊能者,nolink);";
483             break;
484         case MADMAN:
485             result = "&char(狂人,nolink);";
486             break;
487         case HUNTER:
488             result = "&char(狩人,nolink);";
489             break;
490         case FRATER:
491             result = "&char(共有者,nolink);";
492             break;
493         case HAMSTER:
494             result = "&char(ハムスター人間,nolink);";
495             break;
496         default:
497             assert false;
498             result = "";
499             break;
500         }
501
502         return result;
503     }
504
505     /**
506      * 運命に対応する色を返す。
507      *
508      * @param destiny 運命
509      * @return 色
510      */
511     public static Color getDestinyColor(Destiny destiny){
512         Color result;
513         if(destiny == Destiny.ALIVE) result = COLOR_ALIVE;
514         else                         result = COLOR_DEAD;
515         return result;
516     }
517
518     /**
519      * そのまままとめサイトパス名に使えそうなシンプルな文字か判定する。
520      *
521      * @param ch 文字
522      * @return まとめサイトパス名に使えそうならtrue
523      */
524     private static boolean isSimpleIdToken(char ch){
525         if('0' <= ch && ch <= '9') return true;
526         if('A' <= ch && ch <= 'Z') return true;
527         if('a' <= ch && ch <= 'z') return true;
528         if(ch == '-' || ch == '_') return true;
529         return false;
530     }
531
532     /**
533      * プレイヤーIDを構成する文字からパス名を組み立てる。
534      *
535      * @param seq パス名
536      * @param ch 文字
537      * @return 引数と同じもの
538      */
539     private static StringBuilder encodeId(StringBuilder seq, char ch){
540         if(isSimpleIdToken(ch)){
541             seq.append(ch);
542             return seq;
543         }
544
545         CharBuffer cbuf = CharBuffer.allocate(1);
546         cbuf.append(ch);
547         cbuf.rewind();
548
549         CharsetEncoder encoder = CHARSET_EUC.newEncoder();
550         ByteBuffer bytebuf;
551         try{
552             bytebuf = encoder.encode(cbuf);
553         }catch(CharacterCodingException e){
554             seq.append('X');
555             return seq;
556         }
557
558         int limit = bytebuf.limit();
559         while(bytebuf.position() < limit){
560             int iVal = bytebuf.get();
561             if(iVal < 0) iVal += 0x0100;
562             String hex = Integer.toHexString(iVal).toUpperCase(Locale.JAPAN);
563             seq.append('%');
564             if(hex.length() < 2) seq.append('0');
565             seq.append(hex);
566         }
567
568         return seq;
569     }
570
571     /**
572      * プレイヤーIDからパス名の一部を予測する。
573      *
574      * @param id プレイヤーID
575      * @return .htmlを抜いたパス名
576      */
577     private static StringBuilder encodeId(CharSequence id){
578         StringBuilder result = new StringBuilder();
579         int length = id.length();
580         for(int pt = 0; pt < length; pt++){
581             char ch = id.charAt(pt);
582             encodeId(result, ch);
583         }
584         return result;
585     }
586
587     /**
588      * プレイヤーIDからまとめサイト上の個人ページを推測する。
589      *
590      * @param id プレイヤーID
591      * @return 個人ページURL文字列
592      */
593     public static String encodeURLFromId(CharSequence id){
594         CharSequence encodedId = encodeId(id);
595
596         String result = WOLFBBS_URL + encodedId + ".html";
597
598         return result;
599     }
600
601     /**
602      * キャスト紹介ジェネレータ出力のURLを得る。
603      *
604      * @param village 村
605      * @return ジェネレータ出力URL
606      */
607     public static String getCastGeneratorUrl(Village village){
608         String villageName = village.getVillageName();
609
610         StringBuilder txt = new StringBuilder();
611         txt.append(WOLFBBS_URL);
612         txt.append(villageName);
613         txt.append("%C2%BC.html");
614
615         String result = txt.toString();
616         return result;
617     }
618
619 }