source: ps/trunk/binaries/data/mods/public/simulation/components/Player.js

Last change on this file was 27722, checked in by Freagarach, 13 months ago

Pull Diplomacy out of cmpPlayer.

Who says only players should be able to conduct diplomacy?
Also separation of concerns, more maintainable files.

Differential revision: https://code.wildfiregames.com/D4921
Comments by: @elexis, @Stan
Refs. #5894

  • Property svn:eol-style set to native
  • Property svn:mime-type set to text/plain
File size: 19.7 KB
Line 
1function Player() {}
2
3Player.prototype.Schema =
4 "<element name='BarterMultiplier' a:help='Multipliers for barter prices.'>" +
5 "<interleave>" +
6 "<element name='Buy' a:help='Multipliers for the buy prices.'>" +
7 Resources.BuildSchema("positiveDecimal") +
8 "</element>" +
9 "<element name='Sell' a:help='Multipliers for the sell prices.'>" +
10 Resources.BuildSchema("positiveDecimal") +
11 "</element>" +
12 "</interleave>" +
13 "</element>" +
14 "<element name='Formations' a:help='Space-separated list of formations this player can use.'>" +
15 "<attribute name='datatype'>" +
16 "<value>tokens</value>" +
17 "</attribute>" +
18 "<text/>" +
19 "</element>" +
20 "<element name='SpyCostMultiplier'>" +
21 "<ref name='nonNegativeDecimal'/>" +
22 "</element>";
23
24// The GUI expects these strings.
25Player.prototype.STATE_ACTIVE = "active";
26Player.prototype.STATE_DEFEATED = "defeated";
27Player.prototype.STATE_WON = "won";
28
29Player.prototype.Serialize = function()
30{
31 let state = {};
32 for (let key in this)
33 if (this.hasOwnProperty(key))
34 state[key] = this[key];
35
36 // Modified by GUI, so don't serialise.
37 delete state.displayDiplomacyColor;
38 return state;
39};
40
41Player.prototype.Deserialize = function(state)
42{
43 for (let prop in state)
44 this[prop] = state[prop];
45};
46
47/**
48 * Which units will be shown with special icons at the top.
49 */
50var panelEntityClasses = "Hero Relic";
51
52Player.prototype.Init = function()
53{
54 this.playerID = undefined;
55 this.color = undefined;
56 this.popUsed = 0; // Population of units owned or trained by this player.
57 this.popBonuses = 0; // Sum of population bonuses of player's entities.
58 this.maxPop = 300; // Maximum population.
59 this.trainingBlocked = false; // Indicates whether any training queue is currently blocked.
60 this.resourceCount = {};
61 this.resourceGatherers = {};
62 this.tradingGoods = []; // Goods for next trade-route and its probabilities * 100.
63 this.state = this.STATE_ACTIVE;
64 this.formations = this.template.Formations._string.split(" ");
65 this.startCam = undefined;
66 this.controlAllUnits = false;
67 this.isAI = false;
68 this.cheatsEnabled = false;
69 this.panelEntities = [];
70 this.resourceNames = {};
71 this.disabledTemplates = {};
72 this.disabledTechnologies = {};
73 this.spyCostMultiplier = +this.template.SpyCostMultiplier;
74 this.barterEntities = [];
75 this.barterMultiplier = {
76 "buy": clone(this.template.BarterMultiplier.Buy),
77 "sell": clone(this.template.BarterMultiplier.Sell)
78 };
79
80 // Initial resources.
81 let resCodes = Resources.GetCodes();
82 for (let res of resCodes)
83 {
84 this.resourceCount[res] = 300;
85 this.resourceNames[res] = Resources.GetResource(res).name;
86 this.resourceGatherers[res] = 0;
87 }
88 // Trading goods probability in steps of 5.
89 let resTradeCodes = Resources.GetTradableCodes();
90 let quotient = Math.floor(20 / resTradeCodes.length);
91 let remainder = 20 % resTradeCodes.length;
92 for (let i in resTradeCodes)
93 this.tradingGoods.push({
94 "goods": resTradeCodes[i],
95 "proba": 5 * (quotient + (+i < remainder ? 1 : 0))
96 });
97};
98
99Player.prototype.SetPlayerID = function(id)
100{
101 this.playerID = id;
102};
103
104Player.prototype.GetPlayerID = function()
105{
106 return this.playerID;
107};
108
109Player.prototype.SetColor = function(r, g, b)
110{
111 let colorInitialized = !!this.color;
112
113 this.color = { "r": r / 255, "g": g / 255, "b": b / 255, "a": 1 };
114
115 // Used in Atlas.
116 if (colorInitialized)
117 Engine.BroadcastMessage(MT_PlayerColorChanged, {
118 "player": this.playerID
119 });
120};
121
122Player.prototype.SetDisplayDiplomacyColor = function(displayDiplomacyColor)
123{
124 this.displayDiplomacyColor = displayDiplomacyColor;
125};
126
127Player.prototype.GetColor = function()
128{
129 return this.color;
130};
131
132Player.prototype.GetDisplayedColor = function()
133{
134 return this.displayDiplomacyColor ? Engine.QueryInterface(this.entity, IID_Diplomacy).GetColor() : this.color;
135};
136
137// Try reserving num population slots. Returns 0 on success or number of missing slots otherwise.
138Player.prototype.TryReservePopulationSlots = function(num)
139{
140 if (num != 0 && num > (this.GetPopulationLimit() - this.popUsed))
141 return num - (this.GetPopulationLimit() - this.popUsed);
142
143 this.popUsed += num;
144 return 0;
145};
146
147Player.prototype.UnReservePopulationSlots = function(num)
148{
149 this.popUsed -= num;
150};
151
152Player.prototype.GetPopulationCount = function()
153{
154 return this.popUsed;
155};
156
157Player.prototype.AddPopulation = function(num)
158{
159 this.popUsed += num;
160};
161
162Player.prototype.SetPopulationBonuses = function(num)
163{
164 this.popBonuses = num;
165};
166
167Player.prototype.AddPopulationBonuses = function(num)
168{
169 this.popBonuses += num;
170};
171
172Player.prototype.GetPopulationLimit = function()
173{
174 return Math.min(this.GetMaxPopulation(), this.popBonuses);
175};
176
177Player.prototype.SetMaxPopulation = function(max)
178{
179 this.maxPop = max;
180};
181
182Player.prototype.GetMaxPopulation = function()
183{
184 return Math.round(ApplyValueModificationsToEntity("Player/MaxPopulation", this.maxPop, this.entity));
185};
186
187Player.prototype.CanBarter = function()
188{
189 return this.barterEntities.length > 0;
190};
191
192Player.prototype.GetBarterMultiplier = function()
193{
194 return this.barterMultiplier;
195};
196
197Player.prototype.GetSpyCostMultiplier = function()
198{
199 return this.spyCostMultiplier;
200};
201
202Player.prototype.GetPanelEntities = function()
203{
204 return this.panelEntities;
205};
206
207Player.prototype.IsTrainingBlocked = function()
208{
209 return this.trainingBlocked;
210};
211
212Player.prototype.BlockTraining = function()
213{
214 this.trainingBlocked = true;
215};
216
217Player.prototype.UnBlockTraining = function()
218{
219 this.trainingBlocked = false;
220};
221
222Player.prototype.SetResourceCounts = function(resources)
223{
224 for (let res in resources)
225 this.resourceCount[res] = resources[res];
226};
227
228Player.prototype.GetResourceCounts = function()
229{
230 return this.resourceCount;
231};
232
233Player.prototype.GetResourceGatherers = function()
234{
235 return this.resourceGatherers;
236};
237
238/**
239 * @param {string} type - The generic type of resource to add the gatherer for.
240 */
241Player.prototype.AddResourceGatherer = function(type)
242{
243 ++this.resourceGatherers[type];
244};
245
246/**
247 * @param {string} type - The generic type of resource to remove the gatherer from.
248 */
249Player.prototype.RemoveResourceGatherer = function(type)
250{
251 --this.resourceGatherers[type];
252};
253
254/**
255 * Add resource of specified type to player.
256 * @param {string} type - Generic type of resource.
257 * @param {number} amount - Amount of resource, which should be added.
258 */
259Player.prototype.AddResource = function(type, amount)
260{
261 this.resourceCount[type] += +amount;
262};
263
264/**
265 * Add resources to player.
266 */
267Player.prototype.AddResources = function(amounts)
268{
269 for (let type in amounts)
270 this.resourceCount[type] += +amounts[type];
271};
272
273Player.prototype.GetNeededResources = function(amounts)
274{
275 // Check if we can afford it all.
276 let amountsNeeded = {};
277 for (let type in amounts)
278 if (this.resourceCount[type] != undefined && amounts[type] > this.resourceCount[type])
279 amountsNeeded[type] = amounts[type] - Math.floor(this.resourceCount[type]);
280
281 if (Object.keys(amountsNeeded).length == 0)
282 return undefined;
283 return amountsNeeded;
284};
285
286Player.prototype.SubtractResourcesOrNotify = function(amounts)
287{
288 let amountsNeeded = this.GetNeededResources(amounts);
289
290 // If we don't have enough resources, send a notification to the player.
291 if (amountsNeeded)
292 {
293 let parameters = {};
294 let i = 0;
295 for (let type in amountsNeeded)
296 {
297 ++i;
298 parameters["resourceType" + i] = this.resourceNames[type];
299 parameters["resourceAmount" + i] = amountsNeeded[type];
300 }
301
302 let msg = "";
303 // When marking strings for translations, you need to include the actual string,
304 // not some way to derive the string.
305 if (i < 1)
306 warn("Amounts needed but no amounts given?");
307 else if (i == 1)
308 msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s");
309 else if (i == 2)
310 msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s");
311 else if (i == 3)
312 msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s");
313 else if (i == 4)
314 msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s, %(resourceAmount4)s %(resourceType4)s");
315 else
316 warn("Localisation: Strings are not localised for more than 4 resources");
317
318 // Send as time-notification.
319 let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
320 cmpGUIInterface.PushNotification({
321 "players": [this.playerID],
322 "message": msg,
323 "parameters": parameters,
324 "translateMessage": true,
325 "translateParameters": {
326 "resourceType1": "withinSentence",
327 "resourceType2": "withinSentence",
328 "resourceType3": "withinSentence",
329 "resourceType4": "withinSentence"
330 }
331 });
332 return false;
333 }
334
335 for (let type in amounts)
336 this.resourceCount[type] -= amounts[type];
337
338 return true;
339};
340
341Player.prototype.TrySubtractResources = function(amounts)
342{
343 if (!this.SubtractResourcesOrNotify(amounts))
344 return false;
345
346 let cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker);
347 if (cmpStatisticsTracker)
348 for (let type in amounts)
349 cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]);
350
351 return true;
352};
353
354Player.prototype.RefundResources = function(amounts)
355{
356 const cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker);
357 if (cmpStatisticsTracker)
358 for (const type in amounts)
359 cmpStatisticsTracker.IncreaseResourceUsedCounter(type, -amounts[type]);
360
361 this.AddResources(amounts);
362};
363
364Player.prototype.GetNextTradingGoods = function()
365{
366 let value = randFloat(0, 100);
367 let last = this.tradingGoods.length - 1;
368 let sumProba = 0;
369 for (let i = 0; i < last; ++i)
370 {
371 sumProba += this.tradingGoods[i].proba;
372 if (value < sumProba)
373 return this.tradingGoods[i].goods;
374 }
375 return this.tradingGoods[last].goods;
376};
377
378Player.prototype.GetTradingGoods = function()
379{
380 let tradingGoods = {};
381 for (let resource of this.tradingGoods)
382 tradingGoods[resource.goods] = resource.proba;
383
384 return tradingGoods;
385};
386
387Player.prototype.SetTradingGoods = function(tradingGoods)
388{
389 let resTradeCodes = Resources.GetTradableCodes();
390 let sumProba = 0;
391 for (let resource in tradingGoods)
392 {
393 if (resTradeCodes.indexOf(resource) == -1 || tradingGoods[resource] < 0)
394 {
395 error("Invalid trading goods: " + uneval(tradingGoods));
396 return;
397 }
398 sumProba += tradingGoods[resource];
399 }
400
401 if (sumProba != 100)
402 {
403 error("Invalid trading goods probability: " + uneval(sumProba));
404 return;
405 }
406
407 this.tradingGoods = [];
408 for (let resource in tradingGoods)
409 this.tradingGoods.push({
410 "goods": resource,
411 "proba": tradingGoods[resource]
412 });
413};
414
415/**
416 * @param {string} message - The message to send in the chat. May be undefined.
417 */
418Player.prototype.Win = function(message)
419{
420 this.SetState(this.STATE_WON, message);
421};
422
423/**
424 * @param {string} message - The message to send in the chat. May be undefined.
425 */
426Player.prototype.Defeat = function(message)
427{
428 this.SetState(this.STATE_DEFEATED, message);
429};
430
431/**
432 * @return {string} - The string identified with the current state.
433 */
434Player.prototype.GetState = function()
435{
436 return this.state;
437};
438
439/**
440 * @return {boolean} -
441 */
442Player.prototype.IsActive = function()
443{
444 return this.state === this.STATE_ACTIVE;
445};
446
447/**
448 * @return {boolean} -
449 */
450Player.prototype.IsDefeated = function()
451{
452 return this.state === this.STATE_DEFEATED;
453};
454
455/**
456 * @return {boolean} -
457 */
458Player.prototype.HasWon = function()
459{
460 return this.state === this.STATE_WON;
461};
462
463/**
464 * @param {string} newState - Either "defeated" or "won".
465 * @param {string|undefined} message - A string to be shown in chat, for example
466 * markForTranslation("%(player)s has been defeated (failed objective).").
467 * If it is undefined, the caller MUST send that GUI notification manually.
468 */
469Player.prototype.SetState = function(newState, message)
470{
471 if (!this.IsActive())
472 return;
473
474 if (newState != this.STATE_WON && newState != this.STATE_DEFEATED)
475 {
476 warn("Can't change playerstate to " + newState);
477 return;
478 }
479
480 if (!this.playerID)
481 {
482 warn("Gaia can't change state.");
483 return;
484 }
485
486 this.state = newState;
487
488 const won = this.HasWon();
489 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
490 if (won)
491 cmpRangeManager.SetLosRevealAll(this.playerID, true);
492 else
493 {
494 // Reassign all player's entities to Gaia.
495 let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID);
496
497 // The ownership change is done in two steps so that entities don't hit idle
498 // (and thus possibly look for "enemies" to attack) before nearby allies get
499 // converted to Gaia as well.
500 for (let entity of entities)
501 {
502 let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership);
503 cmpOwnership.SetOwnerQuiet(0);
504 }
505
506 // With the real ownership change complete, send OwnershipChanged messages.
507 for (let entity of entities)
508 Engine.PostMessage(entity, MT_OwnershipChanged, {
509 "entity": entity,
510 "from": this.playerID,
511 "to": 0
512 });
513 }
514
515 if (message)
516 Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
517 "type": won ? "won" : "defeat",
518 "players": [this.playerID],
519 "allies": [this.playerID],
520 "message": message
521 });
522
523 Engine.PostMessage(this.entity, won ? MT_PlayerWon : MT_PlayerDefeated, { "playerId": this.playerID });
524};
525
526Player.prototype.GetFormations = function()
527{
528 return this.formations;
529};
530
531Player.prototype.SetFormations = function(formations)
532{
533 this.formations = formations;
534};
535
536Player.prototype.GetStartingCameraPos = function()
537{
538 return this.startCam.position;
539};
540
541Player.prototype.GetStartingCameraRot = function()
542{
543 return this.startCam.rotation;
544};
545
546Player.prototype.SetStartingCamera = function(pos, rot)
547{
548 this.startCam = { "position": pos, "rotation": rot };
549};
550
551Player.prototype.HasStartingCamera = function()
552{
553 return this.startCam !== undefined;
554};
555
556Player.prototype.SetControlAllUnits = function(c)
557{
558 this.controlAllUnits = c;
559};
560
561Player.prototype.CanControlAllUnits = function()
562{
563 return this.controlAllUnits;
564};
565
566Player.prototype.SetAI = function(flag)
567{
568 this.isAI = flag;
569};
570
571Player.prototype.IsAI = function()
572{
573 return this.isAI;
574};
575
576/**
577 * Do some map dependant initializations
578 */
579Player.prototype.OnGlobalInitGame = function(msg)
580{
581 // Replace the "{civ}" code with this civ ID.
582 let disabledTemplates = this.disabledTemplates;
583 this.disabledTemplates = {};
584 const civ = Engine.QueryInterface(this.entity, IID_Identity).GetCiv();
585 for (let template in disabledTemplates)
586 if (disabledTemplates[template])
587 this.disabledTemplates[template.replace(/\{civ\}/g, civ)] = true;
588};
589
590/**
591 * Keep track of population effects of all entities that
592 * become owned or unowned by this player.
593 */
594Player.prototype.OnGlobalOwnershipChanged = function(msg)
595{
596 if (msg.from != this.playerID && msg.to != this.playerID)
597 return;
598
599 let cmpCost = Engine.QueryInterface(msg.entity, IID_Cost);
600
601 if (msg.from == this.playerID)
602 {
603 if (cmpCost)
604 this.popUsed -= cmpCost.GetPopCost();
605
606 let panelIndex = this.panelEntities.indexOf(msg.entity);
607 if (panelIndex >= 0)
608 this.panelEntities.splice(panelIndex, 1);
609
610 let barterIndex = this.barterEntities.indexOf(msg.entity);
611 if (barterIndex >= 0)
612 this.barterEntities.splice(barterIndex, 1);
613 }
614 if (msg.to == this.playerID)
615 {
616 if (cmpCost)
617 this.popUsed += cmpCost.GetPopCost();
618
619 let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
620 if (!cmpIdentity)
621 return;
622
623 if (MatchesClassList(cmpIdentity.GetClassesList(), panelEntityClasses))
624 this.panelEntities.push(msg.entity);
625
626 if (cmpIdentity.HasClass("Barter") && !Engine.QueryInterface(msg.entity, IID_Foundation))
627 this.barterEntities.push(msg.entity);
628 }
629};
630
631Player.prototype.OnValueModification = function(msg)
632{
633 if (msg.component != "Player")
634 return;
635
636 if (msg.valueNames.indexOf("Player/SpyCostMultiplier") != -1)
637 this.spyCostMultiplier = ApplyValueModificationsToEntity("Player/SpyCostMultiplier", +this.template.SpyCostMultiplier, this.entity);
638
639 if (msg.valueNames.some(mod => mod.startsWith("Player/BarterMultiplier/")))
640 for (let res in this.template.BarterMultiplier.Buy)
641 {
642 this.barterMultiplier.buy[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Buy/"+res, +this.template.BarterMultiplier.Buy[res], this.entity);
643 this.barterMultiplier.sell[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Sell/"+res, +this.template.BarterMultiplier.Sell[res], this.entity);
644 }
645};
646
647Player.prototype.SetCheatsEnabled = function(flag)
648{
649 this.cheatsEnabled = flag;
650};
651
652Player.prototype.GetCheatsEnabled = function()
653{
654 return this.cheatsEnabled;
655};
656
657Player.prototype.TributeResource = function(player, amounts)
658{
659 let cmpPlayer = QueryPlayerIDInterface(player);
660 if (!cmpPlayer)
661 return;
662
663 if (!this.IsActive() || !cmpPlayer.IsActive())
664 return;
665
666 let resTribCodes = Resources.GetTributableCodes();
667 for (let resCode in amounts)
668 if (resTribCodes.indexOf(resCode) == -1 ||
669 !Number.isInteger(amounts[resCode]) ||
670 amounts[resCode] < 0)
671 {
672 warn("Invalid tribute amounts: " + uneval(resCode) + ": " + uneval(amounts));
673 return;
674 }
675
676 if (!this.SubtractResourcesOrNotify(amounts))
677 return;
678 cmpPlayer.AddResources(amounts);
679
680 let total = Object.keys(amounts).reduce((sum, type) => sum + amounts[type], 0);
681 let cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker);
682 if (cmpOurStatisticsTracker)
683 cmpOurStatisticsTracker.IncreaseTributesSentCounter(total);
684 let cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker);
685 if (cmpTheirStatisticsTracker)
686 cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total);
687
688 let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
689 if (cmpGUIInterface)
690 cmpGUIInterface.PushNotification({
691 "type": "tribute",
692 "players": [player],
693 "donator": this.playerID,
694 "amounts": amounts
695 });
696
697 Engine.BroadcastMessage(MT_TributeExchanged, {
698 "to": player,
699 "from": this.playerID,
700 "amounts": amounts
701 });
702};
703
704Player.prototype.AddDisabledTemplate = function(template)
705{
706 this.disabledTemplates[template] = true;
707 Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID });
708};
709
710Player.prototype.RemoveDisabledTemplate = function(template)
711{
712 this.disabledTemplates[template] = false;
713 Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID });
714};
715
716Player.prototype.SetDisabledTemplates = function(templates)
717{
718 this.disabledTemplates = {};
719 for (let template of templates)
720 this.disabledTemplates[template] = true;
721 Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID });
722};
723
724Player.prototype.GetDisabledTemplates = function()
725{
726 return this.disabledTemplates;
727};
728
729Player.prototype.AddDisabledTechnology = function(tech)
730{
731 this.disabledTechnologies[tech] = true;
732 Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID });
733};
734
735Player.prototype.RemoveDisabledTechnology = function(tech)
736{
737 this.disabledTechnologies[tech] = false;
738 Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID });
739};
740
741Player.prototype.SetDisabledTechnologies = function(techs)
742{
743 this.disabledTechnologies = {};
744 for (let tech of techs)
745 this.disabledTechnologies[tech] = true;
746 Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID });
747};
748
749Player.prototype.GetDisabledTechnologies = function()
750{
751 return this.disabledTechnologies;
752};
753
754Player.prototype.OnGlobalPlayerDefeated = function(msg)
755{
756 let cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
757 if (!cmpSound)
758 return;
759
760 const soundGroup = cmpSound.GetSoundGroup(this.playerID === msg.playerId ? "defeated" : Engine.QueryInterface(this.entity, IID_Diplomacy).IsAlly(msg.playerId) ? "defeated_ally" : this.HasWon() ? "won" : "defeated_enemy");
761 if (soundGroup)
762 Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, this.playerID);
763};
764
765Engine.RegisterComponentType(IID_Player, "Player", Player);
Note: See TracBrowser for help on using the repository browser.