public static BackgroundInfo GetBgInfo(GMFileContent content, uint id) { if (id >= content.Backgrounds->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var ret = new BackgroundInfo(); var be = (BgEntry *)GMFile.PtrFromOffset(content, (&content.Backgrounds->Offsets)[id]); ret.Name = StringFromOffset(content, be->Name); ret.TexPageIndex = be->TextureOffset; for (uint i = 0; i < content.TexturePages->Count; i++) { if (be->TextureOffset == (&content.TexturePages->Offsets)[i]) { ret.TexPageIndex = i; break; } } return(ret); }
public static PathInfo GetPathInfo(GMFileContent content, uint id) { if (id >= content.Paths->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var curOff = (&content.Paths->Offsets)[id]; var pe = (PathEntry *)GMFile.PtrFromOffset(content, curOff); var ret = new PathInfo(); ret.Name = StringFromOffset(content, pe->Name); ret.IsSmooth = pe->IsSmooth.IsTrue(); ret.IsClosed = pe->IsClosed.IsTrue(); ret.Precision = pe->Precision; ret.Points = new PathPoint[pe->PointCount]; for (uint i = 0; i < pe->PointCount; i++) { ret.Points[i] = (&pe->Points)[i]; } return(ret); }
public static byte[][] ListToByteArrays(GMFileContent content, SectionCountOffsets *list, long elemLen = 0) { var ret = new byte[list->Count][]; for (uint i = 0; i < list->Count; i++) { var curOff = (&list->Offsets)[i]; var nextOff = i == list->Count - 1L ? list->Header.Size - 4L : (&list->Offsets)[i + 1]; var curPtr = (byte *)GMFile.PtrFromOffset(content, curOff); var len = elemLen <= 0L ? ((byte *)GMFile.PtrFromOffset(content, nextOff) - curPtr) : elemLen; if (len < 0L && elemLen < 0L) { len = -elemLen; } var data = new byte[len]; Marshal.Copy((IntPtr)curPtr, data, 0, (int)len); ret[i] = data; } return(ret); }
public static SpriteInfo GetSpriteInfo(GMFileContent content, uint id) { if (id >= content.Sprites->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var se = (SpriteEntry *)GMFile.PtrFromOffset(content, (&content.Sprites->Offsets)[id]); var ret = new SpriteInfo(); ret.Name = StringFromOffset(content, se->Name); ret.Size = se->Size; ret.Bounding = se->Bounding; ret.BBoxMode = se->BBoxMode; ret.SepMasks = se->SepMasks; ret.Origin = se->Origin; ret.TextureIndices = new uint[se->Textures.Count]; for (uint i = 0; i < se->Textures.Count; i++) { for (uint j = 0; j < content.TexturePages->Count; j++) { if ((&se->Textures.Offsets)[i] == (&content.TexturePages->Offsets)[j]) { ret.TextureIndices[i] = j; break; } } } return(ret); }
public static SoundInfo GetSoundInfo(GMFileContent content, uint id) { if (id >= content.Sounds->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var se = (SoundEntry *)GMFile.PtrFromOffset(content, (&content.Sounds->Offsets)[id]); var ret = new SoundInfo(); ret.Name = StringFromOffset(content, se->NameOffset); ret.Type = StringFromOffset(content, se->TypeOffset); ret.File = StringFromOffset(content, se->FileOffset); ret.VolumeMod = se->Volume; ret.PitchMod = se->Pitch; ret.Group = se->GroupID < 0 ? null : GetAudioGroupInfo(content, (uint)se->GroupID); ret.GroupID = se->GroupID; ret.AudioID = se->AudioID; ret.IsEmbedded = (se->Flags & SoundEntryFlags.Embedded) != 0; ret.IsCompressed = (se->Flags & SoundEntryFlags.Compressed) != 0; return(ret); }
public static Dictionary <IntPtr, int> GetReferenceTable(GMFileContent content, ReferenceDef[] defs) { var ret = new Dictionary <IntPtr, int>(defs.Length); for (int i = 0; i < defs.Length; i++) { var offTotal = (long)defs[i].FirstOffset; var addr = (AnyInstruction *)GMFile.PtrFromOffset(content, offTotal); if (defs[i].Occurrences != 0) { defs[i].VariableType = (VariableType)(((uint *)addr)[1] >> 24); } for (int j = 0; j < defs[i].Occurrences /*&& curOffset != 0*/; j++) { ret.Add((IntPtr)addr, i); if (j < defs[i].Occurrences - 1) // at least one more iteration afterwards { var off = ((uint *)addr)[1] & 0x00FFFFFFL; addr = (AnyInstruction *)GMFile.PtrFromOffset(content, offTotal += off); //! '+=', not '+' } } } return(ret); }
public static ShaderInfo GetShaderInfo(GMFileContent c, uint id) { if (id >= c.Shaders->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var sh = (ShaderEntry *)GMFile.PtrFromOffset(c, (&c.Shaders->Offsets)[id]); var si = new ShaderInfo(); si.Name = StringFromOffset(c, sh->Name); si.Type = sh->Type.Decode(); si.Code.GLSL_ES = GetVxFxStrings(c, sh->GLSL_ES); si.Code.GLSL = GetVxFxStrings(c, sh->GLSL); si.Code.HLSL9 = GetVxFxStrings(c, sh->HLSL9); ShaderEntry2 *sh2; // hack for a special case w. pre-DX11 stuff? if ((uint)c.General->BytecodeVersion == 0xE && sh->HLSL11.VertexData == 0 && sh->HLSL11.VertexLength == 0) { var sho = (ShaderEntryOld *)sh; var ats = new string[sho->AttributeCount]; for (uint i = 0; i < ats.Length; ++i) { ats[i] = StringFromOffset(c, (&sho->Attributes)[i]); } si.Attributes = ats; sh2 = (ShaderEntry2 *)&((&sho->Attributes)[sho->AttributeCount]); } else { //si.Code.HLSL11 = GetVxFxBlobs (c, sh->HLSL11 , length=???); // TODO var ats = new string[sh->AttributeCount]; for (uint i = 0; i < ats.Length; ++i) { ats[i] = StringFromOffset(c, (&sh->Attributes)[i]); } si.Attributes = ats; sh2 = (ShaderEntry2 *)&((&sh->Attributes)[sh->AttributeCount]); } si.Code.PSSL = GetVxFxBlobs(c, sh2->PSSL); si.Code.Cg = GetVxFxBlobs(c, sh2->Cg); si.Code.Cg_PS3 = GetVxFxBlobs(c, sh2->Cg_PS3); return(si); }
public static JsonData SerializeProject(GMFile f) { var r = CreateObj(); r["general"] = "general.json"; r["options"] = "options.json"; // --- r["textures"] = CreateArr(); for (int i = 0; i < f.Textures.Length; i++) { r["textures"].Add(SR.DIR_TEX + i.ToString(CultureInfo.InvariantCulture) + SR.EXT_PNG); } r["tpags"] = CreateArr(); for (int i = 0; i < f.TexturePages.Length; i++) { r["tpags"].Add(SR.DIR_TXP + i.ToString(CultureInfo.InvariantCulture) + SR.EXT_JSON); } // --- var infoTable = new Dictionary <int, SoundInfo>(); foreach (var s in f.Sound) { if ((s.IsEmbedded || s.IsCompressed) && s.AudioId != -1) { infoTable[s.AudioId] = s; } } r["audio"] = CreateArr(); for (int i = 0; i < f.Audio.Length; i++) { r["audio"].Add(SR.DIR_WAV + infoTable[i].Name + SR.EXT_WAV); } r["code"] = CreateArr(); for (int i = 0; i < f.Code.Length; i++) { r["code"].Add(SR.DIR_CODE + f.Code[i].Name + SR.EXT_GML_LSP); } // --- r["sounds"] = SerializeArray(f.Sound, s => SR.DIR_SND + s.Name + SR.EXT_JSON); r["sprites"] = SerializeArray(f.Sprites, s => SR.DIR_SPR + s.Name + SR.EXT_JSON); r["bg"] = SerializeArray(f.Backgrounds, s => SR.DIR_BG + s.Name + SR.EXT_JSON); r["paths"] = SerializeArray(f.Paths, s => SR.DIR_PATH + s.Name + SR.EXT_JSON); r["scripts"] = SerializeArray(f.Scripts, s => SR.DIR_SCR + s.Name + SR.EXT_JSON); r["fonts"] = SerializeArray(f.Fonts, s => SR.DIR_FNT + s.CodeName + SR.EXT_JSON); r["objs"] = SerializeArray(f.Objects, s => SR.DIR_OBJ + s.Name + SR.EXT_JSON); r["rooms"] = SerializeArray(f.Rooms, s => SR.DIR_ROOM + s.Name + SR.EXT_JSON); return(r); }
internal static string StringFromOffset(GMFileContent content, long off) { if (off == 0 || (off & 0xFFFFFF00) == 0xFFFFFF00) { return(String.Empty); } return(ReadString((byte *)GMFile.PtrFromOffset(content, off))); }
public static ExtensionInfo GetExtensionInfo(GMFileContent c, uint id) { if (id >= c.Extensions->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var ex = (ExtensionEntry *)GMFile.PtrFromOffset(c, (&c.Extensions->Offsets)[id]); ExtensionInfo ei; ei.Name = StringFromOffset(c, ex->Name); ei.ClassName = StringFromOffset(c, ex->ClassName); var efi = new ExtensionFileInfo[ex->Includes.Count]; for (uint i = 0; i < efi.Length; ++i) { var efx = (ExtensionFileEntry *)GMFile.PtrFromOffset(c, (&ex->Includes.Offsets)[i]); ExtensionFileInfo ii; ii.Filename = StringFromOffset(c, efx->Filename); ii.KillSymbol = StringFromOffset(c, efx->KillSymbol); ii.InitSymbol = StringFromOffset(c, efx->InitSymbol); ii.Type = efx->Type; var effi = new ExtensionFunctionInfo[efx->Functions.Count]; for (uint j = 0; j < effi.Length; ++j) { var efff = (ExtensionFunctionEntry *)GMFile.PtrFromOffset(c, (&efx->Functions.Offsets)[j]); ExtensionFunctionInfo fff; fff.GMLName = StringFromOffset(c, efff->GMLName); fff.ID = efff->ID; fff.CallingConvention = efff->CallingConvention; fff.ReturnType = efff->ReturnType; fff.SymbolName = StringFromOffset(c, efff->SymbolName); var args = new ExtensionFFIType[efff->ArgumentCount]; // TODO: memcpy much? for (uint k = 0; k < args.Length; ++k) { args[k] = (&efff->ArgumentTypes)[k]; } fff.Arguments = args; } ii.Functions = effi; } ei.Includes = efi; return(ei); }
internal static string StringFromOffset(GMFileContent content, long off) { if (off == 0 || (off & 0xFFFFFF00) == 0xFFFFFF00 /* avoid crashes due * to bogus offsets */) { return(null); } return(ReadString((byte *)GMFile.PtrFromOffset(content, off - 4))); }
static byte[] ReadByteArray(GMFileContent c, uint off, uint len) { if (len == 0 || off == 0) { return(new byte[0]); } byte[] r = new byte[len]; Marshal.Copy((IntPtr)GMFile.PtrFromOffset(c, off), r, 0, unchecked ((int)len)); return(r); }
public static string GetAudioGroupInfo(GMFileContent content, uint id) { if (id >= content.AudioGroup->Count) { return(null); } var ag = GMFile.PtrFromOffset(content, (&content.AudioGroup->Offsets)[id]); return(StringFromOffset(content, *(uint *)ag)); // it's just a name }
static void Main(string[] args) { IO.Path = "C:/Program Files (x86)/Steam/steamapps/common/Gato Roboto"; IO.Path = "E:\\Projects\\GatoRobotoRandomizer\\GatoRobotoRandomizer\\bin\\Debug"; GMFile gm = GMFile.GetFile(IO.DataWinFile); mapExplore(); //Console.Write(Logic.Dumps()); }
public static string GetAudioGroupInfo(GMFileContent content, uint id) { if (id >= content.AudioGroup->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var ag = GMFile.PtrFromOffset(content, (&content.AudioGroup->Offsets)[id]); return(StringFromOffset(content, *(uint *)ag)); // it's just a name }
public static RoomInfo GetRoomInfo(GMFileContent content, uint id) { if (id >= content.Rooms->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var ret = new RoomInfo(); var re = (RoomEntry *)GMFile.PtrFromOffset(content, (&content.Rooms->Offsets)[id]); ret.Name = StringFromOffset(content, re->Name); ret.Caption = StringFromOffset(content, re->Caption); ret.Size = re->Size; ret.Speed = re->Speed; ret.IsPersistent = re->Persistent.IsTrue(); ret.Colour = re->Colour; ret.DrawBackgroundColour = re->DrawBackgroundColour.IsTrue(); ret._unknown = re->_unknown; ret.EnableViews = (re->Flags & RoomEntryFlags.EnableViews) != 0; ret.ShowColour = (re->Flags & RoomEntryFlags.ShowColour) != 0; ret.ClearDisplayBuffer = (re->Flags & RoomEntryFlags.ClearDisplayBuffer) != 0; ret.UnknownFlag = (re->Flags & RoomEntryFlags.Unknown) != 0; ret.World = re->World; ret.Bounding = re->Bounding; ret.Gravity = re->Gravity; ret.MetresPerPixel = re->MetresPerPixel; ret.Backgrounds = ReadList(content, (CountOffsetsPair *)GMFile.PtrFromOffset(content, re->BgOffset), ReadRoomBg); ret.Views = ReadList(content, (CountOffsetsPair *)GMFile.PtrFromOffset(content, re->ViewOffset), ReadRoomView); ret.Objects = ReadList(content, (CountOffsetsPair *)GMFile.PtrFromOffset(content, re->ObjOffset), ReadRoomObj); ret.Tiles = ReadList(content, (CountOffsetsPair *)GMFile.PtrFromOffset(content, re->TileOffset), ReadRoomTile); if (re->BgOffset != (&content.Rooms->Offsets)[id] + sizeof(RoomEntry)) { // heuristic to find instance data uint instanceListOffset = *(uint *)GMFile.PtrFromOffset(content, (&content.Rooms->Offsets)[id] + sizeof(RoomEntry)); if (instanceListOffset > (&content.Rooms->Offsets)[id] && (instanceListOffset & 0xFF000000) != 0xFF000000) { ret.ObjInst = ReadList(content, (CountOffsetsPair *)GMFile.PtrFromOffset(content, instanceListOffset), ReadRoomObjInst); } } // TODO: There's an extra 32 bytes at the end of each room entry in // JetBoy, and they are important! I got a different background // color and some motion blur effects when I set them to dummy // values. // background color is 16 bytes in return(ret); }
public static string GetStringInfo(GMFileContent content, uint id) { if (id >= content.Strings->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var se = (StringEntry *)GMFile.PtrFromOffset(content, (&content.Strings->Offsets)[id] - 4); return(ReadString(&se->Data)); }
public static JsonData SerializeFuncs(GMFile f) { var r = CreateObj(); r["functions"] = SerializeArray(f.RefData.Functions, SerializeReferenceDef); if (f.FunctionLocals != null) { r["locals"] = SerializeArray(f.FunctionLocals, SerializeFuncLocalsInfo); } return(r); }
public static JsonData SerializeVars(GMFile f) { var r = CreateObj(); if (f.VariableExtra != null) { r["extra"] = SerializeArray(f.VariableExtra, Utils.Identity); } r["variables"] = SerializeArray(f.RefData.Variables, SerializeReferenceDef); return(r); }
static TexturePageInfo TPagFromOffset(GMFileContent content, long off) { var tpe = (TexPageEntry *)GMFile.PtrFromOffset(content, off); var ret = new TexturePageInfo(); ret.Position = tpe->Position; ret.RenderOffset = tpe->RenderOffset; ret.Size = tpe->Size; ret.BoundingBox = tpe->BoundingBox; ret.SpritesheetId = tpe->SpritesheetId; return(ret); }
public static ObjectInfo GetObjectInfo(GMFileContent content, uint id) { if (id >= content.Objects->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var ret = new ObjectInfo(); var oe = (ObjectEntry *)GMFile.PtrFromOffset(content, (&content.Objects->Offsets)[id]); ret.Name = StringFromOffset(content, oe->Name); ret.SpriteIndex = oe->SpriteIndex; ret.IsVisible = oe->Visible.IsTrue(); ret.IsSolid = oe->Solid.IsTrue(); ret.Depth = oe->Depth; ret.IsPersistent = oe->Persistent.IsTrue(); ret.ParentId = oe->ParentId < 0 ? null : (uint?)oe->ParentId; ret.TexMaskId = oe->MaskId < 0 ? null : (uint?)oe->MaskId; ret.Physics = oe->Physics; var hasMore = oe->Rest.ShapePoints.Count > 0x00FFFFFF; // good enough for now var shapeCop = hasMore ? &oe->Rest.ShapePoints_IfMoreFloats : &oe->Rest.ShapePoints; if (hasMore) { ret.OtherFloats = new float[4]; Marshal.Copy((IntPtr)(oe->Rest.MoreFloats), ret.OtherFloats, 0, 4); } else { ret.OtherFloats = EmptyFloatArr; } ret.ShapePoints = new Point[shapeCop->Count / 2]; for (uint i = 0; i < shapeCop->Count / 2; i++) { ret.ShapePoints[i] = new Point( *(int *)GMFile.PtrFromOffset(content, (&shapeCop->Offsets)[i * 2]), *(int *)GMFile.PtrFromOffset(content, (&shapeCop->Offsets)[i * 2 + 1]) ); } return(ret); }
public static ScriptInfo GetScriptInfo(GMFileContent content, uint id) { if (id >= content.Scripts->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var se = (ScriptEntry *)GMFile.PtrFromOffset(content, (&content.Scripts->Offsets)[id]); var ret = new ScriptInfo(); ret.Name = StringFromOffset(content, se->Name); ret.CodeId = se->CodeId; return(ret); }
public static OptionInfo GetOptionInfo(GMFileContent content) { var ret = new OptionInfo(); var oe = content.Options; var h = GMFile.ChunkOf(content, oe->IconOffset); ret.Constants = new Dictionary <string, string>((int)oe->ConstMap.Count); for (uint i = 0; i < oe->ConstMap.Count; i++) { ret.Constants.Add(StringFromOffset(content, (&oe->ConstMap.Offsets)[i * 2]), StringFromOffset(content, (&oe->ConstMap.Offsets)[i * 2 + 1])); } return(ret); }
public static FontInfo GetFontInfo(GMFileContent content, uint id) { if (id >= content.Fonts->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var fe = (FontEntry *)GMFile.PtrFromOffset(content, (&content.Fonts->Offsets)[id]); var ret = new FontInfo(); var tpag = TPagFromOffset(content, fe->TPagOffset); ret.CodeName = StringFromOffset(content, fe->CodeName); ret.SystemName = StringFromOffset(content, fe->SystemName); ret.EmSize = fe->EmSize; ret.IsBold = fe->Bold.IsTrue(); ret.IsItalic = fe->Italic.IsTrue(); ret.Charset = fe->Charset; ret.AntiAliasing = fe->AntiAliasing; ret.Scale = fe->Scale; for (uint i = 0; i < content.TexturePages->Count; i++) { if (fe->TPagOffset == (&content.TexturePages->Offsets)[i]) { ret.TexPagId = i; break; } } ret.Characters = ReadList(content, &fe->Chars, (_, p) => { var entry = (FontCharEntry *)p; var c = new FontCharacter(); c.Character = entry->Character; c.TPagFrame = entry->TexPagFrame; c.Shift = entry->Shift; c.Offset = entry->Offset; return(c); }); return(ret); }
public static AudioInfo GetAudioInfo(GMFileContent content, uint id) { if (id >= content.Audio->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var au = (AudioEntry *)GMFile.PtrFromOffset(content, (&content.Audio->Offsets)[id]); var ret = new AudioInfo(); ret.Wave = new byte[au->Length]; Marshal.Copy((IntPtr)(&au->Data), ret.Wave, 0, ret.Wave.Length); return(ret); }
public static TexturePageInfo GetTexPageInfo(GMFileContent content, uint id) { if (id >= content.TexturePages->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var tpe = (TexPageEntry *)GMFile.PtrFromOffset(content, (&content.TexturePages->Offsets)[id]); var ret = new TexturePageInfo(); ret.Source = tpe->Source; ret.Destination = tpe->Dest; ret.Size = tpe->Size; ret.SpritesheetId = tpe->SpritesheetId; return(ret); }
static T[] ReadList <T>(GMFileContent content, CountOffsetsPair *list, Func <GMFileContent, IntPtr, T> readThing) { if (readThing == null) { throw new ArgumentNullException(nameof(readThing)); } var len = list->Count; var ret = new T[len]; var addresses = &list->Offsets; for (uint i = 0; i < len; i++) { ret[i] = readThing(content, (IntPtr)GMFile.PtrFromOffset(content, addresses[i])); } return(ret); }
public static CodeInfo DisassembleCode(GMFileContent content, uint id) { if (id >= content.Code->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var cee = (CodeEntryE *)GMFile.PtrFromOffset(content, (&content.Code->Offsets)[id]); var len = cee->Length; var bc = &cee->Bytecode; if (content.General->BytecodeVersion > 0xE) { var cef = (CodeEntryF *)cee; bc = (uint *)((byte *)&cef->BytecodeOffset + cef->BytecodeOffset); // ikr? } var ret = new List <IntPtr>(); // doesn't like T* as type arg len = Utils.PadTo(len, 4); AnyInstruction *instr; for (uint i = 0; i * 4 < len; /* see loop end */) { instr = (AnyInstruction *)(bc + i); ret.Add((IntPtr)instr); i += DisasmExt.Size(instr, content.General->BytecodeVersion); } return(new CodeInfo { Name = SectionReader.StringFromOffset(content, cee->Name), Instructions = Utils.MPtrListToPtrArr(ret), Size = cee->Length }); }
public static TextureInfo GetTextureInfo(GMFileContent content, uint id) { if (id >= content.Textures->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var te = (TextureEntry *)GMFile.PtrFromOffset(content, (&content.Textures->Offsets)[id]); var ret = new TextureInfo(); var png = (PngHeader *)GMFile.PtrFromOffset(content, te->Offset); ret.Width = Utils.SwapEnd32(png->IHDR.Width); ret.Height = Utils.SwapEnd32(png->IHDR.Height); ret.PngData = new byte[PngLength(png)]; Marshal.Copy((IntPtr)png, ret.PngData, 0, ret.PngData.Length); return(ret); }
public static ShaderInfo GetShaderInfo(GMFileContent c, uint id) { if (id >= c.Shaders->Count) { throw new ArgumentOutOfRangeException(nameof(id)); } var sh = (ShaderEntry *)GMFile.PtrFromOffset(c, (&c.Shaders->Offsets)[id]); var si = new ShaderInfo(); si.Name = StringFromOffset(c, sh->Name); si.Type = sh->Type.Decode(); si.Code.GLSL_ES = GetVxFxStrings(c, sh->GLSL_ES); si.Code.GLSL = GetVxFxStrings(c, sh->GLSL); si.Code.HLSL9 = GetVxFxStrings(c, sh->HLSL9); //si.Code.HLSL11 = GetVxFxBlobs (c, sh->HLSL11 , length=???); // TODO var ats = new string[sh->AttributeCount]; for (uint i = 0; i < ats.Length; ++i) { ats[i] = StringFromOffset(c, (&sh->Attributes)[i]); } si.Attributes = ats; var sh2 = (ShaderEntry2 *)&((&sh->Attributes)[sh->AttributeCount]); si.Code.PSSL = GetVxFxBlobs(c, sh2->PSSL); si.Code.Cg = GetVxFxBlobs(c, sh2->Cg); si.Code.Cg_PS3 = GetVxFxBlobs(c, sh2->Cg_PS3); return(si); }