using System.Threading;
using Hjg.Pngcs;
-
+using Mono.Collections.Generic;
using ProtoBuf;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
+using Vintagestory.API.Common.Entities;
using Vintagestory.API.Config;
using Vintagestory.API.Datastructures;
using Vintagestory.API.MathTools;
public class AutomapSystem
{
private Thread cartographer_thread;
- private Thread snapshotThread;
+
private Snapshotter snapshot;
private ICoreClientAPI ClientAPI { get; set; }
private ILogger Logger { get; set; }
private static Regex chunkShardRegex = new Regex(@"(?<X>[\d]+)_(?<Z>[\d]+)\.png", RegexOptions.Singleline);
private ConcurrentDictionary<Vec2i, ColumnCounter> columnCounters = new ConcurrentDictionary<Vec2i, ColumnCounter>(3, 150);
+ private Queue<Vec2i> revisitChunkList = new Queue<Vec2i>();
private ColumnsMetadata chunkTopMetadata;
- private PointsOfInterest POIs = new PointsOfInterest();
- private EntitiesOfInterest EOIs = new EntitiesOfInterest();
- private Dictionary<int, ulong> blockTally;//TODO: TOTAL Road Tally
+ internal PointsOfInterest POIs = new PointsOfInterest();
+ internal EntitiesOfInterest EOIs = new EntitiesOfInterest();
internal Dictionary<int, BlockDesignator> BlockID_Designators { get; private set; }
internal Dictionary<AssetLocation, EntityDesignator> Entity_Designators { get; private set; }
//Listen on bus for commands
ClientAPI.Event.RegisterEventBusListener(CommandListener, 1.0, AutomapSystem.AutomapCommandEventKey);
-
-
- if (configuration.Autostart)
- {
- CurrentState = CommandType.Run;
- Logger.Debug("Autostart is Enabled.");
- }
-
}
IsBackground = true
};
- snapshot = new Snapshotter(path, chunkTopMetadata, chunkSize,ClientAPI.World.Seed );
- snapshotThread = new Thread(Snap)
- {
- Name = "Snapshot",
- Priority = ThreadPriority.Lowest,
- IsBackground = true
- };
+ ClientAPI.Event.RegisterGameTickListener(ThreadDecider, 6000);
- ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
+ if (configuration.Autostart) {
+ CurrentState = CommandType.Run;
+ }
}
private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
columnCounters.AddOrUpdate(topPosition,
new ColumnCounter(chunkSize, newOrEdit, chunkCoord),
(chkPos, chkChng) => chkChng.Update(chunkCoord, chunkSize, newOrEdit)
- );
-
+ );
}
- private void AwakenCartographer(float delayed)
+ /// <summary>
+ /// Cartographer Thread 'decider'
+ /// </summary>
+ /// <param name="delayed">called delay offset</param>
+ private void ThreadDecider(float delayed)
{
if (CurrentState == CommandType.Run && (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true))
{
-#if DEBUG
- Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
-#endif
+ #if DEBUG
+ Logger.VerboseDebug("ThreadDecider re-trigger from [{0}]", cartographer_thread.ThreadState);
+ #endif
if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted))
{
}
else if (CurrentState == CommandType.Snapshot)
{
- if (snapshotThread.ThreadState.HasFlag(ThreadState.Unstarted))
- {
- snapshotThread.Start();
- } else if (snapshotThread.ThreadState.HasFlag(ThreadState.WaitSleepJoin))
- {
- snapshotThread.Interrupt();
+ //Prepare for taking a snopshot
+ if (snapshot == null) {
+ snapshot = new Snapshotter(path, chunkTopMetadata, chunkSize, ClientAPI.World.Seed);
+ #if DEBUG
+ Logger.VerboseDebug("Starting new Snapshot: {0} Wx{1} Hx{2}", snapshot.fileName, snapshot.Width, snapshot.Height);
+ #endif
+ snapshot.Take( );
+ }
+ else if (snapshot != null && snapshot.Finished) {
+ #if DEBUG
+ Logger.VerboseDebug("COMPLETED Snapshot: {0} Wx{1} Hx{2}, taking {3}", snapshot.fileName, snapshot.Width, snapshot.Height, snapshot.Timer.Elapsed);
+ #endif
+ snapshot = null;
+ CurrentState = CommandType.Run;
}
}
-
}
uint updatedChunks = 0;
uint updatedPixels = 0;
- //-- Should dodge enumerator changing underfoot....at a cost.
- if (!columnCounters.IsEmpty)
- {
- var tempSet = columnCounters.ToArray().Where(cks => cks.Value.WeightedSum > editThreshold) .OrderByDescending(kvp => kvp.Value.WeightedSum);
+
+ //Revisit failed chunks first;
+ while (revisitChunkList.Count > 0)
+ {
+ var revisitCoord = revisitChunkList.Dequeue( );
+ var rv_mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(revisitCoord);
+
+ if (rv_mapChunk == null) continue;
+
+ ColumnMeta rv_chunkMeta;
+ if (chunkTopMetadata.Contains(revisitCoord))
+ {
+ rv_chunkMeta = chunkTopMetadata[revisitCoord];
+ #if DEBUG
+ Logger.VerboseDebug("(re)Loaded meta-chunk {0}", revisitCoord);
+ #endif
+ }
+ else
+ {
+ rv_chunkMeta = CreateColumnMetadata(revisitCoord, rv_mapChunk);
+ #if DEBUG
+ Logger.VerboseDebug("(re)Created meta-chunk {0}", revisitCoord);
+ #endif
+ }
+ //TODO: Double validation of....?
+ ProcessChunkBlocks(revisitCoord, rv_mapChunk, ref rv_chunkMeta);
+
+ ChunkRenderer.SetupPngImage(revisitCoord, path, _chunkPath, ref rv_chunkMeta);
+ ChunkRenderer.GenerateChunkPngShard(revisitCoord, rv_mapChunk, rv_chunkMeta, ref chunkTopMetadata, out updatedPixels);
+
+ if (updatedPixels > 0)
+ {
+ #if DEBUG
+ Logger.VerboseDebug("(re)Wrote top-chunk shard: ({0}), Pixels#:{2}", revisitCoord, updatedPixels);
+ #endif
+ updatedChunks++;
+ chunkTopMetadata.Update(rv_chunkMeta);
+ }
+ }//*********** REVISIT'd ******************
+
+ if (!columnCounters.IsEmpty)
+ {//Resulting snapshot keys mabey NON-UNIQUE!!! a *FEATURE* of ConcurrentDict !
+ var columnsSnapshot = columnCounters.Where(cks => cks.Value.WeightedSum > editThreshold).OrderByDescending(kvp => kvp.Value.WeightedSum).ToArray( );
+
UpdateEntityMetadata();
- foreach (var mostActiveCol in tempSet)
+ foreach (var mostActiveCol in columnsSnapshot)
{
var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
if (mapChunk == null)
- {
- //TODO: REVISIT THIS CHUNK!
+ {
+ #if DEBUG
Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
+ #endif
nullMapCount++;
columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem);
+ revisitChunkList.Enqueue(mostActiveCol.Key);
continue;
}
Logger.VerboseDebug("Created meta-chunk {0}", mostActiveCol.Key);
#endif
}
+
+ /********* Special Interlock with C.D. *************/
+ if (columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem)) {
ProcessChunkBlocks(mostActiveCol.Key, mapChunk, ref chunkMeta);
- mostActiveCol.Value.SetCutoff(chunkMeta.YMax / chunkSize);
+ ejectedItem.SetCutoff(chunkMeta.YMax / chunkSize);
ChunkRenderer.SetupPngImage(mostActiveCol.Key, path, _chunkPath, ref chunkMeta);
ChunkRenderer.GenerateChunkPngShard(mostActiveCol.Key, mapChunk, chunkMeta, ref chunkTopMetadata, out updatedPixels);
+ }
+ else {
+ #if DEBUG
+ Logger.Warning("Prevented duplicate processing of: {0}",mostActiveCol.Key );
+ #endif
+ }
if (updatedPixels > 0)
{
#endif
updatedChunks++;
chunkTopMetadata.Update(chunkMeta);
- columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem);
+ //columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem);
}
else
{
- columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem);
+ //columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem);
#if DEBUG
Logger.VerboseDebug("Un-painted chunk shard: ({0}) ", mostActiveCol.Key);
#endif
+ revisitChunkList.Enqueue(mostActiveCol.Key);
}
}
}
columnCounters.Clear( );
//Then sleep until interupted again, and repeat
-#if DEBUG
+ #if DEBUG
Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
-#endif
+ #endif
Thread.Sleep(Timeout.Infinite);
}
catch (ThreadInterruptedException)
{
-#if DEBUG
+ #if DEBUG
Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
-#endif
+ #endif
goto wake;
}
catch (ThreadAbortException)
{
-#if DEBUG
+ #if DEBUG
Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
-#endif
+ #endif
}
finally
{
-#if DEBUG
+ #if DEBUG
Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
-#endif
+ #endif
PersistPointsData();
Write_PlainMetadata( );
}
private void Snap()
{
snapshotTake:
-#if DEBUG
+ #if DEBUG
Logger.VerboseDebug("Snapshot started");
-#endif
+ #endif
try
{
snapshot.Take();
-#if DEBUG
+ #if DEBUG
Logger.VerboseDebug("Snapshot sleeping");
-#endif
+ #endif
CurrentState = CommandType.Run;
Thread.Sleep(Timeout.Infinite);
}
catch (ThreadInterruptedException)
{
-#if DEBUG
+ #if DEBUG
Logger.VerboseDebug("Snapshot intertupted");
-#endif
+ #endif
goto snapshotTake;
}
}
var airBlocksQuery = from airyBlock in ClientAPI.World.Blocks
where airyBlock.MatterState == EnumMatterState.Solid
- where airyBlock.BlockMaterial == EnumBlockMaterial.Plant || airyBlock.BlockMaterial == EnumBlockMaterial.Leaves
- where airyBlock.CollisionBoxes == null || airyBlock.CollisionBoxes.Length == 0 ||airyBlock.RainPermeable == true
- select airyBlock;
- //^^ 'Solid' phase - 'Plant' Blocks without any boundg box ? Except water...
- this.AiryIdCodes = airBlocksQuery.ToDictionary(aBlk => aBlk.BlockId, aBlk => aBlk.Code.Path);
-
- //Add special marker types for BlockID's of "Interest", overwrite colour, and method
- Reload_POI_Designators();
+ where airyBlock.BlockMaterial == EnumBlockMaterial.Plant || airyBlock.BlockMaterial == EnumBlockMaterial.Leaves
+ where airyBlock.CollisionBoxes == null || airyBlock.CollisionBoxes.Length == 0 || airyBlock.RainPermeable == true
+ select airyBlock;
+ //^^ 'Solid' phase - 'Plant' Blocks without any bounding-box; OR 'Invisible' shapes...
+ var invisibleBlocksQuery = from novisBlock in ClientAPI.World.Blocks
+ where novisBlock.Shape == null || novisBlock.Shape.Base.EndsWith(GlobalConstants.DefaultDomain, @"invisible") //Whaat! [ base: "block/basic/invisible" ]
+ select novisBlock;
+ this.AiryIdCodes = airBlocksQuery.Union(invisibleBlocksQuery).ToDictionary(aBlk => aBlk.BlockId, aBlk => aBlk.Code.Path);
+
+ #if DEBUG
+ foreach (var fluffBlock in AiryIdCodes) {
+ Logger.VerboseDebug("ID#\t{0}:\t{1} IGNORED", fluffBlock.Key, fluffBlock.Value);
+ }
+ Logger.VerboseDebug("Ignoring {0} blocks", AiryIdCodes.Count);
+ #endif
+
+ //Add special marker types for BlockID's of "Interest", overwrite colour, and method
+ Reload_POI_Designators();
}
private void Reload_POI_Designators()
{
- Logger.VerboseDebug("Connecting {0} Configured Block-Designators", configuration.BlockDesignators.Count);
+ uint poisSetup =0, eoiSetup = 0;
foreach (var designator in configuration.BlockDesignators)
{
+ if (designator.Enabled == false) continue;
var blockIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, designator.Pattern, designator.Material);
if (blockIDs.Count > 0) { Logger.VerboseDebug("Designator {0} has {1} associated blockIDs", designator.ToString(), blockIDs.Count); }
foreach (var entry in blockIDs)
{
BlockID_Designators.Add(entry.Key, designator);
+ poisSetup++;
}
}
this.ChunkRenderer.BlockID_Designators = BlockID_Designators;
+ Logger.VerboseDebug("Connected {0} IDs from {1} Block-Designators", poisSetup, configuration.BlockDesignators.Count );
- Logger.VerboseDebug("Connecting {0} Configured Entity-Designators", configuration.EntityDesignators.Count);
foreach (var designator in configuration.EntityDesignators)
{
+ if (designator.Enabled == false) continue;
//Get Variants first, from EntityTypes...better be populated!
var matched = ClientAPI.World.EntityTypes.FindAll(entp => entp.Code.BeginsWith(designator.Pattern.Domain, designator.Pattern.Path));
foreach (var match in matched)
- {
+ {
Logger.VerboseDebug("Linked Entity: {0} Designator: {1}", match.Code, designator);
this.Entity_Designators.Add(match.Code, designator);
+ eoiSetup++;
}
-
- //EntityProperties props = ClientAPI.World.GetEntityType(designator.Pattern);
}
-
+ Logger.VerboseDebug("Connected {0} IDs from {1} Entity-Designators", eoiSetup, configuration.EntityDesignators.Count);
}
if (this.POIs.Count > 0)
{
- using (var poiFile = File.OpenWrite(poiPath))
+ using (var poiFile = File.Open(poiPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
Serializer.Serialize<PointsOfInterest>(poiFile, this.POIs);
+ poiFile.Flush(true);
}
}
if (this.EOIs.Count > 0)
{
- using (var eoiFile = File.OpenWrite(eoiPath))
+ using (var eoiFile = File.Open(eoiPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
Serializer.Serialize<EntitiesOfInterest>(eoiFile, this.EOIs);
+ eoiFile.Flush(true);
}
}
mdWriter.WriteLine("WorldSeed {0}", ClientAPI.World.Seed);
mdWriter.WriteLine("PlayerChunkCoords {0:D} {1:D}", startChunkColumn.X, startChunkColumn.Y);
mdWriter.WriteLine("DefaultSpawnPos {0:D} {1:D} {2:D}", ClientAPI.World.DefaultSpawnPosition.AsBlockPos.X,ClientAPI.World.DefaultSpawnPosition.AsBlockPos.Y,ClientAPI.World.DefaultSpawnPosition.AsBlockPos.Z);
+ //mdWriter.WriteLine("CurrentPlayerSpawn", ClientAPI.World.Player.WorldData.EntityPlayer.);
mdWriter.WriteLine("ChunkSize {0}", chunkSize);
mdWriter.WriteLine("SeaLevel {0:D}", ClientAPI.World.SeaLevel);
mdWriter.WriteLine("WorldSize {0:D} {1:D} {2:D}", ClientAPI.World.BulkBlockAccessor.MapSizeX, ClientAPI.World.BulkBlockAccessor.MapSizeY,ClientAPI.World.BulkBlockAccessor.MapSizeZ);
mdWriter.WriteLine("AMVersion '{0}'", ClientAPI.Self().Info.Version);
mdWriter.WriteLine("PlayTime {0:F1}", ClientAPI.InWorldEllapsedMilliseconds / 1000);
mdWriter.WriteLine("GameDate {0}", ClientAPI.World.Calendar.PrettyDate());
+ mdWriter.WriteLine("Chunks {0:D}", chunkTopMetadata.Count);
+ mdWriter.WriteLine("Chunks Updated {0:D}", updatedChunksTotal);
+ mdWriter.WriteLine("Null Chunks {0:D}", nullChunkCount);
mdWriter.Flush( );
}
}
return data;
}
+ private ColumnMeta CreateColumnMetadata(Vec2i coords, IMapChunk mapChunk)
+ {
+ ColumnMeta data = new ColumnMeta(coords, ClientAPI, ( byte )chunkSize, (ClientAPI.World.BlockAccessor.MapSizeY / chunkSize));
+ BlockPos equivBP = new BlockPos(coords.X * chunkSize,
+ mapChunk.YMax,
+ coords.Y * chunkSize);
+
+ var climate = ClientAPI.World.BlockAccessor.GetClimateAt(equivBP);
+ data.UpdateFieldsFrom(climate, mapChunk, TimeSpan.FromHours(ClientAPI.World.Calendar.TotalHours));
+
+ return data;
+ }
+
/// <summary>
/// Reload chunk bounds from chunk shards
/// </summary>
{
WorldChunk worldChunk = ClientAPI.World.BlockAccessor.GetChunk(key.X, targetChunkY, key.Y) as WorldChunk;
- if (worldChunk == null || worldChunk.BlockEntities == null)
+ if (worldChunk == null || worldChunk.BlockEntities == null || worldChunk.Disposed)
{
#if DEBUG
- Logger.VerboseDebug("WORLD chunk: null or empty X{0} Y{1} Z{2} !", key.X, targetChunkY, key.Y);
+ Logger.VerboseDebug("WORLD chunk, null/disposed OR B.E. null: X{0} Y{1} Z{2} !", key.X, targetChunkY, key.Y);
#endif
nullChunkCount++;
continue;
if (worldChunk.IsPacked())
{
+ #if DEBUG
Logger.VerboseDebug("WORLD chunk: Compressed: X{0} Y{1} Z{2}", key.X, targetChunkY, key.Y);
- worldChunk.Unpack( );//RESEARCH: Thread Unsafe?
+ #endif
+ //VALIDATE: check Read-only applicable
+ if (worldChunk.Unpack_ReadOnly( ) == false)
+ {
+ Logger.Warning("Failed to unpack chunk: X{0} Y{1} Z{2}", key.X, targetChunkY, key.Y);
+ nullChunkCount++;
+ continue;
+ };
}
/*************** Chunk Entities Scanning *********************/
if (worldChunk.BlockEntities != null && worldChunk.BlockEntities.Count > 0)
{
#if DEBUG
- Logger.VerboseDebug("Scan pos.({0}) for BlockEntities# {1}", key, worldChunk.BlockEntities.Count);
+ Logger.VerboseDebug("Scan in Chunk:({0}, Y {2}): found {1} BlockEntities", key, worldChunk.BlockEntities.Count,targetChunkY);
#endif
foreach (var blockEnt in worldChunk.BlockEntities)
{
- if (blockEnt.Value != null && blockEnt.Value.Block != null && BlockID_Designators.ContainsKey(blockEnt.Value.Block.BlockId))
+ var blockEntityPos = blockEnt.Key;
+ var blockEntity = blockEnt.Value;
+ if (blockEntityPos == null || blockEntity == null ) continue;
+ if (blockEntity.Block != null && !blockEntity.Block.IsMissing && BlockID_Designators.ContainsKey(blockEntity.Block.BlockId))
{
var designator = BlockID_Designators[blockEnt.Value.Block.BlockId];
- designator.SpecialAction(ClientAPI, POIs, blockEnt.Value.Pos.Copy(), blockEnt.Value.Block);
+ if (designator != null && designator.SpecialAction != null) designator.SpecialAction(ClientAPI, POIs, blockEnt.Value.Pos.Copy(), blockEnt.Value.Block);
}
}
}
int X_index, Y_index, Z_index;
//First Chance fail-safe;
- if (worldChunk.Blocks == null || worldChunk.Blocks.Length <= 0) {
+ if (worldChunk.MaybeBlocks == null || worldChunk.MaybeBlocks.Length <= 0) {
+ #if DEBUG
Logger.VerboseDebug("WORLD chunk; Missing block DATA⁈ X{0} Y{1} Z{2} ⁈", key.X, targetChunkY, key.Y);
+ #endif
nullChunkCount++;
continue;
}
var indicie = MapUtil.Index3d(X_index, Y_index, Z_index, chunkSize, chunkSize);
//'Last' Chance fail-safe;
- if (worldChunk.Blocks == null || worldChunk.Blocks.Length <= 0) {
+ if (worldChunk.MaybeBlocks == null || worldChunk.MaybeBlocks.Length <= 0) {
+ #if DEBUG
Logger.VerboseDebug("Processing Block: Missing block DATA⁈ X{0} Y{1} Z{2} ⁈", X_index, Y_index, Z_index);
+ #endif
nullChunkCount++;
- goto loop_bustout; ;
+ goto loop_bustout;
}
- int aBlockId = worldChunk.Blocks[indicie];
+ int aBlockId = worldChunk.MaybeBlocks[indicie];
if (aBlockId == 0 || AiryIdCodes.ContainsKey(aBlockId)) {//Airy blocks,,,
chunkMeta.AirBlocks++;
}
loop_bustout:;
}
+ #if DEBUG
Logger.VerboseDebug("COLUMN X{0} Z{1}: {2}, processed.", key.X , key.Y, chunkTally + 1);
+ #endif
}
private void UpdateEntityMetadata()
{
- Logger.Debug("Presently {0} Entities", ClientAPI.World.LoadedEntities.Count);
- //Mabey scan only for 'new' entities by tracking ID in set?
- foreach (var loadedEntity in ClientAPI.World.LoadedEntities.ToArray())
- {
+ #if DEBUG
+ Logger.Debug("Presently {0} Entities", ClientAPI.World.LoadedEntities.Count);
+ #endif
+
+ var keyList = new long[ClientAPI.World.LoadedEntities.Keys.Count];
+ ClientAPI.World.LoadedEntities.Keys.CopyTo(keyList, 0);
+ //'ElementAt'; worse! instead; walk fixed list...
+ Entity loadedEntity;
+ foreach (var key in keyList)
+ {
+ if (ClientAPI.World.LoadedEntities.TryGetValue(key, out loadedEntity))
+ {
#if DEBUG
//Logger.VerboseDebug($"ENTITY: ({loadedEntity.Value.Code}) = #{loadedEntity.Value.EntityId} {loadedEntity.Value.State} {loadedEntity.Value.LocalPos} <<<<<<<<<<<<");
#endif
- var dMatch = Entity_Designators.SingleOrDefault(se => se.Key.Equals(loadedEntity.Value.Code));
- if (dMatch.Value != null)
- {
- dMatch.Value.SpecialAction(ClientAPI, this.EOIs, loadedEntity.Value.Pos.AsBlockPos.Copy(), loadedEntity.Value);
- }
-
+ var dMatch = Entity_Designators.SingleOrDefault(se => se.Key.Equals(loadedEntity.Code));
+ if (dMatch.Value != null)
+ {
+ dMatch.Value.SpecialAction(ClientAPI, this.EOIs, loadedEntity.Pos.AsBlockPos.Copy( ), loadedEntity);
+ }
+ }
}
-
-
}
private void AddNote(string notation)
if (CurrentState != cmdData.State)
{
CurrentState = cmdData.State;
- AwakenCartographer(0.0f);
+ ThreadDecider(0.0f);
}
break;
AddNote(cmdData.Notation);
break;
}
- #if DEBUG
+
ClientAPI.TriggerChatMessage($"Automap commanded to: {cmdData.State} ");
- #endif
+
}
- #endregion
+#endregion
private AChunkRenderer InstantiateChosenRenderer(string rendererName )
{
}
return null;
- }
+ }
}
}