1 /***************************************************************************
2  *                   (C) Copyright 2003-2013 - Stendhal                    *
3  ***************************************************************************
4  ***************************************************************************
5  *                                                                         *
6  *   This program is free software; you can redistribute it and/or modify  *
7  *   it under the terms of the GNU General Public License as published by  *
8  *   the Free Software Foundation; either version 2 of the License, or     *
9  *   (at your option) any later version.                                   *
10  *                                                                         *
11  ***************************************************************************/
12 package games.stendhal.server.script;
13 
14 import java.util.Arrays;
15 import java.util.LinkedList;
16 import java.util.List;
17 
18 import org.apache.log4j.Logger;
19 
20 import games.stendhal.common.Direction;
21 import games.stendhal.common.grammar.Grammar;
22 import games.stendhal.common.parser.Sentence;
23 import games.stendhal.server.core.engine.SingletonRepository;
24 import games.stendhal.server.core.engine.StendhalRPZone;
25 import games.stendhal.server.core.events.TurnListener;
26 import games.stendhal.server.core.events.TurnNotifier;
27 import games.stendhal.server.core.scripting.ScriptImpl;
28 import games.stendhal.server.core.scripting.ScriptingSandbox;
29 import games.stendhal.server.entity.npc.ChatAction;
30 import games.stendhal.server.entity.npc.ConversationStates;
31 import games.stendhal.server.entity.npc.EventRaiser;
32 import games.stendhal.server.entity.npc.SpeakerNPC;
33 import games.stendhal.server.entity.npc.condition.AdminCondition;
34 import games.stendhal.server.entity.npc.condition.NotCondition;
35 import games.stendhal.server.entity.player.Player;
36 
37 
38 /**
39  * A herald which will tell news to citizens.
40  *
41  * @author yoriy
42  */
43 public class Herald extends ScriptImpl {
44 
45 	// TODO: there is ability of using list of herald names,
46 	// it will add to game more fun.
47     public final String HeraldName = "Patrick";
48 
49     // after some thinking, i decided to not implement here
50 	// news records to file.
51 	private final Logger logger = Logger.getLogger(Herald.class);
52     private final int REQUIRED_ADMINLEVEL_INFO = 100;
53     private final int REQUIRED_ADMINLEVEL_SET = 1000;
54     private final TurnNotifier turnNotifier = TurnNotifier.get();
55 
56     //private final String HaveNoTime = "Hi, I have to do my job, so I have no time to speak with you, sorry.";
57     private final String HiOldFriend = "Oh, you're here! Hi, my old friend, glad to see you.";
58     private final String TooScared = "Oh, you are crazy, sure. I can't help you, the Emperor will kill us both for that.";
59     private final String BadJoke = "Joke, yes? I like jokes, but not too much.";
60     private final String FeelBad = "Oh, I don't know what is wrong with me, I'm not feeling very well... sorry, I can't help you...";
61     private final String DontUnderstand = "Sorry, I don't understand you";
62     private final String InfoOnly = "Oh, I think I can trust you enough to tell you my current announcements list. ";
63     private final String WillHelp = "Sure, I will do for you all that you want."+
64 									" Tell me '#speech <time interval (seconds)> <time limit (seconds)> <text to speech>'. " +
65 									"If you want to remove one of my current announcements, "+
66 									"tell me '#remove <number of speech>'. "+
67 									"You can also ask me about current announcements, say '#info' for that.";
68 
69     private final LinkedList<HeraldNews> heraldNews = new LinkedList<HeraldNews>();
70 
71     /**
72      * class for herald announcements.
73      */
74     private final static class HeraldNews {
75 
76 		private final String news;
77 		private final int interval;
78 		private final int limit;
79 		private int counter;
80 		private final int id;
81 		private final HeraldListener tnl;
getNews()82 		public String getNews(){
83 			return(news);
84 		}
getInterval()85 		public int getInterval(){
86 			return(interval);
87 		}
getLimit()88 		public int getLimit(){
89 			return(limit);
90 		}
getCounter()91 		public int getCounter(){
92 			return(counter);
93 		}
getid()94 		public int getid(){
95 			return(id);
96 		}
getTNL()97 		public HeraldListener getTNL(){
98 			return(tnl);
99 		}
setCounter(int count)100 		public void setCounter(int count){
101 			this.counter=count;
102 		}
103 
104 		/**
105 		 * constructor for news
106 		 * @param news - text to speech
107 		 * @param interval - interval between speeches in seconds.
108 		 * @param limit - time limit in seconds.
109 		 * @param counter - counter of speeches
110 		 * @param tnl - listener object
111 		 * @param id - unique number to internal works with news.
112 		 */
HeraldNews(String news, int interval, int limit, int counter, HeraldListener tnl, int id)113 		public HeraldNews(String news, int interval, int limit, int counter,
114 				HeraldListener tnl, int id){
115 			this.news=news;
116 			this.interval=interval;
117 			this.limit=limit;
118 			this.counter=counter;
119 			this.tnl=tnl;
120 			this.id=id;
121 		}
122 	}
123 
124 	/**
125 	 * herald turn listener object.
126 	 */
127 	class HeraldListener implements TurnListener{
128 		private final int id;
129 		/**
130 		 * function invokes by TurnNotifier each time when herald have to speech.
131 		 */
132 		@Override
onTurnReached(int currentTurn)133 		public void onTurnReached(int currentTurn) {
134 			workWithCounters(id);
135 			}
136 			/**
137 			 * tnl constructor.
138 			 * @param i - id of news
139 			 */
HeraldListener(int i)140 		public HeraldListener(int i) {
141 			id=i;
142 		}
143 	}
144 
145 	/**
146 	 * invokes by /script -load Herald.class,
147 	 * placing herald near calling admin.
148 	 */
149 	@Override
load(final Player admin, final List<String> args, final ScriptingSandbox sandbox)150 	public void load(final Player admin, final List<String> args, final ScriptingSandbox sandbox) {
151 		if (admin==null) {
152 			logger.error("herald called by null admin", new Throwable());
153 		} else {
154 			if (sandbox.getZone(admin).collides(admin.getX()+1, admin.getY())) {
155 				logger.info("Spot for placing herald is occupied.");
156 				admin.sendPrivateText("Spot (right) near you is occupied, can't place herald here.");
157 				return;
158 			}
159 			sandbox.setZone(admin.getZone());
160 			sandbox.add(getHerald(sandbox.getZone(admin), admin.getX() + 1, admin.getY()));
161 		}
162 	}
163 
164 	/**
165 	 * function invokes by HeraldListener.onTurnReached each time when herald have to speech.
166 	 * @param id - ID for news in news list
167 	 */
workWithCounters(int id)168 	public void workWithCounters(int id) {
169 		int index=-1;
170 		for (int i=0; i<heraldNews.size(); i++){
171 			if(heraldNews.get(i).getid()==id){
172 				index=i;
173 			}
174 		}
175 		if (index==-1) {
176 			logger.info("workWithCounters: id not found. ");
177 		}
178 		try {
179 			final int interval = heraldNews.get(index).getInterval();
180 			final int limit = heraldNews.get(index).getLimit();
181 			final String text = heraldNews.get(index).getNews();
182 			int counter = heraldNews.get(index).getCounter();
183 			HeraldListener tnl = heraldNews.get(index).getTNL();
184 			final SpeakerNPC npc = SingletonRepository.getNPCList().get(HeraldName);
185 			npc.say(text);
186 			counter++;
187 			turnNotifier.dontNotify(tnl);
188 			if(interval*counter<limit){
189 				heraldNews.get(index).setCounter(counter);
190 				turnNotifier.notifyInSeconds(interval, tnl);
191 			} else {
192 				// it was last announce.
193 				heraldNews.remove(index);
194 			}
195 		} catch (IndexOutOfBoundsException ioobe) {
196 			logger.error("workWithCounters: index is out of bounds: "+Integer.toString(index)+
197 					     ", size "+Integer.toString(heraldNews.size())+
198 					     ", id "+Integer.toString(id),ioobe);
199 		}
200 	}
201 
202 	/**
203 	 * kind of herald constructor
204 	 * @param zone - zone to place herald
205 	 * @param x - x coord in zone
206 	 * @param y - y coord in zone
207 	 * @return herald NPC :-)
208 	 */
getHerald(StendhalRPZone zone, int x, int y)209 	private SpeakerNPC getHerald(StendhalRPZone zone, int x, int y) {
210 		final SpeakerNPC npc = new SpeakerNPC(HeraldName) {
211 
212 			/**
213 			 * npc says his job list
214 			 */
215 			class ReadJobsAction implements ChatAction {
216 				@Override
217 				public void fire(final Player player, final Sentence sentence, final EventRaiser npc){
218 					int newssize = heraldNews.size();
219 					if(newssize==0){
220 						npc.say("My announcements list is empty.");
221 						return;
222 					}
223 					StringBuilder sb=new StringBuilder();
224 					sb.append("Here " + Grammar.isare(newssize) + " my current " + Grammar.plnoun(newssize,"announcement") + ": ");
225 
226 
227 					for(int i=0; i<newssize;i++){
228 						// will add 1 to position numbers to show position 0 as 1.
229 						logger.info("info: index "+Integer.toString(i));
230 						try {
231 						final int left = heraldNews.get(i).getLimit()/heraldNews.get(i).getInterval()-
232 										 heraldNews.get(i).getCounter();
233 						sb.append(" #"+Integer.toString(i+1)+". (left "+
234 								  Integer.toString(left)+" times): "+
235 								"#Every #"+Integer.toString(heraldNews.get(i).getInterval())+
236 								" #seconds #to #"+Integer.toString(heraldNews.get(i).getLimit())+
237 								" #seconds: \""+heraldNews.get(i).getNews()+"\"");
238 						} catch (IndexOutOfBoundsException ioobe) {
239 							logger.error("ReadNewsAction: size of heraldNews = "+
240 									Integer.toString(newssize), ioobe);
241 						}
242 						if(i!=(newssize-1)){
243 							sb.append("; ");
244 						}
245 					}
246 					npc.say(sb.toString());
247 				}
248 			}
249 
250 
251 			/**
252 			 * npc says his job list
253 			 */
254 			class ReadNewsAction implements ChatAction {
255 				@Override
256 				public void fire(final Player player, final Sentence sentence, final EventRaiser npc){
257 					int newssize = heraldNews.size();
258 					if(newssize==0){
259 						npc.say("My announcements list is empty.");
260 						return;
261 					}
262 
263 					StringBuilder sb=new StringBuilder();
264 					sb.append("Here " + Grammar.isare(newssize) + " my current " + Grammar.plnoun(newssize,"announcement") + ": ");
265 
266 					for(int i=0; i<newssize;i++){
267 						// will add 1 to position numbers to show position 0 as 1.
268 						logger.info("info: index "+Integer.toString(i));
269 						try {
270 						sb.append("\""+heraldNews.get(i).getNews()+"\"");
271 						} catch (IndexOutOfBoundsException ioobe) {
272 							logger.error("ReadNewsAction: size of heraldNews = "+
273 									Integer.toString(newssize), ioobe);
274 						}
275 						if(i!=(newssize-1)){
276 							sb.append("; ");
277 						}
278 					}
279 					npc.say(sb.toString());
280 				}
281 			}
282 			/**
283 			 * NPC adds new job to his job list.
284 			 */
285 			class WriteNewsAction implements ChatAction {
286 				@Override
287 				public void fire(final Player player, final Sentence sentence, final EventRaiser npc){
288 					String text = sentence.getOriginalText();
289 					logger.info("Original sentence: " + text);
290 					final String[] starr = text.split(" ");
291 					if(starr.length < 2){
292 						npc.say("You forget time limit. I am mortal too and somewhat senile, you know.");
293 						return;
294 					}
295 					try {
296 						final int interval = Integer.parseInt(starr[1].trim());
297 						final int limit = Integer.parseInt(starr[2].trim());
298 						if(limit < interval){
299 							npc.say("I can count to "+Integer.toString(interval)+
300 									", and "+Integer.toString(limit)+" is less then " + Integer.toString(interval)+
301 									". Repeat please.");
302 							return;
303 						}
304 						try {
305 							text = text.substring(starr[0].length()).trim().
306 										substring(starr[1].length()).trim().
307 								        substring(starr[2].length()).trim();
308 							final String out="Interval: "+Integer.toString(interval)+", limit: "+
309 							Integer.toString(limit)+", text: \""+text+"\"";
310 							npc.say("Ok, I have recorded it. "+out);
311 							logger.info("Admin "+player.getName()+
312 										" added announcement: " +out);
313 							final HeraldListener tnl = new HeraldListener(heraldNews.size());
314 							heraldNews.add(new HeraldNews(text, interval, limit, 0, tnl, heraldNews.size()));
315 							turnNotifier.notifyInSeconds(interval,tnl);
316 						} catch (IndexOutOfBoundsException ioobe) {
317 						    	npc.say(FeelBad);
318 						    	logger.error("WriteNewsAction: Error while parsing sentence "+sentence.toString(), ioobe);
319 						}
320 					} catch (NumberFormatException nfe) {
321 						 npc.say(DontUnderstand);
322 						 logger.info("Error while parsing numbers. Interval and limit is: "+"("+starr[0]+"), ("+starr[1]+")");
323 					}
324 				}
325 			}
326 
327 			/**
328 			 * NPC removes one job from his job list.
329 			 */
330 			class RemoveNewsAction implements ChatAction {
331 				@Override
332 				public void fire(final Player player, final Sentence sentence, final EventRaiser npc){
333 					String text = sentence.getOriginalText();
334 					final String[] starr = text.split(" ");
335 					if(starr.length < 2){
336 						npc.say("Tell me the number of sentence to remove.");
337 						return;
338 					}
339 					final String number = starr[1];
340 					try {
341 						final int i = Integer.parseInt(number)-1;
342 						if (i < 0){
343 							npc.say(BadJoke);
344 							return;
345 						}
346 						if (heraldNews.size()==0) {
347 							npc.say("I dont have announcements now.");
348 							return;
349 						}
350 						if(i>=(heraldNews.size())){
351 							npc.say("I have only "+ Integer.toString(heraldNews.size())+
352 									" announcements at the moment.");
353 							return;
354 						}
355 						logger.warn("Admin "+player.getName()+" removing announcement #"+
356 									Integer.toString(i)+": interval "+
357 									Integer.toString(heraldNews.get(i).getInterval())+", limit "+
358 									Integer.toString(heraldNews.get(i).getLimit())+", text \""+
359 									heraldNews.get(i).getNews()+"\"");
360 						turnNotifier.dontNotify(heraldNews.get(i).getTNL());
361 						heraldNews.remove(i);
362 						npc.say("Ok, already forget it.");
363 					} catch (NumberFormatException nfe) {
364 						logger.error("RemoveNewsAction: cant remove "+number+" speech.", nfe);
365 						npc.say(DontUnderstand);
366 					}
367 				}
368 			}
369 
370 			/**
371 			 * npc removes all jobs from his job list
372 			 */
373 			class ClearNewsAction implements ChatAction {
374 					@Override
375 					public void fire(final Player player, final Sentence sentence, final EventRaiser npc){
376 						logger.info("ClearAllAction: Admin "+player.getName()+
377 									" cleared announcement list.");
378 						for (int i=0; i<heraldNews.size(); i++) {
379 							turnNotifier.dontNotify(heraldNews.get(i).getTNL());
380 						}
381 						if(heraldNews.size()!=0){
382 							npc.say("Ufff, I have now some time for rest. I heard, there is a gambling game in Semos city?");
383 							heraldNews.clear();
384 						} else {
385 							npc.say("Oh, thank you for trying to help me, but I'm ok.");
386 						}
387 					}
388 			}
389 
390 			/**
391 			 *  Finite states machine logic for herald.
392 			 */
393 			@Override
394 			public void createDialog() {
395 				add(ConversationStates.IDLE,
396 					Arrays.asList("hi", "hola", "hello", "heya"),
397 					new NotCondition(new AdminCondition(REQUIRED_ADMINLEVEL_INFO)),
398 					ConversationStates.IDLE,
399 					null,	new ReadNewsAction());
400 				add(ConversationStates.IDLE,
401 					Arrays.asList("hi", "hola", "hello", "heya"),
402 					new AdminCondition(REQUIRED_ADMINLEVEL_INFO),
403 					ConversationStates.ATTENDING,
404 					HiOldFriend, null);
405 				add(ConversationStates.ATTENDING,
406 					Arrays.asList("help"),
407 					new AdminCondition(REQUIRED_ADMINLEVEL_SET),
408 					ConversationStates.ATTENDING,
409 					WillHelp, null);
410 				add(ConversationStates.ATTENDING,
411 					Arrays.asList("speech", "remove"),
412 					new NotCondition(new AdminCondition(REQUIRED_ADMINLEVEL_SET)),
413 					ConversationStates.ATTENDING,
414 					TooScared, null);
415 				add(ConversationStates.ATTENDING,
416 					Arrays.asList("help"),
417 					new NotCondition(new AdminCondition(REQUIRED_ADMINLEVEL_SET)),
418 					ConversationStates.ATTENDING,
419 					InfoOnly, new ReadJobsAction());
420 				add(ConversationStates.ATTENDING,
421 					Arrays.asList("info", "list", "tasks", "news"),
422 					new AdminCondition(REQUIRED_ADMINLEVEL_INFO),
423 					ConversationStates.ATTENDING,
424 					null, new ReadJobsAction());
425 				add(ConversationStates.ATTENDING,
426 					Arrays.asList("speech"),
427 					new AdminCondition(REQUIRED_ADMINLEVEL_SET),
428 					ConversationStates.ATTENDING,
429 					null, new WriteNewsAction());
430 				add(ConversationStates.ATTENDING,
431 					Arrays.asList("remove"),
432 					new AdminCondition(REQUIRED_ADMINLEVEL_SET),
433 					ConversationStates.ATTENDING,
434 					null, new RemoveNewsAction());
435 				add(ConversationStates.ATTENDING,
436 						Arrays.asList("clear"),
437 						new AdminCondition(REQUIRED_ADMINLEVEL_SET),
438 						ConversationStates.ATTENDING,
439 						null, new ClearNewsAction());
440 				addGoodbye();
441 			}
442 		};
443 		zone.assignRPObjectID(npc);
444 		npc.setEntityClass("heraldnpc");
445 		npc.setPosition(x, y);
446 		npc.initHP(100);
447 		npc.setDirection(Direction.LEFT);
448 		return(npc);
449 	}
450 }
451