1 /*
2  * This file is part of OpenTTD.
3  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6  */
7 
8 /** @file story_gui.cpp GUI for stories. */
9 
10 #include "stdafx.h"
11 #include "window_gui.h"
12 #include "strings_func.h"
13 #include "date_func.h"
14 #include "gui.h"
15 #include "story_base.h"
16 #include "core/geometry_func.hpp"
17 #include "company_func.h"
18 #include "command_func.h"
19 #include "widgets/dropdown_type.h"
20 #include "widgets/dropdown_func.h"
21 #include "sortlist_type.h"
22 #include "goal_base.h"
23 #include "viewport_func.h"
24 #include "window_func.h"
25 #include "company_base.h"
26 #include "tilehighlight_func.h"
27 #include "vehicle_base.h"
28 
29 #include "widgets/story_widget.h"
30 
31 #include "table/strings.h"
32 #include "table/sprites.h"
33 
34 #include <numeric>
35 
36 #include "safeguards.h"
37 
38 static CursorID TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor);
39 
40 typedef GUIList<const StoryPage*> GUIStoryPageList;
41 typedef GUIList<const StoryPageElement*> GUIStoryPageElementList;
42 
43 struct StoryBookWindow : Window {
44 protected:
45 	struct LayoutCacheElement {
46 		const StoryPageElement *pe;
47 		Rect bounds;
48 	};
49 	typedef std::vector<LayoutCacheElement> LayoutCache;
50 
51 	enum class ElementFloat {
52 		None,
53 		Left,
54 		Right,
55 	};
56 
57 	Scrollbar *vscroll;                ///< Scrollbar of the page text.
58 	mutable LayoutCache layout_cache;  ///< Cached element layout.
59 
60 	GUIStoryPageList story_pages;      ///< Sorted list of pages.
61 	GUIStoryPageElementList story_page_elements; ///< Sorted list of page elements that belong to the current page.
62 	StoryPageID selected_page_id;      ///< Pool index of selected page.
63 	char selected_generic_title[255];  ///< If the selected page doesn't have a custom title, this buffer is used to store a generic page title.
64 
65 	StoryPageElementID active_button_id; ///< Which button element the player is currently using
66 
67 	static GUIStoryPageList::SortFunction * const page_sorter_funcs[];
68 	static GUIStoryPageElementList::SortFunction * const page_element_sorter_funcs[];
69 
70 	/** (Re)Build story page list. */
BuildStoryPageListStoryBookWindow71 	void BuildStoryPageList()
72 	{
73 		if (this->story_pages.NeedRebuild()) {
74 			this->story_pages.clear();
75 
76 			for (const StoryPage *p : StoryPage::Iterate()) {
77 				if (this->IsPageAvailable(p)) {
78 					this->story_pages.push_back(p);
79 				}
80 			}
81 
82 			this->story_pages.shrink_to_fit();
83 			this->story_pages.RebuildDone();
84 		}
85 
86 		this->story_pages.Sort();
87 	}
88 
89 	/** Sort story pages by order value. */
PageOrderSorterStoryBookWindow90 	static bool PageOrderSorter(const StoryPage * const &a, const StoryPage * const &b)
91 	{
92 		return a->sort_value < b->sort_value;
93 	}
94 
95 	/** (Re)Build story page element list. */
BuildStoryPageElementListStoryBookWindow96 	void BuildStoryPageElementList()
97 	{
98 		if (this->story_page_elements.NeedRebuild()) {
99 			this->story_page_elements.clear();
100 
101 			const StoryPage *p = GetSelPage();
102 			if (p != nullptr) {
103 				for (const StoryPageElement *pe : StoryPageElement::Iterate()) {
104 					if (pe->page == p->index) {
105 						this->story_page_elements.push_back(pe);
106 					}
107 				}
108 			}
109 
110 			this->story_page_elements.shrink_to_fit();
111 			this->story_page_elements.RebuildDone();
112 		}
113 
114 		this->story_page_elements.Sort();
115 		this->InvalidateStoryPageElementLayout();
116 	}
117 
118 	/** Sort story page elements by order value. */
PageElementOrderSorterStoryBookWindow119 	static bool PageElementOrderSorter(const StoryPageElement * const &a, const StoryPageElement * const &b)
120 	{
121 		return a->sort_value < b->sort_value;
122 	}
123 
124 	/*
125 	 * Checks if a given page should be visible in the story book.
126 	 * @param page The page to check.
127 	 * @return True if the page should be visible, otherwise false.
128 	 */
IsPageAvailableStoryBookWindow129 	bool IsPageAvailable(const StoryPage *page) const
130 	{
131 		return page->company == INVALID_COMPANY || page->company == this->window_number;
132 	}
133 
134 	/**
135 	 * Get instance of selected page.
136 	 * @return Instance of selected page or nullptr if no page is selected.
137 	 */
GetSelPageStoryBookWindow138 	StoryPage *GetSelPage() const
139 	{
140 		if (!_story_page_pool.IsValidID(selected_page_id)) return nullptr;
141 		return _story_page_pool.Get(selected_page_id);
142 	}
143 
144 	/**
145 	 * Get the page number of selected page.
146 	 * @return Number of available pages before to the selected one, or -1 if no page is selected.
147 	 */
GetSelPageNumStoryBookWindow148 	int GetSelPageNum() const
149 	{
150 		int page_number = 0;
151 		for (const StoryPage *p : this->story_pages) {
152 			if (p->index == this->selected_page_id) {
153 				return page_number;
154 			}
155 			page_number++;
156 		}
157 		return -1;
158 	}
159 
160 	/**
161 	 * Check if the selected page is also the first available page.
162 	 */
IsFirstPageSelectedStoryBookWindow163 	bool IsFirstPageSelected()
164 	{
165 		/* Verify that the selected page exist. */
166 		if (!_story_page_pool.IsValidID(this->selected_page_id)) return false;
167 
168 		return this->story_pages.front()->index == this->selected_page_id;
169 	}
170 
171 	/**
172 	 * Check if the selected page is also the last available page.
173 	 */
IsLastPageSelectedStoryBookWindow174 	bool IsLastPageSelected()
175 	{
176 		/* Verify that the selected page exist. */
177 		if (!_story_page_pool.IsValidID(this->selected_page_id)) return false;
178 
179 		if (this->story_pages.size() <= 1) return true;
180 		const StoryPage *last = this->story_pages.back();
181 		return last->index == this->selected_page_id;
182 	}
183 
184 	/**
185 	 * Updates the content of selected page.
186 	 */
RefreshSelectedPageStoryBookWindow187 	void RefreshSelectedPage()
188 	{
189 		/* Generate generic title if selected page have no custom title. */
190 		StoryPage *page = this->GetSelPage();
191 		if (page != nullptr && page->title == nullptr) {
192 			SetDParam(0, GetSelPageNum() + 1);
193 			GetString(selected_generic_title, STR_STORY_BOOK_GENERIC_PAGE_ITEM, lastof(selected_generic_title));
194 		}
195 
196 		this->story_page_elements.ForceRebuild();
197 		this->BuildStoryPageElementList();
198 
199 		if (this->active_button_id != INVALID_STORY_PAGE_ELEMENT) ResetObjectToPlace();
200 
201 		this->vscroll->SetCount(this->GetContentHeight());
202 		this->SetWidgetDirty(WID_SB_SCROLLBAR);
203 		this->SetWidgetDirty(WID_SB_SEL_PAGE);
204 		this->SetWidgetDirty(WID_SB_PAGE_PANEL);
205 	}
206 
207 	/**
208 	 * Selects the previous available page before the currently selected page.
209 	 */
SelectPrevPageStoryBookWindow210 	void SelectPrevPage()
211 	{
212 		if (!_story_page_pool.IsValidID(this->selected_page_id)) return;
213 
214 		/* Find the last available page which is previous to the current selected page. */
215 		const StoryPage *last_available;
216 		last_available = nullptr;
217 		for (const StoryPage *p : this->story_pages) {
218 			if (p->index == this->selected_page_id) {
219 				if (last_available == nullptr) return; // No previous page available.
220 				this->SetSelectedPage(last_available->index);
221 				return;
222 			}
223 			last_available = p;
224 		}
225 	}
226 
227 	/**
228 	 * Selects the next available page after the currently selected page.
229 	 */
SelectNextPageStoryBookWindow230 	void SelectNextPage()
231 	{
232 		if (!_story_page_pool.IsValidID(this->selected_page_id)) return;
233 
234 		/* Find selected page. */
235 		for (auto iter = this->story_pages.begin(); iter != this->story_pages.end(); iter++) {
236 			const StoryPage *p = *iter;
237 			if (p->index == this->selected_page_id) {
238 				/* Select the page after selected page. */
239 				iter++;
240 				if (iter != this->story_pages.end()) {
241 					this->SetSelectedPage((*iter)->index);
242 				}
243 				return;
244 			}
245 		}
246 	}
247 
248 	/**
249 	 * Builds the page selector drop down list.
250 	 */
BuildDropDownListStoryBookWindow251 	DropDownList BuildDropDownList() const
252 	{
253 		DropDownList list;
254 		uint16 page_num = 1;
255 		for (const StoryPage *p : this->story_pages) {
256 			bool current_page = p->index == this->selected_page_id;
257 			DropDownListStringItem *item = nullptr;
258 			if (p->title != nullptr) {
259 				item = new DropDownListCharStringItem(p->title, p->index, current_page);
260 			} else {
261 				/* No custom title => use a generic page title with page number. */
262 				DropDownListParamStringItem *str_item =
263 						new DropDownListParamStringItem(STR_STORY_BOOK_GENERIC_PAGE_ITEM, p->index, current_page);
264 				str_item->SetParam(0, page_num);
265 				item = str_item;
266 			}
267 
268 			list.emplace_back(item);
269 			page_num++;
270 		}
271 
272 		return list;
273 	}
274 
275 	/**
276 	 * Get the width available for displaying content on the page panel.
277 	 */
GetAvailablePageContentWidthStoryBookWindow278 	uint GetAvailablePageContentWidth() const
279 	{
280 		return this->GetWidget<NWidgetCore>(WID_SB_PAGE_PANEL)->current_x - WD_FRAMETEXT_LEFT - WD_FRAMETEXT_RIGHT - 1;
281 	}
282 
283 	/**
284 	 * Counts how many pixels of height that are used by Date and Title
285 	 * (excluding marginal after Title, as each body element has
286 	 * an empty row before the element).
287 	 * @param max_width Available width to display content.
288 	 * @return the height in pixels.
289 	 */
GetHeadHeightStoryBookWindow290 	uint GetHeadHeight(int max_width) const
291 	{
292 		StoryPage *page = this->GetSelPage();
293 		if (page == nullptr) return 0;
294 		int height = 0;
295 
296 		/* Title lines */
297 		height += FONT_HEIGHT_NORMAL; // Date always use exactly one line.
298 		SetDParamStr(0, page->title != nullptr ? page->title : this->selected_generic_title);
299 		height += GetStringHeight(STR_STORY_BOOK_TITLE, max_width);
300 
301 		return height;
302 	}
303 
304 	/**
305 	 * Decides which sprite to display for a given page element.
306 	 * @param pe The page element.
307 	 * @return The SpriteID of the sprite to display.
308 	 * @pre pe.type must be SPET_GOAL or SPET_LOCATION.
309 	 */
GetPageElementSpriteStoryBookWindow310 	SpriteID GetPageElementSprite(const StoryPageElement &pe) const
311 	{
312 		switch (pe.type) {
313 			case SPET_GOAL: {
314 				Goal *g = Goal::Get((GoalID) pe.referenced_id);
315 				if (g == nullptr) return SPR_IMG_GOAL_BROKEN_REF;
316 				return g->completed ? SPR_IMG_GOAL_COMPLETED : SPR_IMG_GOAL;
317 			}
318 			case SPET_LOCATION:
319 				return SPR_IMG_VIEW_LOCATION;
320 			default:
321 				NOT_REACHED();
322 		}
323 	}
324 
325 	/**
326 	 * Get the height in pixels used by a page element.
327 	 * @param pe The story page element.
328 	 * @param max_width Available width to display content.
329 	 * @return the height in pixels.
330 	 */
GetPageElementHeightStoryBookWindow331 	uint GetPageElementHeight(const StoryPageElement &pe, int max_width) const
332 	{
333 		switch (pe.type) {
334 			case SPET_TEXT:
335 				SetDParamStr(0, pe.text);
336 				return GetStringHeight(STR_BLACK_RAW_STRING, max_width);
337 
338 			case SPET_GOAL:
339 			case SPET_LOCATION: {
340 				Dimension sprite_dim = GetSpriteSize(GetPageElementSprite(pe));
341 				return sprite_dim.height;
342 			}
343 
344 			case SPET_BUTTON_PUSH:
345 			case SPET_BUTTON_TILE:
346 			case SPET_BUTTON_VEHICLE: {
347 				Dimension dim = GetStringBoundingBox(pe.text, FS_NORMAL);
348 				return dim.height + WD_BEVEL_TOP + WD_BEVEL_BOTTOM + WD_FRAMETEXT_TOP + WD_FRAMETEXT_BOTTOM;
349 			}
350 
351 			default:
352 				NOT_REACHED();
353 		}
354 		return 0;
355 	}
356 
357 	/**
358 	 * Get the float style of a page element.
359 	 * @param pe The story page element.
360 	 * @return The float style.
361 	 */
GetPageElementFloatStoryBookWindow362 	ElementFloat GetPageElementFloat(const StoryPageElement &pe) const
363 	{
364 		switch (pe.type) {
365 			case SPET_BUTTON_PUSH:
366 			case SPET_BUTTON_TILE:
367 			case SPET_BUTTON_VEHICLE: {
368 				StoryPageButtonFlags flags = StoryPageButtonData{ pe.referenced_id }.GetFlags();
369 				if (flags & SPBF_FLOAT_LEFT) return ElementFloat::Left;
370 				if (flags & SPBF_FLOAT_RIGHT) return ElementFloat::Right;
371 				return ElementFloat::None;
372 			}
373 
374 			default:
375 				return ElementFloat::None;
376 		}
377 	}
378 
379 	/**
380 	 * Get the width a page element would use if it was floating left or right.
381 	 * @param pe The story page element.
382 	 * @return The calculated width of the element.
383 	 */
GetPageElementFloatWidthStoryBookWindow384 	int GetPageElementFloatWidth(const StoryPageElement &pe) const
385 	{
386 		switch (pe.type) {
387 			case SPET_BUTTON_PUSH:
388 			case SPET_BUTTON_TILE:
389 			case SPET_BUTTON_VEHICLE: {
390 				Dimension dim = GetStringBoundingBox(pe.text, FS_NORMAL);
391 				return dim.width + WD_BEVEL_LEFT + WD_BEVEL_RIGHT + WD_FRAMETEXT_LEFT + WD_FRAMETEXT_RIGHT;
392 			}
393 
394 			default:
395 				NOT_REACHED(); // only buttons can float
396 		}
397 	}
398 
399 	/** Invalidate the current page layout */
InvalidateStoryPageElementLayoutStoryBookWindow400 	void InvalidateStoryPageElementLayout()
401 	{
402 		this->layout_cache.clear();
403 	}
404 
405 	/** Create the page layout if it is missing */
EnsureStoryPageElementLayoutStoryBookWindow406 	void EnsureStoryPageElementLayout() const
407 	{
408 		/* Assume if the layout cache has contents it is valid */
409 		if (!this->layout_cache.empty()) return;
410 
411 		StoryPage *page = this->GetSelPage();
412 		if (page == nullptr) return;
413 		int max_width = GetAvailablePageContentWidth();
414 		int element_dist = FONT_HEIGHT_NORMAL;
415 
416 		/* Make space for the header */
417 		int main_y = GetHeadHeight(max_width) + element_dist;
418 
419 		/* Current bottom of left/right column */
420 		int left_y = main_y;
421 		int right_y = main_y;
422 		/* Current width of left/right column, 0 indicates no content in column */
423 		int left_width = 0;
424 		int right_width = 0;
425 		/* Indexes into element cache for yet unresolved floats */
426 		std::vector<size_t> left_floats;
427 		std::vector<size_t> right_floats;
428 
429 		/* Build layout */
430 		for (const StoryPageElement *pe : this->story_page_elements) {
431 			ElementFloat fl = this->GetPageElementFloat(*pe);
432 
433 			if (fl == ElementFloat::None) {
434 				/* Verify available width */
435 				const int min_required_width = 10 * FONT_HEIGHT_NORMAL;
436 				int left_offset = (left_width == 0) ? 0 : (left_width + element_dist);
437 				int right_offset = (right_width == 0) ? 0 : (right_width + element_dist);
438 				if (left_offset + right_offset + min_required_width >= max_width) {
439 					/* Width of floats leave too little for main content, push down */
440 					main_y = std::max(main_y, left_y);
441 					main_y = std::max(main_y, right_y);
442 					left_width = right_width = 0;
443 					left_offset = right_offset = 0;
444 					/* Do not add element_dist here, to keep together elements which were supposed to float besides each other. */
445 				}
446 				/* Determine height */
447 				const int available_width = max_width - left_offset - right_offset;
448 				const int height = GetPageElementHeight(*pe, available_width);
449 				/* Check for button that needs extra margin */
450 				if (left_offset == 0 && right_offset == 0) {
451 					switch (pe->type) {
452 						case SPET_BUTTON_PUSH:
453 						case SPET_BUTTON_TILE:
454 						case SPET_BUTTON_VEHICLE:
455 							left_offset = right_offset = available_width / 5;
456 							break;
457 						default:
458 							break;
459 					}
460 				}
461 				/* Position element in main column */
462 				LayoutCacheElement ce{ pe, {} };
463 				ce.bounds.left = left_offset;
464 				ce.bounds.right = max_width - right_offset;
465 				ce.bounds.top = main_y;
466 				main_y += height;
467 				ce.bounds.bottom = main_y;
468 				this->layout_cache.push_back(ce);
469 				main_y += element_dist;
470 				/* Clear all floats */
471 				left_width = right_width = 0;
472 				left_y = right_y = main_y = std::max({main_y, left_y, right_y});
473 				left_floats.clear();
474 				right_floats.clear();
475 			} else {
476 				/* Prepare references to correct column */
477 				int &cur_width = (fl == ElementFloat::Left) ? left_width : right_width;
478 				int &cur_y = (fl == ElementFloat::Left) ? left_y : right_y;
479 				std::vector<size_t> &cur_floats = (fl == ElementFloat::Left) ? left_floats : right_floats;
480 				/* Position element */
481 				cur_width = std::max(cur_width, this->GetPageElementFloatWidth(*pe));
482 				LayoutCacheElement ce{ pe, {} };
483 				ce.bounds.left = (fl == ElementFloat::Left) ? 0 : (max_width - cur_width);
484 				ce.bounds.right = (fl == ElementFloat::Left) ? cur_width : max_width;
485 				ce.bounds.top = cur_y;
486 				cur_y += GetPageElementHeight(*pe, cur_width);
487 				ce.bounds.bottom = cur_y;
488 				cur_floats.push_back(this->layout_cache.size());
489 				this->layout_cache.push_back(ce);
490 				cur_y += element_dist;
491 				/* Update floats in column to all have the same width */
492 				for (size_t index : cur_floats) {
493 					LayoutCacheElement &ce = this->layout_cache[index];
494 					ce.bounds.left = (fl == ElementFloat::Left) ? 0 : (max_width - cur_width);
495 					ce.bounds.right = (fl == ElementFloat::Left) ? cur_width : max_width;
496 				}
497 			}
498 		}
499 	}
500 
501 	/**
502 	 * Get the total height of the content displayed in this window.
503 	 * @return the height in pixels
504 	 */
GetContentHeightStoryBookWindow505 	uint GetContentHeight()
506 	{
507 		this->EnsureStoryPageElementLayout();
508 
509 		/* The largest bottom coordinate of any element is the height of the content */
510 		uint max_y = std::accumulate(this->layout_cache.begin(), this->layout_cache.end(), 0, [](uint max_y, const LayoutCacheElement &ce) -> uint { return std::max<uint>(max_y, ce.bounds.bottom); });
511 
512 		return max_y;
513 	}
514 
515 	/**
516 	 * Draws a page element that is composed of a sprite to the left and a single line of
517 	 * text after that. These page elements are generally clickable and are thus called
518 	 * action elements.
519 	 * @param y_offset Current y_offset which will get updated when this method has completed its drawing.
520 	 * @param width Width of the region available for drawing.
521 	 * @param line_height Height of one line of text.
522 	 * @param action_sprite The sprite to draw.
523 	 * @param string_id The string id to draw.
524 	 * @return the number of lines.
525 	 */
DrawActionElementStoryBookWindow526 	void DrawActionElement(int &y_offset, int width, int line_height, SpriteID action_sprite, StringID string_id = STR_JUST_RAW_STRING) const
527 	{
528 		Dimension sprite_dim = GetSpriteSize(action_sprite);
529 		uint element_height = std::max(sprite_dim.height, (uint)line_height);
530 
531 		uint sprite_top = y_offset + (element_height - sprite_dim.height) / 2;
532 		uint text_top = y_offset + (element_height - line_height) / 2;
533 
534 		DrawSprite(action_sprite, PAL_NONE, 0, sprite_top);
535 		DrawString(sprite_dim.width + WD_FRAMETEXT_LEFT, width, text_top, string_id, TC_BLACK);
536 
537 		y_offset += element_height;
538 	}
539 
540 	/**
541 	 * Internal event handler for when a page element is clicked.
542 	 * @param pe The clicked page element.
543 	 */
OnPageElementClickStoryBookWindow544 	void OnPageElementClick(const StoryPageElement& pe)
545 	{
546 		switch (pe.type) {
547 			case SPET_TEXT:
548 				/* Do nothing. */
549 				break;
550 
551 			case SPET_LOCATION:
552 				if (_ctrl_pressed) {
553 					ShowExtraViewportWindow((TileIndex)pe.referenced_id);
554 				} else {
555 					ScrollMainWindowToTile((TileIndex)pe.referenced_id);
556 				}
557 				break;
558 
559 			case SPET_GOAL:
560 				ShowGoalsList((CompanyID)this->window_number);
561 				break;
562 
563 			case SPET_BUTTON_PUSH:
564 				if (this->active_button_id != INVALID_STORY_PAGE_ELEMENT) ResetObjectToPlace();
565 				this->active_button_id = pe.index;
566 				this->SetTimeout();
567 				this->SetWidgetDirty(WID_SB_PAGE_PANEL);
568 
569 				DoCommandP(0, pe.index, 0, CMD_STORY_PAGE_BUTTON);
570 				break;
571 
572 			case SPET_BUTTON_TILE:
573 				if (this->active_button_id == pe.index) {
574 					ResetObjectToPlace();
575 					this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
576 				} else {
577 					CursorID cursor = TranslateStoryPageButtonCursor(StoryPageButtonData{ pe.referenced_id }.GetCursor());
578 					SetObjectToPlaceWnd(cursor, PAL_NONE, HT_RECT, this);
579 					this->active_button_id = pe.index;
580 				}
581 				this->SetWidgetDirty(WID_SB_PAGE_PANEL);
582 				break;
583 
584 			case SPET_BUTTON_VEHICLE:
585 				if (this->active_button_id == pe.index) {
586 					ResetObjectToPlace();
587 					this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
588 				} else {
589 					CursorID cursor = TranslateStoryPageButtonCursor(StoryPageButtonData{ pe.referenced_id }.GetCursor());
590 					SetObjectToPlaceWnd(cursor, PAL_NONE, HT_VEHICLE, this);
591 					this->active_button_id = pe.index;
592 				}
593 				this->SetWidgetDirty(WID_SB_PAGE_PANEL);
594 				break;
595 
596 			default:
597 				NOT_REACHED();
598 		}
599 	}
600 
601 public:
StoryBookWindowStoryBookWindow602 	StoryBookWindow(WindowDesc *desc, WindowNumber window_number) : Window(desc)
603 	{
604 		this->CreateNestedTree();
605 		this->vscroll = this->GetScrollbar(WID_SB_SCROLLBAR);
606 		this->vscroll->SetStepSize(FONT_HEIGHT_NORMAL);
607 
608 		/* Initialize page sort. */
609 		this->story_pages.SetSortFuncs(StoryBookWindow::page_sorter_funcs);
610 		this->story_pages.ForceRebuild();
611 		this->BuildStoryPageList();
612 		this->story_page_elements.SetSortFuncs(StoryBookWindow::page_element_sorter_funcs);
613 		/* story_page_elements will get built by SetSelectedPage */
614 
615 		this->FinishInitNested(window_number);
616 		this->owner = (Owner)this->window_number;
617 
618 		/* Initialize selected vars. */
619 		this->selected_generic_title[0] = '\0';
620 		this->selected_page_id = INVALID_STORY_PAGE;
621 
622 		this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
623 
624 		this->OnInvalidateData(-1);
625 	}
626 
627 	/**
628 	 * Updates the disabled state of the prev/next buttons.
629 	 */
UpdatePrevNextDisabledStateStoryBookWindow630 	void UpdatePrevNextDisabledState()
631 	{
632 		this->SetWidgetDisabledState(WID_SB_PREV_PAGE, story_pages.size() == 0 || this->IsFirstPageSelected());
633 		this->SetWidgetDisabledState(WID_SB_NEXT_PAGE, story_pages.size() == 0 || this->IsLastPageSelected());
634 		this->SetWidgetDirty(WID_SB_PREV_PAGE);
635 		this->SetWidgetDirty(WID_SB_NEXT_PAGE);
636 	}
637 
638 	/**
639 	 * Sets the selected page.
640 	 * @param page_index pool index of the page to select.
641 	 */
SetSelectedPageStoryBookWindow642 	void SetSelectedPage(uint16 page_index)
643 	{
644 		if (this->selected_page_id != page_index) {
645 			if (this->active_button_id) ResetObjectToPlace();
646 			this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
647 			this->selected_page_id = page_index;
648 			this->RefreshSelectedPage();
649 			this->UpdatePrevNextDisabledState();
650 		}
651 	}
652 
SetStringParametersStoryBookWindow653 	void SetStringParameters(int widget) const override
654 	{
655 		switch (widget) {
656 			case WID_SB_SEL_PAGE: {
657 				StoryPage *page = this->GetSelPage();
658 				SetDParamStr(0, page != nullptr && page->title != nullptr ? page->title : this->selected_generic_title);
659 				break;
660 			}
661 			case WID_SB_CAPTION:
662 				if (this->window_number == INVALID_COMPANY) {
663 					SetDParam(0, STR_STORY_BOOK_SPECTATOR_CAPTION);
664 				} else {
665 					SetDParam(0, STR_STORY_BOOK_CAPTION);
666 					SetDParam(1, this->window_number);
667 				}
668 				break;
669 		}
670 	}
671 
OnPaintStoryBookWindow672 	void OnPaint() override
673 	{
674 		/* Detect if content has changed height. This can happen if a
675 		 * multi-line text contains eg. {COMPANY} and that company is
676 		 * renamed.
677 		 */
678 		if (this->vscroll->GetCount() != this->GetContentHeight()) {
679 			this->vscroll->SetCount(this->GetContentHeight());
680 			this->SetWidgetDirty(WID_SB_SCROLLBAR);
681 			this->SetWidgetDirty(WID_SB_PAGE_PANEL);
682 		}
683 
684 		this->DrawWidgets();
685 	}
686 
DrawWidgetStoryBookWindow687 	void DrawWidget(const Rect &r, int widget) const override
688 	{
689 		if (widget != WID_SB_PAGE_PANEL) return;
690 
691 		StoryPage *page = this->GetSelPage();
692 		if (page == nullptr) return;
693 
694 		const int x = r.left + WD_FRAMETEXT_LEFT;
695 		const int y = r.top + WD_FRAMETEXT_TOP;
696 		const int right = r.right - WD_FRAMETEXT_RIGHT;
697 		const int bottom = r.bottom - WD_FRAMETEXT_BOTTOM;
698 
699 		/* Set up a clipping region for the panel. */
700 		DrawPixelInfo tmp_dpi;
701 		if (!FillDrawPixelInfo(&tmp_dpi, x, y, right - x + 1, bottom - y + 1)) return;
702 
703 		DrawPixelInfo *old_dpi = _cur_dpi;
704 		_cur_dpi = &tmp_dpi;
705 
706 		/* Draw content (now coordinates given to Draw** are local to the new clipping region). */
707 		int line_height = FONT_HEIGHT_NORMAL;
708 		const int scrollpos = this->vscroll->GetPosition();
709 		int y_offset = -scrollpos;
710 
711 		/* Date */
712 		if (page->date != INVALID_DATE) {
713 			SetDParam(0, page->date);
714 			DrawString(0, right - x, y_offset, STR_JUST_DATE_LONG, TC_BLACK);
715 		}
716 		y_offset += line_height;
717 
718 		/* Title */
719 		SetDParamStr(0, page->title != nullptr ? page->title : this->selected_generic_title);
720 		y_offset = DrawStringMultiLine(0, right - x, y_offset, bottom - y, STR_STORY_BOOK_TITLE, TC_BLACK, SA_TOP | SA_HOR_CENTER);
721 
722 		/* Page elements */
723 		this->EnsureStoryPageElementLayout();
724 		for (const LayoutCacheElement &ce : this->layout_cache) {
725 			y_offset = ce.bounds.top - scrollpos;
726 			switch (ce.pe->type) {
727 				case SPET_TEXT:
728 					SetDParamStr(0, ce.pe->text);
729 					y_offset = DrawStringMultiLine(ce.bounds.left, ce.bounds.right, ce.bounds.top - scrollpos, ce.bounds.bottom - scrollpos, STR_JUST_RAW_STRING, TC_BLACK, SA_TOP | SA_LEFT);
730 					break;
731 
732 				case SPET_GOAL: {
733 					Goal *g = Goal::Get((GoalID) ce.pe->referenced_id);
734 					StringID string_id = g == nullptr ? STR_STORY_BOOK_INVALID_GOAL_REF : STR_JUST_RAW_STRING;
735 					if (g != nullptr) SetDParamStr(0, g->text);
736 					DrawActionElement(y_offset, ce.bounds.right - ce.bounds.left, line_height, GetPageElementSprite(*ce.pe), string_id);
737 					break;
738 				}
739 
740 				case SPET_LOCATION:
741 					SetDParamStr(0, ce.pe->text);
742 					DrawActionElement(y_offset, ce.bounds.right - ce.bounds.left, line_height, GetPageElementSprite(*ce.pe));
743 					break;
744 
745 				case SPET_BUTTON_PUSH:
746 				case SPET_BUTTON_TILE:
747 				case SPET_BUTTON_VEHICLE: {
748 					const int tmargin = WD_BEVEL_TOP + WD_FRAMETEXT_TOP;
749 					const FrameFlags frame = this->active_button_id == ce.pe->index ? FR_LOWERED : FR_NONE;
750 					const Colours bgcolour = StoryPageButtonData{ ce.pe->referenced_id }.GetColour();
751 
752 					DrawFrameRect(ce.bounds.left, ce.bounds.top - scrollpos, ce.bounds.right, ce.bounds.bottom - scrollpos - 1, bgcolour, frame);
753 
754 					SetDParamStr(0, ce.pe->text);
755 					DrawString(ce.bounds.left + WD_BEVEL_LEFT, ce.bounds.right - WD_BEVEL_RIGHT, ce.bounds.top + tmargin - scrollpos, STR_JUST_RAW_STRING, TC_WHITE, SA_CENTER);
756 					break;
757 				}
758 
759 				default: NOT_REACHED();
760 			}
761 		}
762 
763 		/* Restore clipping region. */
764 		_cur_dpi = old_dpi;
765 	}
766 
UpdateWidgetSizeStoryBookWindow767 	void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
768 	{
769 		if (widget != WID_SB_SEL_PAGE && widget != WID_SB_PAGE_PANEL) return;
770 
771 		Dimension d;
772 		d.height = FONT_HEIGHT_NORMAL;
773 		d.width = 0;
774 
775 		switch (widget) {
776 			case WID_SB_SEL_PAGE: {
777 
778 				/* Get max title width. */
779 				for (size_t i = 0; i < this->story_pages.size(); i++) {
780 					const StoryPage *s = this->story_pages[i];
781 
782 					if (s->title != nullptr) {
783 						SetDParamStr(0, s->title);
784 					} else {
785 						SetDParamStr(0, this->selected_generic_title);
786 					}
787 					Dimension title_d = GetStringBoundingBox(STR_BLACK_RAW_STRING);
788 
789 					if (title_d.width > d.width) {
790 						d.width = title_d.width;
791 					}
792 				}
793 
794 				d.width += padding.width;
795 				d.height += padding.height;
796 				*size = maxdim(*size, d);
797 				break;
798 			}
799 
800 			case WID_SB_PAGE_PANEL: {
801 				d.height *= 5;
802 				d.height += padding.height + WD_FRAMETEXT_TOP + WD_FRAMETEXT_BOTTOM;
803 				*size = maxdim(*size, d);
804 				break;
805 			}
806 		}
807 
808 	}
809 
OnResizeStoryBookWindow810 	void OnResize() override
811 	{
812 		this->InvalidateStoryPageElementLayout();
813 		this->vscroll->SetCapacityFromWidget(this, WID_SB_PAGE_PANEL, WD_FRAMETEXT_TOP + WD_FRAMETEXT_BOTTOM);
814 		this->vscroll->SetCount(this->GetContentHeight());
815 	}
816 
OnClickStoryBookWindow817 	void OnClick(Point pt, int widget, int click_count) override
818 	{
819 		switch (widget) {
820 			case WID_SB_SEL_PAGE: {
821 				DropDownList list = this->BuildDropDownList();
822 				if (!list.empty()) {
823 					/* Get the index of selected page. */
824 					int selected = 0;
825 					for (size_t i = 0; i < this->story_pages.size(); i++) {
826 						const StoryPage *p = this->story_pages[i];
827 						if (p->index == this->selected_page_id) break;
828 						selected++;
829 					}
830 
831 					ShowDropDownList(this, std::move(list), selected, widget);
832 				}
833 				break;
834 			}
835 
836 			case WID_SB_PREV_PAGE:
837 				this->SelectPrevPage();
838 				break;
839 
840 			case WID_SB_NEXT_PAGE:
841 				this->SelectNextPage();
842 				break;
843 
844 			case WID_SB_PAGE_PANEL: {
845 				int clicked_y = this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_SB_PAGE_PANEL, WD_FRAMETEXT_TOP);
846 				this->EnsureStoryPageElementLayout();
847 
848 				for (const LayoutCacheElement &ce : this->layout_cache) {
849 					if (clicked_y >= ce.bounds.top && clicked_y < ce.bounds.bottom && pt.x >= ce.bounds.left && pt.x < ce.bounds.right) {
850 						this->OnPageElementClick(*ce.pe);
851 						return;
852 					}
853 				}
854 			}
855 		}
856 	}
857 
OnDropdownSelectStoryBookWindow858 	void OnDropdownSelect(int widget, int index) override
859 	{
860 		if (widget != WID_SB_SEL_PAGE) return;
861 
862 		/* index (which is set in BuildDropDownList) is the page id. */
863 		this->SetSelectedPage(index);
864 	}
865 
866 	/**
867 	 * Some data on this window has become invalid.
868 	 * @param data Information about the changed data.
869 	 *   -1     Rebuild page list and refresh current page;
870 	 *   >= 0   Id of the page that needs to be refreshed. If it is not the current page, nothing happens.
871 	 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
872 	 */
OnInvalidateDataStoryBookWindow873 	void OnInvalidateData(int data = 0, bool gui_scope = true) override
874 	{
875 		if (!gui_scope) return;
876 
877 		/* If added/removed page, force rebuild. Sort order never change so just a
878 		 * re-sort is never needed.
879 		 */
880 		if (data == -1) {
881 			this->story_pages.ForceRebuild();
882 			this->BuildStoryPageList();
883 
884 			/* Was the last page removed? */
885 			if (this->story_pages.size() == 0) {
886 				this->selected_generic_title[0] = '\0';
887 			}
888 
889 			/* Verify page selection. */
890 			if (!_story_page_pool.IsValidID(this->selected_page_id)) {
891 				this->selected_page_id = INVALID_STORY_PAGE;
892 			}
893 			if (this->selected_page_id == INVALID_STORY_PAGE && this->story_pages.size() > 0) {
894 				/* No page is selected, but there exist at least one available.
895 				 * => Select first page.
896 				 */
897 				this->SetSelectedPage(this->story_pages[0]->index);
898 			}
899 
900 			this->SetWidgetDisabledState(WID_SB_SEL_PAGE, this->story_pages.size() == 0);
901 			this->SetWidgetDirty(WID_SB_SEL_PAGE);
902 			this->UpdatePrevNextDisabledState();
903 		} else if (data >= 0 && this->selected_page_id == data) {
904 			this->RefreshSelectedPage();
905 		}
906 	}
907 
OnTimeoutStoryBookWindow908 	void OnTimeout() override
909 	{
910 		this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
911 		this->SetWidgetDirty(WID_SB_PAGE_PANEL);
912 	}
913 
OnPlaceObjectStoryBookWindow914 	void OnPlaceObject(Point pt, TileIndex tile) override
915 	{
916 		const StoryPageElement *const pe = StoryPageElement::GetIfValid(this->active_button_id);
917 		if (pe == nullptr || pe->type != SPET_BUTTON_TILE) {
918 			ResetObjectToPlace();
919 			this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
920 			this->SetWidgetDirty(WID_SB_PAGE_PANEL);
921 			return;
922 		}
923 
924 		DoCommandP(tile, pe->index, 0, CMD_STORY_PAGE_BUTTON);
925 		ResetObjectToPlace();
926 	}
927 
OnVehicleSelectStoryBookWindow928 	bool OnVehicleSelect(const Vehicle *v) override
929 	{
930 		const StoryPageElement *const pe = StoryPageElement::GetIfValid(this->active_button_id);
931 		if (pe == nullptr || pe->type != SPET_BUTTON_VEHICLE) {
932 			ResetObjectToPlace();
933 			this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
934 			this->SetWidgetDirty(WID_SB_PAGE_PANEL);
935 			return false;
936 		}
937 
938 		/* Check that the vehicle matches the requested type */
939 		StoryPageButtonData data{ pe->referenced_id };
940 		VehicleType wanted_vehtype = data.GetVehicleType();
941 		if (wanted_vehtype != VEH_INVALID && wanted_vehtype != v->type) return false;
942 
943 		DoCommandP(0, pe->index, v->index, CMD_STORY_PAGE_BUTTON);
944 		ResetObjectToPlace();
945 		return true;
946 	}
947 
OnPlaceObjectAbortStoryBookWindow948 	void OnPlaceObjectAbort() override
949 	{
950 		this->active_button_id = INVALID_STORY_PAGE_ELEMENT;
951 		this->SetWidgetDirty(WID_SB_PAGE_PANEL);
952 	}
953 };
954 
955 GUIStoryPageList::SortFunction * const StoryBookWindow::page_sorter_funcs[] = {
956 	&PageOrderSorter,
957 };
958 
959 GUIStoryPageElementList::SortFunction * const StoryBookWindow::page_element_sorter_funcs[] = {
960 	&PageElementOrderSorter,
961 };
962 
963 static const NWidgetPart _nested_story_book_widgets[] = {
964 	NWidget(NWID_HORIZONTAL),
965 		NWidget(WWT_CLOSEBOX, COLOUR_BROWN),
966 		NWidget(WWT_CAPTION, COLOUR_BROWN, WID_SB_CAPTION), SetDataTip(STR_JUST_STRING, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
967 		NWidget(WWT_SHADEBOX, COLOUR_BROWN),
968 		NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN),
969 		NWidget(WWT_STICKYBOX, COLOUR_BROWN),
970 	EndContainer(),
971 	NWidget(NWID_HORIZONTAL), SetFill(1, 1),
972 		NWidget(NWID_VERTICAL), SetFill(1, 1),
973 			NWidget(WWT_PANEL, COLOUR_BROWN, WID_SB_PAGE_PANEL), SetResize(1, 1), SetScrollbar(WID_SB_SCROLLBAR), EndContainer(),
974 			NWidget(NWID_HORIZONTAL),
975 				NWidget(WWT_TEXTBTN, COLOUR_BROWN, WID_SB_PREV_PAGE), SetMinimalSize(100, 0), SetFill(0, 0), SetDataTip(STR_STORY_BOOK_PREV_PAGE, STR_STORY_BOOK_PREV_PAGE_TOOLTIP),
976 				NWidget(NWID_BUTTON_DROPDOWN, COLOUR_BROWN, WID_SB_SEL_PAGE), SetMinimalSize(93, 12), SetFill(1, 0),
977 														SetDataTip(STR_BLACK_RAW_STRING, STR_STORY_BOOK_SEL_PAGE_TOOLTIP), SetResize(1, 0),
978 				NWidget(WWT_TEXTBTN, COLOUR_BROWN, WID_SB_NEXT_PAGE), SetMinimalSize(100, 0), SetFill(0, 0), SetDataTip(STR_STORY_BOOK_NEXT_PAGE, STR_STORY_BOOK_NEXT_PAGE_TOOLTIP),
979 			EndContainer(),
980 		EndContainer(),
981 		NWidget(NWID_VERTICAL), SetFill(0, 1),
982 			NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_SB_SCROLLBAR),
983 			NWidget(WWT_RESIZEBOX, COLOUR_BROWN),
984 		EndContainer(),
985 	EndContainer(),
986 };
987 
988 static WindowDesc _story_book_desc(
989 	WDP_CENTER, "view_story", 400, 300,
990 	WC_STORY_BOOK, WC_NONE,
991 	0,
992 	_nested_story_book_widgets, lengthof(_nested_story_book_widgets)
993 );
994 
TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor)995 static CursorID TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor)
996 {
997 	switch (cursor) {
998 		case SPBC_MOUSE:          return SPR_CURSOR_MOUSE;
999 		case SPBC_ZZZ:            return SPR_CURSOR_ZZZ;
1000 		case SPBC_BUOY:           return SPR_CURSOR_BUOY;
1001 		case SPBC_QUERY:          return SPR_CURSOR_QUERY;
1002 		case SPBC_HQ:             return SPR_CURSOR_HQ;
1003 		case SPBC_SHIP_DEPOT:     return SPR_CURSOR_SHIP_DEPOT;
1004 		case SPBC_SIGN:           return SPR_CURSOR_SIGN;
1005 		case SPBC_TREE:           return SPR_CURSOR_TREE;
1006 		case SPBC_BUY_LAND:       return SPR_CURSOR_BUY_LAND;
1007 		case SPBC_LEVEL_LAND:     return SPR_CURSOR_LEVEL_LAND;
1008 		case SPBC_TOWN:           return SPR_CURSOR_TOWN;
1009 		case SPBC_INDUSTRY:       return SPR_CURSOR_INDUSTRY;
1010 		case SPBC_ROCKY_AREA:     return SPR_CURSOR_ROCKY_AREA;
1011 		case SPBC_DESERT:         return SPR_CURSOR_DESERT;
1012 		case SPBC_TRANSMITTER:    return SPR_CURSOR_TRANSMITTER;
1013 		case SPBC_AIRPORT:        return SPR_CURSOR_AIRPORT;
1014 		case SPBC_DOCK:           return SPR_CURSOR_DOCK;
1015 		case SPBC_CANAL:          return SPR_CURSOR_CANAL;
1016 		case SPBC_LOCK:           return SPR_CURSOR_LOCK;
1017 		case SPBC_RIVER:          return SPR_CURSOR_RIVER;
1018 		case SPBC_AQUEDUCT:       return SPR_CURSOR_AQUEDUCT;
1019 		case SPBC_BRIDGE:         return SPR_CURSOR_BRIDGE;
1020 		case SPBC_RAIL_STATION:   return SPR_CURSOR_RAIL_STATION;
1021 		case SPBC_TUNNEL_RAIL:    return SPR_CURSOR_TUNNEL_RAIL;
1022 		case SPBC_TUNNEL_ELRAIL:  return SPR_CURSOR_TUNNEL_ELRAIL;
1023 		case SPBC_TUNNEL_MONO:    return SPR_CURSOR_TUNNEL_MONO;
1024 		case SPBC_TUNNEL_MAGLEV:  return SPR_CURSOR_TUNNEL_MAGLEV;
1025 		case SPBC_AUTORAIL:       return SPR_CURSOR_AUTORAIL;
1026 		case SPBC_AUTOELRAIL:     return SPR_CURSOR_AUTOELRAIL;
1027 		case SPBC_AUTOMONO:       return SPR_CURSOR_AUTOMONO;
1028 		case SPBC_AUTOMAGLEV:     return SPR_CURSOR_AUTOMAGLEV;
1029 		case SPBC_WAYPOINT:       return SPR_CURSOR_WAYPOINT;
1030 		case SPBC_RAIL_DEPOT:     return SPR_CURSOR_RAIL_DEPOT;
1031 		case SPBC_ELRAIL_DEPOT:   return SPR_CURSOR_ELRAIL_DEPOT;
1032 		case SPBC_MONO_DEPOT:     return SPR_CURSOR_MONO_DEPOT;
1033 		case SPBC_MAGLEV_DEPOT:   return SPR_CURSOR_MAGLEV_DEPOT;
1034 		case SPBC_CONVERT_RAIL:   return SPR_CURSOR_CONVERT_RAIL;
1035 		case SPBC_CONVERT_ELRAIL: return SPR_CURSOR_CONVERT_ELRAIL;
1036 		case SPBC_CONVERT_MONO:   return SPR_CURSOR_CONVERT_MONO;
1037 		case SPBC_CONVERT_MAGLEV: return SPR_CURSOR_CONVERT_MAGLEV;
1038 		case SPBC_AUTOROAD:       return SPR_CURSOR_AUTOROAD;
1039 		case SPBC_AUTOTRAM:       return SPR_CURSOR_AUTOTRAM;
1040 		case SPBC_ROAD_DEPOT:     return SPR_CURSOR_ROAD_DEPOT;
1041 		case SPBC_BUS_STATION:    return SPR_CURSOR_BUS_STATION;
1042 		case SPBC_TRUCK_STATION:  return SPR_CURSOR_TRUCK_STATION;
1043 		case SPBC_ROAD_TUNNEL:    return SPR_CURSOR_ROAD_TUNNEL;
1044 		case SPBC_CLONE_TRAIN:    return SPR_CURSOR_CLONE_TRAIN;
1045 		case SPBC_CLONE_ROADVEH:  return SPR_CURSOR_CLONE_ROADVEH;
1046 		case SPBC_CLONE_SHIP:     return SPR_CURSOR_CLONE_SHIP;
1047 		case SPBC_CLONE_AIRPLANE: return SPR_CURSOR_CLONE_AIRPLANE;
1048 		case SPBC_DEMOLISH:       return ANIMCURSOR_DEMOLISH;
1049 		case SPBC_LOWERLAND:      return ANIMCURSOR_LOWERLAND;
1050 		case SPBC_RAISELAND:      return ANIMCURSOR_RAISELAND;
1051 		case SPBC_PICKSTATION:    return ANIMCURSOR_PICKSTATION;
1052 		case SPBC_BUILDSIGNALS:   return ANIMCURSOR_BUILDSIGNALS;
1053 		default: return SPR_CURSOR_QUERY;
1054 	}
1055 }
1056 
1057 /**
1058  * Raise or create the story book window for \a company, at page \a page_id.
1059  * @param company 'Owner' of the story book, may be #INVALID_COMPANY.
1060  * @param page_id Page to open, may be #INVALID_STORY_PAGE.
1061  */
ShowStoryBook(CompanyID company,uint16 page_id)1062 void ShowStoryBook(CompanyID company, uint16 page_id)
1063 {
1064 	if (!Company::IsValidID(company)) company = (CompanyID)INVALID_COMPANY;
1065 
1066 	StoryBookWindow *w = AllocateWindowDescFront<StoryBookWindow>(&_story_book_desc, company, true);
1067 	if (page_id != INVALID_STORY_PAGE) w->SetSelectedPage(page_id);
1068 }
1069