1-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
2-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
3
4local Game = require 'Game'
5local Lang = require 'Lang'
6local Ship = require 'Ship'
7local Comms = require 'Comms'
8local Event = require 'Event'
9local Space = require 'Space'
10local Timer = require 'Timer'
11local Engine = require 'Engine'
12local Format = require 'Format'
13local Mission = require 'Mission'
14local ShipDef = require 'ShipDef'
15local Character = require 'Character'
16local Equipment = require 'Equipment'
17local Serializer = require 'Serializer'
18
19local utils = require 'utils'
20
21local l = Lang.GetResource("module-scoop")
22local lc = Lang.GetResource("ui-core")
23
24local AU = 149597870700.0
25local LEGAL = 1
26local ILLEGAL = 2
27
28local mission_reputation = 1
29local mission_time = 14*24*60*60
30local max_dist = 20 * AU
31
32local ads = {}
33local missions = {}
34
35local rescue_capsule = Equipment.EquipType.New({
36	name = "rescue_capsule",
37	l10n_key = "RESCUE_CAPSULE",
38	l10n_resource = "module-scoop",
39	slots = "cargo",
40	price = 500,
41	icon_name = "Default",
42	model_name = "escape_pod",
43	capabilities = { mass = 1, crew = 1 },
44	purchasable = false
45})
46
47local rocket_launchers = Equipment.EquipType.New({
48	name = "rocket_launchers",
49	l10n_key = "ROCKET_LAUNCHERS",
50	l10n_resource = "module-scoop",
51	slots = "cargo",
52	price = 500,
53	icon_name = "Default",
54	capabilities = { mass = 1 },
55	purchasable = false
56})
57
58local detonators = Equipment.EquipType.New({
59	name = "detonators",
60	l10n_key = "DETONATORS",
61	l10n_resource = "module-scoop",
62	slots = "cargo",
63	price = 250,
64	icon_name = "Default",
65	capabilities = { mass = 1 },
66	purchasable = false
67})
68
69local nuclear_missile = Equipment.EquipType.New({
70	name = "nuclear_missile",
71	l10n_key = "NUCLEAR_MISSILE",
72	l10n_resource = "module-scoop",
73	slots = "cargo",
74	price = 1250,
75	icon_name = "Default",
76	model_name = "missile",
77	capabilities = { mass = 1 },
78	purchasable = false
79})
80
81-- Useless waste that the player has to sort out
82local toxic_waste = Equipment.EquipType.New({
83	name = "toxic_waste",
84	l10n_key = "TOXIC_WASTE",
85	l10n_resource = "module-scoop",
86	slots = "cargo",
87	price = -50,
88	icon_name = "Default",
89	capabilities = { mass = 1 },
90	purchasable = false
91})
92
93local spoiled_food = Equipment.EquipType.New({
94	name = "spoiled_food",
95	l10n_key = "SPOILED_FOOD",
96	l10n_resource = "module-scoop",
97	slots = "cargo",
98	price = -10,
99	icon_name = "Default",
100	capabilities = { mass = 1 },
101	purchasable = false
102})
103
104local unknown = Equipment.EquipType.New({
105	name = "unknown",
106	l10n_key = "UNKNOWN",
107	l10n_resource = "module-scoop",
108	slots = "cargo",
109	price = -5,
110	icon_name = "Default",
111	capabilities = { mass = 1 },
112	purchasable = false
113})
114
115local rescue_capsules = {
116	rescue_capsule
117}
118
119local weapons = {
120	rocket_launchers,
121	detonators,
122	nuclear_missile
123}
124
125local waste = {
126	toxic_waste,
127	spoiled_food,
128	unknown,
129	Equipment.cargo.radioactives,
130	Equipment.cargo.rubbish
131}
132
133local flavours = {
134	{
135		id = "LEGAL_GOODS",
136		cargo_type = nil,
137		reward = -500,
138		amount = 20,
139	},
140	{
141		id = "ILLEGAL_GOODS",
142		cargo_type = nil,
143		reward = -1000,
144		amount = 10,
145	},
146	{
147		id = "RESCUE",
148		cargo_type = rescue_capsules,
149		reward = 750,
150		amount = 4,
151		return_to_station = true,
152	},
153	{
154		id = "ARMS_DEALER",
155		cargo_type = weapons,
156		reward = 1000,
157		amount = 1,
158		deliver_to_ship = true,
159	},
160}
161
162-- Sort goods, legal and illegal
163local sortGoods = function (goods)
164	local legal_goods = {}
165	local illegal_goods = {}
166	local system = Game.system
167
168	for _, e in pairs(goods) do
169		if e.purchasable and system:IsCommodityLegal(e.name) then
170			table.insert(legal_goods, e)
171		else
172			table.insert(illegal_goods, e)
173		end
174	end
175
176	return legal_goods, illegal_goods
177end
178
179-- Returns the number of flavours of the given string (assuming first flavour has suffix '_1').
180local getNumberOfFlavours = function (str)
181	local num = 1
182
183	while l:get(str .. "_" .. num) do
184		num = num + 1
185	end
186
187	return num - 1
188end
189
190-- Create a debris field in a random distance to a system body
191local spawnDebris = function (debris, amount, sbody, min, max, lifetime)
192	local list = {}
193	local cargo
194	local body = Space.GetBody(sbody:GetSystemBody().index)
195
196	for i = 1, Engine.rand:Integer(math.ceil(amount / 4), amount) do
197		cargo = debris[Engine.rand:Integer(1, #debris)]
198		body = Space.SpawnCargoNear(cargo, body, min, max, lifetime)
199		if i > 1 then body:SetVelocity(list[1].body:GetVelocity()) end
200		table.insert(list, { cargo = cargo, body = body })
201		min = 10
202		max = 1000
203	end
204
205	-- add some useless waste
206	for i = 1, Engine.rand:Integer(1, 9) do
207		cargo = waste[Engine.rand:Integer(1, #waste)]
208		body = Space.SpawnCargoNear(cargo, body, min, max, lifetime)
209		body:SetVelocity(list[1].body:GetVelocity())
210	end
211
212	return list
213end
214
215-- Create a couple of police ships
216local spawnPolice = function (station)
217	local ship
218	local police = {}
219	local shipdef = ShipDef[Game.system.faction.policeShip]
220
221	for i = 1, 2 do
222		ship = Space.SpawnShipDocked(shipdef.id, station)
223		ship:SetLabel(lc.POLICE)
224		ship:AddEquip(Equipment.laser.pulsecannon_1mw)
225		table.insert(police, ship)
226		if station.type == "STARPORT_SURFACE" then
227			ship:AIEnterLowOrbit(Space.GetBody(station:GetSystemBody().parent.index))
228		end
229	end
230
231	Timer:CallAt(Game.time + 5, function ()
232		for _, s in pairs(police) do
233			s:AIKill(Game.player)
234		end
235	end)
236
237	return police
238end
239
240-- Returns a random system close to the players location
241local nearbySystem = function ()
242	local dist = 5
243	local systems = {}
244
245	while #systems < 1 do
246		systems = Game.system:GetNearbySystems(dist)
247		dist = dist + 5
248	end
249
250	return systems[Engine.rand:Integer(1, #systems)].path
251end
252
253-- Create a ship in orbit
254local spawnClientShip = function (star, ship_label)
255	local shipdefs = utils.build_array(utils.filter(
256		function (k, def)
257			return def.tag == "SHIP" and def.hyperdriveClass > 0 and def.equipSlotCapacity["scoop"] > 0
258		end,
259		pairs(ShipDef)))
260	local shipdef = shipdefs[Engine.rand:Integer(1, #shipdefs)]
261
262	local radius = star:GetSystemBody().radius
263	local min, max
264	if star:GetSystemBody().type == "WHITE_DWARF" then
265		min = radius * 30
266		max = radius * 40
267	else
268		min = radius * 3.5
269		max = radius * 4.5
270	end
271
272	local ship = Space.SpawnShipOrbit(shipdef.id, Space.GetBody(star:GetSystemBody().index), min, max)
273
274	ship:SetLabel(ship_label)
275	ship:AddEquip(Equipment.hyperspace["hyperdrive_" .. shipdef.hyperdriveClass])
276	ship:AddEquip(Equipment.laser.pulsecannon_2mw)
277	ship:AddEquip(Equipment.misc.shield_generator)
278
279	return ship
280end
281
282local removeMission = function (mission, ref)
283	local oldReputation = Character.persistent.player.reputation
284	local sender = mission.client_ship and mission.ship_label or mission.client.name
285
286	if mission.status == "COMPLETED" then
287		Character.persistent.player.reputation = oldReputation + mission_reputation
288		Game.player:AddMoney(mission.reward)
289		Comms.ImportantMessage(l["SUCCESS_MSG_" .. mission.id], sender)
290	elseif mission.status == "FAILED" then
291		Character.persistent.player.reputation = oldReputation - mission_reputation
292		Comms.ImportantMessage(l["FAILURE_MSG_" .. mission.id], sender)
293	end
294	Event.Queue("onReputationChanged", oldReputation, Character.persistent.player.killcount,
295		Character.persistent.player.reputation, Character.persistent.player.killcount)
296
297	if ref == nil then
298		for r, m in pairs(missions) do
299			if m == mission then ref = r break end
300		end
301	end
302	mission:Remove()
303	missions[ref] = nil
304end
305
306-- Cargo transfer to a ship
307local transferCargo = function (mission, ref)
308	Timer:CallEvery(9, function ()
309		if not mission.client_ship then return true end
310
311		if not mission.docking_in_progress and Game.player:DistanceTo(mission.client_ship) <= 5000 then
312			mission.docking_in_progress = true
313			Comms.ImportantMessage(l.DOCKING_INSTRUCTION, mission.ship_label)
314		end
315
316		if Game.player:DistanceTo(mission.client_ship) <= 100 then
317
318			-- unload mission cargo
319			for i, e in pairs(mission.debris) do
320				if e.body == nil then
321					if Game.player:RemoveEquip(e.cargo, 1, "cargo") == 1 then
322						mission.client_ship:AddEquip(e.cargo, 1, "cargo")
323						mission.debris[i] = nil
324						mission.amount = mission.amount - 1
325					end
326				end
327			end
328
329			if mission.amount == 0 then
330				mission.status = "COMPLETED"
331			elseif mission.destination == nil then
332				mission.status = "FAILED"
333			end
334		end
335
336		if mission.status == "COMPLETED" or mission.status == "FAILED" then
337			local ship = mission.client_ship
338			mission.client_ship = nil
339			removeMission(mission, ref)
340			ship:HyperjumpTo(nearbySystem())
341		end
342	end)
343end
344
345local isQualifiedFor = function(reputation, ad)
346	return reputation > (ad.reward/100) or false
347end
348
349local onDelete = function (ref)
350	ads[ref] = nil
351end
352
353local isEnabled = function (ref)
354	return ads[ref] ~= nil and isQualifiedFor(Character.persistent.player.reputation, ads[ref])
355end
356
357local onChat = function (form, ref, option)
358	local ad = ads[ref]
359	local player = Game.player
360	local debris, ship, radius, mindist, maxdist
361
362	form:Clear()
363
364	if option == -1 then
365		form:Close()
366		return
367	end
368
369	local qualified = isQualifiedFor(Character.persistent.player.reputation, ad)
370
371	form:SetFace(ad.client)
372
373	if not qualified then
374		form:SetMessage(l["DENY_" .. Engine.rand:Integer(1, getNumberOfFlavours("DENY"))])
375		return
376	end
377
378	form:AddNavButton(ad.planet)
379
380	if option == 0 then
381		local introtext = string.interp(ad.introtext, {
382			client = ad.client.name,
383			shipid = ad.ship_label,
384			star   = ad.star:GetSystemBody().name,
385			planet = ad.planet:GetSystemBody().name,
386			cash   = Format.Money(math.abs(ad.reward), false),
387		})
388		form:SetMessage(introtext)
389
390	elseif option == 1 then
391		form:SetMessage(l["WHY_NOT_YOURSELF_" .. ad.id])
392
393	elseif option == 2 then
394		form:SetMessage(string.interp(l["HOW_MUCH_TIME_" .. ad.id], { star = ad.star:GetSystemBody().name, date = Format.Date(ad.due) }))
395
396	elseif option == 3 then
397		if ad.reward > 0 and player:CountEquip(Equipment.misc.cargo_scoop) == 0 and player:CountEquip(Equipment.misc.multi_scoop) == 0 then
398			form:SetMessage(l.YOU_DO_NOT_HAVE_A_SCOOP)
399			form:RemoveNavButton()
400			return
401		end
402
403		if ad.reward < 0 and player:GetMoney() < math.abs(ad.reward) then
404			form:SetMessage(l.YOU_DO_NOT_HAVE_ENOUGH_MONEY)
405			form:RemoveNavButton()
406			return
407		end
408
409		form:RemoveAdvertOnClose()
410		ads[ref] = nil
411
412		radius = ad.planet:GetSystemBody().radius
413		if ad.planet:GetSystemBody().superType == "ROCKY_PLANET" then
414			mindist = radius * 2.5
415			maxdist = radius * 3.5
416		else
417			mindist = radius * 25
418			maxdist = radius * 35
419		end
420		debris = spawnDebris(ad.debris_type, ad.amount, ad.planet, mindist, maxdist, ad.due - Game.time)
421
422		if ad.reward < 0 then player:AddMoney(ad.reward) end
423		if ad.deliver_to_ship then
424			ship = spawnClientShip(ad.star, ad.ship_label)
425		end
426
427		local mission = {
428			type              = "Scoop",
429			location          = ad.location,
430			introtext         = ad.introtext,
431			client            = ad.client,
432			station           = ad.station.path,
433			star              = ad.star,
434			planet            = ad.planet,
435			id                = ad.id,
436			debris            = debris,
437			amount            = #debris,
438			reward            = ad.reward,
439			due               = ad.due,
440			return_to_station = ad.return_to_station,
441			deliver_to_ship   = ad.deliver_to_ship,
442			client_ship       = ship,
443			ship_label        = ad.ship_label,
444			destination       = debris[1].body
445		}
446
447		table.insert(missions, Mission.New(mission))
448		form:SetMessage(l["ACCEPTED_" .. ad.id])
449		form:RemoveNavButton()
450		form:AddNavButton(debris[1].body)
451		return
452	end
453
454	form:AddOption(l.WHY_NOT_YOURSELF, 1)
455	form:AddOption(l.HOW_MUCH_TIME, 2)
456	form:AddOption(l.REPEAT_THE_REQUEST, 0)
457	form:AddOption(l.OK_AGREED, 3)
458end
459
460local getPlanets = function (system)
461	local planets = {}
462
463	for _, p in ipairs(system:GetBodyPaths()) do
464		if p:GetSystemBody().superType == "ROCKY_PLANET" or p:GetSystemBody().superType == "GAS_GIANT" then
465			table.insert(planets, p)
466		end
467	end
468	return planets
469end
470
471local planets = nil
472
473local makeAdvert = function (station)
474	if planets == nil then planets = getPlanets(Game.system) end
475	if #planets == 0 then return end
476
477	if flavours[LEGAL].cargo_type == nil then
478		flavours[LEGAL].cargo_type, flavours[ILLEGAL].cargo_type = sortGoods(Equipment.cargo)
479	end
480
481	local stars = Game.system:GetStars()
482	local star = stars[Engine.rand:Integer(1, #stars)]
483	local planet = planets[Engine.rand:Integer(1, #planets)]
484	local dist = station:DistanceTo(Space.GetBody(planet:GetSystemBody().index))
485	local flavour = flavours[Engine.rand:Integer(1, #flavours)]
486	local due = Game.time + mission_time * (1 + dist / max_dist) * Engine.rand:Number(0.8, 1.2)
487	local reward
488
489	if flavour.reward < 0 then
490		reward = flavour.reward * (1.15 - dist / max_dist) * Engine.rand:Number(0.9, 1.1)
491	else
492		reward = flavour.reward * (1 + dist / max_dist) * Engine.rand:Number(0.75, 1.25)
493	end
494	reward = utils.round(reward, 50)
495
496	if #flavour.cargo_type > 0 and dist < max_dist and station:DistanceTo(Space.GetBody(star.index)) < max_dist then
497		local ad = {
498			station           = station,
499			location          = planet,
500			introtext         = l["INTROTEXT_" .. flavour.id],
501			client            = Character.New(),
502			star              = star.path,
503			planet            = planet,
504			id                = flavour.id,
505			debris_type       = flavour.cargo_type,
506			reward            = math.ceil(reward),
507			amount            = flavour.amount,
508			due               = due,
509			return_to_station = flavour.return_to_station,
510			deliver_to_ship   = flavour.deliver_to_ship,
511			ship_label        = flavour.deliver_to_ship and Ship.MakeRandomLabel() or nil
512		}
513
514		ad.desc = string.interp(l["ADTEXT_" .. flavour.id], { cash = Format.Money(ad.reward, false) })
515
516		local ref = station:AddAdvert({
517			description = ad.desc,
518			icon        = flavour.id == "RESCUE" and "searchrescue" or "haul",
519			onChat      = onChat,
520			onDelete    = onDelete,
521			isEnabled   = isEnabled
522		})
523		ads[ref] = ad
524	end
525end
526
527local onCreateBB = function (station)
528	local num = Engine.rand:Integer(0, math.ceil(Game.system.population * Game.system.lawlessness))
529	for i = 1, num do
530		makeAdvert(station)
531	end
532end
533
534local onUpdateBB = function (station)
535	for ref, ad in pairs(ads) do
536		if ad.due < Game.time + 5*24*60*60 then -- five day timeout
537			ad.station:RemoveAdvert(ref)
538		end
539	end
540	if Engine.rand:Integer(4*24*60*60) < 60*60 then -- roughly once every four days
541		makeAdvert(station)
542	end
543end
544
545local onShipEquipmentChanged = function (ship, equipment)
546	if not ship:IsPlayer() or equipment == nil or equipment:GetDefaultSlot() ~= "cargo" then return end
547
548	for ref, mission in pairs(missions) do
549		if not mission.police and not Game.system:IsCommodityLegal(equipment.name) and not ship:IsDocked() and mission.location:IsSameSystem(Game.system.path) then
550			if (1 - Game.system.lawlessness) > Engine.rand:Number(4) then
551				local station = ship:FindNearestTo("SPACESTATION")
552				if station then mission.police = spawnPolice(station) end
553			end
554		end
555	end
556end
557
558-- The attacker could be a ship or the planet
559-- If scooped or destroyed by self-destruction, attacker is nil
560local onCargoDestroyed = function (body, attacker)
561	for ref, mission in pairs(missions) do
562		for i, e in pairs(mission.debris) do
563			if body == e.body then
564				e.body = nil
565				if body == mission.destination then
566					-- remove NavButton
567					mission.destination = nil
568				end
569				if attacker and (mission.return_to_station or mission.deliver_to_ship) then
570					mission.status = "FAILED"
571				end
572				if mission.destination == nil then
573					for i, e in pairs(mission.debris) do
574						if e.body ~= nil then
575							-- set next target
576							mission.destination = e.body
577							break
578						end
579					end
580				end
581				break
582			end
583		end
584	end
585end
586
587local onJettison = function (ship, cargo)
588	if not ship:IsPlayer() then return end
589
590	for ref, mission in pairs(missions) do
591		if mission.reward > 0 and not mission.warning then
592			for i, e in pairs(mission.debris) do
593				if cargo == e.cargo then
594					mission.warning = true
595					Comms.ImportantMessage(l.WARNING, mission.client.name)
596					break
597				end
598			end
599		end
600	end
601end
602
603local onShipHit = function (ship, attacker)
604	if ship:IsPlayer() then return end
605	if attacker == nil or not attacker:isa('Ship') then return end
606
607	for ref, mission in pairs(missions) do
608		if mission.police then
609			for _, s in pairs(mission.police) do
610				if s == ship then
611					ship:AIKill(attacker)
612					break
613				end
614			end
615		elseif mission.client_ship == ship then
616			ship:AIKill(attacker)
617			break
618		end
619	end
620end
621
622local onShipDestroyed = function (ship, attacker)
623	if ship:IsPlayer() then return end
624
625	for ref, mission in pairs(missions) do
626		if mission.police then
627			for i, s in pairs(mission.police) do
628				if s == ship then
629					table.remove(mission.police, i)
630					break
631				end
632			end
633		elseif mission.client_ship == ship then
634			mission.client_ship = nil
635			local msg = string.interp(l.SHIP_DESTROYED, {
636				shipid  = mission.ship_label,
637				station = mission.station:GetSystemBody().name
638			})
639			Comms.ImportantMessage(msg, mission.client.name)
640			break
641		end
642	end
643end
644
645local onShipDocked = function (player, station)
646	if not player:IsPlayer() then return end
647
648	for ref, mission in pairs(missions) do
649		if mission.police then
650			for _, s in pairs(mission.police) do
651				if station.type == "STARPORT_SURFACE" then
652					s:AIEnterLowOrbit(Space.GetBody(station:GetSystemBody().parent.index))
653				else
654					s:AIFlyTo(station)
655				end
656			end
657		end
658
659		if mission.return_to_station or mission.deliver_to_ship and not mission.client_ship and mission.station == station.path then
660
661			-- unload mission cargo
662			for i, e in pairs(mission.debris) do
663				if e.body == nil then
664					if player:RemoveEquip(e.cargo, 1, "cargo") == 1 then
665						mission.debris[i] = nil
666						mission.amount = mission.amount - 1
667					end
668				end
669			end
670			if mission.amount == 0 then
671				mission.status = "COMPLETED"
672			elseif mission.destination == nil then
673				mission.status = "FAILED"
674			end
675
676			if mission.status == "COMPLETED" or mission.status == "FAILED" then
677				removeMission(mission, ref)
678			end
679
680		-- remove stale missions, if any
681		-- all cargo related to flavour 1 and 2 scooped or destroyed
682		elseif mission.reward < 0 and mission.destination == nil then
683			mission:Remove()
684			missions[ref] = nil
685		end
686	end
687end
688
689local onShipUndocked = function (player, station)
690	if not player:IsPlayer() then return end
691
692	for ref, mission in pairs(missions) do
693		if mission.police then
694			for _, s in pairs(mission.police) do
695				s:AIKill(player)
696			end
697		end
698
699		if mission.deliver_to_ship and not mission.in_progess then
700			mission.in_progress = true
701			transferCargo(mission, ref)
702		end
703	end
704end
705
706local getPopulatedPlanets = function (system)
707	local planets = {}
708
709	for _, p in ipairs(system:GetBodyPaths()) do
710		if p:GetSystemBody().population > 0 then
711			table.insert(planets, p)
712		end
713	end
714	return planets
715end
716
717local onEnterSystem = function (ship)
718	if not ship:IsPlayer() or Game.system.population == 0 then return end
719
720	local planets = getPopulatedPlanets(Game.system)
721	local num = Engine.rand:Integer(0, math.ceil(Game.system.population * Game.system.lawlessness))
722
723	flavours[LEGAL].cargo_type, flavours[ILLEGAL].cargo_type = sortGoods(Equipment.cargo)
724
725	-- spawn random cargo (legal or illegal goods)
726	for i = 1, num do
727		local planet = planets[Engine.rand:Integer(1, #planets)]
728		local radius = planet:GetSystemBody().radius
729		local flavour = flavours[Engine.rand:Integer(LEGAL, ILLEGAL)]
730		local debris = flavour.cargo_type
731		if #debris > 0 then
732			spawnDebris(debris, flavour.amount, planet, radius * 1.2, radius * 3.5, mission_time)
733		end
734	end
735end
736
737local onLeaveSystem = function (ship)
738	if ship:IsPlayer() then
739		for ref, mission in pairs(missions) do
740			mission.destination = nil
741			mission.police = nil
742			if mission.client_ship then
743				mission.client_ship = nil
744				mission.status = "FAILED"
745			end
746			for i, e in pairs(mission.debris) do
747				e.body = nil
748			end
749		end
750		planets = nil
751		flavours[LEGAL].cargo_type = nil
752		flavours[ILLEGAL].cargo_type = nil
753	end
754end
755
756local onReputationChanged = function (oldRep, oldKills, newRep, newKills)
757	for ref, ad in pairs(ads) do
758		local oldQualified = isQualifiedFor(oldRep, ad)
759		if isQualifiedFor(newRep, ad) ~= oldQualified then
760			Event.Queue("onAdvertChanged", ad.station, ref);
761		end
762	end
763end
764
765local buildMissionDescription = function(mission)
766	local ui = require 'pigui'
767	local desc = {}
768
769	desc.description = mission.introtext:interp({
770		client = mission.client.name,
771		shipid = mission.ship_label,
772		star   = mission.star:GetSystemBody().name,
773		planet = mission.planet:GetSystemBody().name,
774		cash   = Format.Money(math.abs(mission.reward), false)
775	})
776
777	desc.details = {
778		{ l.CLIENT, mission.client.name },
779		{ l.SPACEPORT, mission.station:GetSystemBody().name },
780		mission.client_ship and { l.SHIP, mission.client_ship.label } or false,
781		false,
782		{ l.DEADLINE, ui.Format.Date(mission.due) }
783	}
784
785	desc.client = mission.client
786	desc.location = mission.destination or nil
787	if mission.deliver_to_ship then
788		desc.returnLocation = mission.client_ship or mission.station
789	end
790
791	return desc
792end
793
794local loaded_data
795
796local onGameStart = function ()
797	ads = {}
798	missions = {}
799
800	if loaded_data and loaded_data.ads then
801
802		for k, ad in pairs(loaded_data.ads) do
803			local ref = ad.station:AddAdvert({
804				description = ad.desc,
805				icon        = ad.id == "RESCUE" and "searchrescue" or "haul",
806				onChat      = onChat,
807				onDelete    = onDelete,
808				isEnabled   = isEnabled
809			})
810			ads[ref] = ad
811		end
812
813		missions = loaded_data.missions
814
815		loaded_data = nil
816
817		for ref, mission in pairs(missions) do
818			if mission.deliver_to_ship then
819				mission.in_progress = true
820				transferCargo(mission, ref)
821			end
822		end
823	end
824end
825
826local onGameEnd = function ()
827	planets = nil
828	flavours[LEGAL].cargo_type = nil
829	flavours[ILLEGAL].cargo_type = nil
830end
831
832local serialize = function ()
833	return { ads = ads, missions = missions }
834end
835
836local unserialize = function (data)
837	loaded_data = data
838end
839
840Event.Register("onCreateBB", onCreateBB)
841Event.Register("onUpdateBB", onUpdateBB)
842Event.Register("onShipEquipmentChanged", onShipEquipmentChanged)
843Event.Register("onShipDocked", onShipDocked)
844Event.Register("onShipUndocked", onShipUndocked)
845Event.Register("onShipHit", onShipHit)
846Event.Register("onShipDestroyed", onShipDestroyed)
847Event.Register("onJettison", onJettison)
848Event.Register("onCargoDestroyed", onCargoDestroyed)
849Event.Register("onEnterSystem", onEnterSystem)
850Event.Register("onLeaveSystem", onLeaveSystem)
851Event.Register("onGameStart", onGameStart)
852Event.Register("onGameEnd", onGameEnd)
853Event.Register("onReputationChanged", onReputationChanged)
854
855Mission.RegisterType("Scoop", l.SCOOP, buildMissionDescription)
856
857Serializer:Register("Scoop", serialize, unserialize)
858