using System; using System.IO; using System.Linq; using System.Reflection; using System.Drawing; using System.Windows.Forms; using System.Runtime.InteropServices; using System.ComponentModel; using System.Xml.Linq; using Microsoft.Win32; class NeonInstaller { static string Dir=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"NeonCursorPack"); static string KeyPath=@"Control Panel\Cursors"; static string RunPath=@"Software\Microsoft\Windows\CurrentVersion\Run"; static string InstalledExe{get{return Path.Combine(Dir,"NeonCursor-Install.exe");}} static string Backup{get{return Path.Combine(Dir,"previous-cursors.xml");}} static string Flag{get{return Path.Combine(Dir,"installed.flag");}} static XElement Manifest{get{using(var s=Assembly.GetExecutingAssembly().GetManifestResourceStream("manifest.xml"))return XElement.Load(s);}} [DllImport("user32.dll",CharSet=CharSet.Unicode,SetLastError=true)]static extern IntPtr LoadImageW(IntPtr h,string name,uint type,int w,int height,uint flags); [DllImport("user32.dll",SetLastError=true)]static extern bool SetSystemCursor(IntPtr c,uint id); [DllImport("user32.dll",SetLastError=true)]static extern bool SystemParametersInfo(uint a,uint b,IntPtr p,uint flags); [DllImport("user32.dll")]static extern bool DestroyCursor(IntPtr c); [DllImport("user32.dll",SetLastError=true)]static extern bool GetIconInfo(IntPtr c,out ICONINFO info); [DllImport("user32.dll")]static extern IntPtr LoadCursor(IntPtr h,IntPtr id); [DllImport("gdi32.dll")]static extern bool DeleteObject(IntPtr h); [DllImport("gdi32.dll")]static extern int GetObject(IntPtr h,int count,out BITMAP b); [StructLayout(LayoutKind.Sequential)]struct ICONINFO{public bool icon;public uint x,y;public IntPtr mask,color;} [StructLayout(LayoutKind.Sequential)]struct BITMAP{public int type,width,height,widthBytes;public ushort planes,bits;public IntPtr data;} static void Log(string text){Directory.CreateDirectory(Dir);File.AppendAllText(Path.Combine(Dir,"install.log"),DateTime.Now.ToString("s")+" "+text+Environment.NewLine);} static string[] ChangedNames{get{return Manifest.Elements("cursor").Where(x=>x.Attribute("key")!=null).Select(x=>(string)x.Attribute("key")).Concat(new[]{"","CursorBaseSize","SchemeSource"}).ToArray();}} static XElement Snapshot(string path,string[] names){ var root=new XElement("registry",new XAttribute("path",path)); using(var k=Registry.CurrentUser.OpenSubKey(path))foreach(string name in names){ var item=new XElement("value",new XAttribute("name",name)); if(k!=null&&k.GetValueNames().Contains(name)){ var kind=k.GetValueKind(name);object v=k.GetValue(name,null,RegistryValueOptions.DoNotExpandEnvironmentNames);item.Add(new XAttribute("kind",kind)); if(kind==RegistryValueKind.Binary)item.Value=Convert.ToBase64String((byte[])v); else if(kind==RegistryValueKind.MultiString)foreach(string line in (string[])v)item.Add(new XElement("line",line)); else item.Value=Convert.ToString(v,System.Globalization.CultureInfo.InvariantCulture); }root.Add(item); }return root; } static void RestoreKey(XElement snapshot){using(var k=Registry.CurrentUser.CreateSubKey((string)snapshot.Attribute("path")))foreach(var v in snapshot.Elements("value")){ string name=(string)v.Attribute("name");if(v.Attribute("kind")==null){k.DeleteValue(name,false);continue;} var kind=(RegistryValueKind)Enum.Parse(typeof(RegistryValueKind),(string)v.Attribute("kind"));object data=v.Value; if(kind==RegistryValueKind.DWord)data=int.Parse(v.Value,System.Globalization.CultureInfo.InvariantCulture); else if(kind==RegistryValueKind.QWord)data=long.Parse(v.Value,System.Globalization.CultureInfo.InvariantCulture); else if(kind==RegistryValueKind.Binary)data=Convert.FromBase64String(v.Value); else if(kind==RegistryValueKind.MultiString)data=v.Elements("line").Select(x=>x.Value).ToArray(); k.SetValue(name,data,kind); }} static void Extract(){Directory.CreateDirectory(Dir);string target=Path.Combine(Dir,"cursors");Directory.CreateDirectory(target); foreach(var item in Manifest.Elements("cursor")){string name=(string)item.Attribute("name")+".cur";using(var src=Assembly.GetExecutingAssembly().GetManifestResourceStream(name))using(var dst=File.Create(Path.Combine(target,name))){if(src==null)throw new Exception("Missing asset "+name);src.CopyTo(dst);}} } static string CursorFile(XElement item){return Path.Combine(Dir,"cursors",(string)item.Attribute("name")+".cur");} static void Validate(){var system=Manifest.Elements("cursor").Where(x=>x.Attribute("id")!=null).ToArray();if(system.Select(x=>(int)x.Attribute("id")).Distinct().Count()!=system.Length)throw new Exception("Duplicate system cursor IDs");foreach(var item in Manifest.Elements("cursor")){ string path=CursorFile(item);IntPtr c=LoadImageW(IntPtr.Zero,path,2,42,42,0x10);if(c==IntPtr.Zero)throw new Win32Exception(Marshal.GetLastWin32Error(),"Cannot load "+path); ICONINFO info;if(!GetIconInfo(c,out info))throw new Win32Exception();BITMAP bitmap;GetObject(info.color,Marshal.SizeOf(typeof(BITMAP)),out bitmap); try{if(info.x>=42||info.y>=42||bitmap.width!=42||bitmap.height!=42||info.x!=(uint)(int)item.Attribute("x")||info.y!=(uint)(int)item.Attribute("y"))throw new Exception("Size/hotspot mismatch "+path);}finally{DeleteObject(info.color);DeleteObject(info.mask);DestroyCursor(c);} }Log("Validated 40 cursor files: 42px and embedded hotspots");} static int Apply(){int done=0;foreach(var item in Manifest.Elements("cursor").Where(x=>x.Attribute("id")!=null)){ IntPtr c=LoadImageW(IntPtr.Zero,CursorFile(item),2,42,42,0x10);if(c==IntPtr.Zero)throw new Win32Exception(Marshal.GetLastWin32Error()); if(SetSystemCursor(c,(uint)(int)item.Attribute("id")))done++; else{int error=Marshal.GetLastWin32Error();DestroyCursor(c);int id=(int)item.Attribute("id");if(id==32631||id==32671||id==32672)Log("Optional system slot deferred to theme: "+id+" error "+error);else throw new Win32Exception(error,"System cursor update failed: "+item.Attribute("name"));} }Log("Applied "+done+" system cursor slots at 42px");return done;} static void Install(){Extract();Validate();bool already=File.Exists(Flag); if(!already){if(File.Exists(Backup))File.Copy(Backup,Backup+"."+DateTime.Now.ToString("yyyyMMddHHmmss")+".archive",true);new XElement("backup",Snapshot(KeyPath,ChangedNames),Snapshot(RunPath,new[]{"NeonCursorPack"})).Save(Backup);} try{ string self=Assembly.GetExecutingAssembly().Location;if(!String.Equals(Path.GetFullPath(self),Path.GetFullPath(InstalledExe),StringComparison.OrdinalIgnoreCase))File.Copy(self,InstalledExe,true); using(var p=Registry.CurrentUser.CreateSubKey(@"Software\Classes\neon-noz-cursor")){p.SetValue("","URL:NEON cursor controller");p.SetValue("URL Protocol","");} using(var p=Registry.CurrentUser.CreateSubKey(@"Software\Classes\neon-noz-cursor\shell\open\command"))p.SetValue("","\""+InstalledExe+"\" --protocol \"%1\""); using(var k=Registry.CurrentUser.CreateSubKey(KeyPath)){ foreach(var item in Manifest.Elements("cursor").Where(x=>x.Attribute("key")!=null))k.SetValue((string)item.Attribute("key"),CursorFile(item),RegistryValueKind.String); k.SetValue("","NEON / Clean Cyberpunk 42px",RegistryValueKind.String);k.SetValue("CursorBaseSize",42,RegistryValueKind.DWord);k.SetValue("SchemeSource",2,RegistryValueKind.DWord); } using(var k=Registry.CurrentUser.CreateSubKey(RunPath))k.SetValue("NeonCursorPack","\""+InstalledExe+"\" --refresh",RegistryValueKind.String); if(!SystemParametersInfo(0x57,0,IntPtr.Zero,0))throw new Win32Exception(Marshal.GetLastWin32Error(),"Cursor theme reload failed"); int done=Apply();File.WriteAllText(Flag,"42px / V3 / "+done+" slots");Log("Installation successful; original backup retained; login refresh registered"); }catch{if(!already)Restore();throw;} } static void Restore(){if(!File.Exists(Backup))throw new Exception("Saved previous cursor settings were not found. Install once before restoring."); foreach(var k in XElement.Load(Backup).Elements("registry"))RestoreKey(k); if(!SystemParametersInfo(0x57,0,IntPtr.Zero,0))throw new Win32Exception(Marshal.GetLastWin32Error()); if(File.Exists(Flag))File.Delete(Flag);Log("Restored previous cursor settings and prior startup value"); } static void Inspect(){var lines=new System.Collections.Generic.List();foreach(var item in Manifest.Elements("cursor").Where(x=>x.Attribute("id")!=null)){ IntPtr c=LoadCursor(IntPtr.Zero,(IntPtr)(int)item.Attribute("id"));ICONINFO info;if(c==IntPtr.Zero||!GetIconInfo(c,out info)){lines.Add((string)item.Attribute("name")+": unavailable");continue;}BITMAP b;GetObject(info.color,Marshal.SizeOf(typeof(BITMAP)),out b); lines.Add(item.Attribute("name").Value+": "+b.width+"x"+b.height+" hotspot="+info.x+","+info.y);DeleteObject(info.color);DeleteObject(info.mask); }File.WriteAllLines(Path.Combine(Dir,"system-verification.txt"),lines);} [STAThread]static int Main(string[] args){ bool silent=args.Any(x=>x=="--apply-silent"||x=="--restore-silent"||x=="--refresh"||x=="--validate"||x=="--inspect"); try{ if(args.Contains("--validate")){Extract();Validate();return 0;} if(args.Contains("--inspect")){Inspect();return 0;} if(args.Contains("--refresh")){if(File.Exists(Flag)){Apply();Inspect();}return 0;} bool restore=args.Contains("--restore-silent")||args.Contains("--restore"); int protocol=Array.IndexOf(args,"--protocol"); if(protocol>=0){ Uri uri;if(protocol+1>=args.Length||!Uri.TryCreate(args[protocol+1],UriKind.Absolute,out uri)||uri.Scheme!="neon-noz-cursor"||(uri.Host!="apply"&&uri.Host!="restore")||uri.Query!=""||uri.Fragment!=""||(uri.AbsolutePath!=""&&uri.AbsolutePath!="/"))throw new Exception("Invalid NEON action URL"); restore=uri.Host=="restore"; } #if RESTORE restore=true; #endif if(restore){Restore();}else{Install();Inspect();} if(silent)return 0; Application.EnableVisualStyles();Application.SetCompatibleTextRenderingDefault(false); Form f=new Form{Text="NEON / Cursor Pack",ClientSize=new Size(490,230),StartPosition=FormStartPosition.CenterScreen,FormBorderStyle=FormBorderStyle.FixedDialog,MaximizeBox=false,BackColor=Color.FromArgb(13,19,28),ForeColor=Color.White}; Label heading=new Label{Text=restore?"이전 커서로 복원했습니다":"NEON 42px 커서를 적용했습니다",Font=new Font("Malgun Gothic",16,FontStyle.Bold),AutoSize=false,Size=new Size(450,42),Location=new Point(22,24)}; Label sub=new Label{Text="현재 사용자에게 적용 · 재로그인 후에도 유지\n원래 커서 설정은 이 컴퓨터에 저장되어 있습니다.",AutoSize=false,Size=new Size(450,50),Location=new Point(22,76),Font=new Font("Malgun Gothic",10)}; Button apply=new Button{Text="NEON 다시 적용",Location=new Point(22,155),Size=new Size(210,46)};Button undo=new Button{Text="원래 커서로 복원",Location=new Point(247,155),Size=new Size(220,46)}; apply.Click+=(s,e)=>{try{Install();heading.Text="NEON 42px 커서를 적용했습니다";}catch(Exception ex){MessageBox.Show(ex.Message,"NEON");}}; undo.Click+=(s,e)=>{try{Restore();heading.Text="이전 커서로 복원했습니다";}catch(Exception ex){MessageBox.Show(ex.Message,"NEON");}}; f.Controls.AddRange(new Control[]{heading,sub,apply,undo});Application.Run(f);return 0; }catch(Exception ex){Log("ERROR "+ex);if(!silent)MessageBox.Show(ex.Message,"NEON / 작업 실패",MessageBoxButtons.OK,MessageBoxIcon.Error);return 1;} } }