Skip navigation

Category Archives: Uncategorized

So I’m watching Mace Windu in action, then something occurred to me.

 

jedimindtrick

Anyway, I laughed pretty hard at this realization.

Mind = blown

My wife has done the blogging to introduce our son in these two posts: here and here.

Jake

I know quite a few people who wouldn’t want this robot at their dinner table….


A father buys a lie detecting robot that slaps a person when he lies. The father decides to test it out on his son at supper.

“Where were you last night?”

 “I was at the library.”

The robot slaps the son.

 “Okay…I was at a friend’s house.”

“Doing what?” asks the father.

“Watching a movie; ‘Toy Story.'”

The robot slaps the son.

 “Okay…it was porn!” cries the son.

The father yells, “What? When I was your age, I didn’t know what porn was!”

The robot slaps the father.

 The mother laughs and says, “He certainly is your son!”

The robot slaps the mother.

 


bazziiing!

A simple vbscript I use for manual client installation. It does some basic quick checks and fixes before begining an install. One could feasibly use this for health checking, but it’s not nearly as robust as my actual logon framework or other scripts from people like Jason Sandys or Dan Thompson.

The variables strAdmin (local admin service account), strCcmSetup (path to folder with the ccmsetup.exe), and strArguement (install string) need to be defined before running the script. It’s worth mentioning that strCcmSetup is just the path to ccmsetup.exe, there is no need to actually type ccmsetup.exe into the path and it’s best if you don’t since I didn’t bother writing anything in to verify if it is or isn’t. The script will auto append the executable to that variable so ccmsetup.exe in the path will give you ccmsetup.execcmsetup.exe

'Author Daniel Belcher             ||
'CCMsetup Function                 ||
'Date 2/11/2011 rev 5/20/2011      ||
'==================================||
'|Objects, Variables, and Constants ********************************************
'=============================================================================||
'|| Non-Standard Installer Variables - Please define them                     ||
'|| Admin Account
	strAdmin = "domain\user"
'|| Path to ccmsetup.exe
	strCcmSetup = "\\path\where\ccmsetup.exe\resides\"
'|| Install string
	strArguement = "/noservice SMSSITECODE=ABC SMSSLP=SERVER.ADDRESS " _
				& "SMSFSP=SERVER.ADDRESS"
'                                                                             ||
'=============================================================================||
Const DEBUGMSG = False 'Boolean to use for testing False = Silent True = Verbose
Const ForAppending = 8
Dim oWShell
Set oWShell = CreateObject("WScript.Shell")
Dim oFSo
Set oFSO = CreateObject("Scripting.FileSystemObject")
Dim oNet
Set oNet = CreateObject("Wscript.Network")
ComputerName = oNet.ComputerName
Dim Target
Target = "."
Dim oWMISvc
Set oWMISvc = GetObject("winmgmts:\\"&Target)
'|Main Run *******************************************************************||
'=============================================================================||
WriteLog "Starting "&Wscript.Scriptname&" on "&ComputerName,2
PreReq
CCMSetup
Report
'SubRoutines *****************************************************************||
'=============================================================================||
Sub WriteLog(msg,mtype)
'Subroutine for writing the log
logname = wscript.scriptname&".log"
if not oFSo.FileExists(logname) then
oFSo.CreateTextFile(logname)
end if

msgline = "<![LOG["&msg&"]LOG]!><time="&""""&DatePart("h",Time)
msgline = msgline &":"&DatePart("n",Time)&":"&DatePart("s",Time)
msgline = msgline &".000+0"""&" date="""&Replace(Date,"/","-")
msgline = msgline &""""&" component="""&WScript.ScriptName
msgline = msgline &""" context="""" type="""&mtype
msgline = msgline &""" thread="""" file="""&WScript.ScriptName& """>"

Set oLogFile = oFSo.OpenTextFile(logname, ForAppending, True)
    oLogFile.WriteLine msgline
oLogFile.Close
if DEBUGMSG then wscript.echo msg&" "& mtype
End Sub
'*******************************************************************************
Sub SvcStart(service)
'Attempts to start a service
Set WMIServices = oWMISvc.ExecQuery _
("Select * from Win32_Service where name = '"&service&"'")
     For Each item in WMIServices
         if lcase(item.startmode) <> "automatic" then
                             item.ChangeStartMode("Automatic")
         end if
         start = item.startservice()
         wscript.sleep 4000
           if start <> 0 Then
                   WriteLog "ERROR: Failed to start the "&service&" Service, " _
                   & "investigation required", 3
                             wscript.quit(0)
                    else
                    WriteLog "Succesfully started "& service &" Service",1
           end if
     next
End Sub
'|Functions ******************************************************************||
'=============================================================================||
Function PreReq
'Prerequisite check for client installation
Set WMIServices = oWMISvc.ExecQuery _
("Select * from Win32_Service")
strService = "lanmanworkstation"
regAdminSPath = "HKLM\SYSTEM\CurrentControlSet\services\LanmanServer\" _
				& "Parameters\"
 For Each item in WMIServices
          if lcase(item.name) = strService then
             if lcase(item.state) <> "running" then
                writelog "Warning: "&strService&" was not running, " _
                & "attempting to start...", 2
                SvcStart strService
             else
                 writelog strService& " was found running, OK", 1
             end if
          end if
 next
if not oFSO.FolderExists("\\"&computername&"\admin$") then
WriteLog "Warning: Admin shares not available, attempting to add " _
		& "registry key...", 2
    On Error Resume Next
oWShell.RegWrite regAdminSPath & "AutoShareWKS", 1, "REG_SZ"
              if err.Number <> 0 then
WriteLog "ERROR: "&regAdminSPath&"AutoShareWKS, 1 failed to write, or " _
		&"already exists.", 3
                  else
WriteLog regAdminSPath&"Notice: AutoShareWKS, 1 written succesfully. " _
					& " Restart required to take effect.", 2
              end if
Wscript.Echo "AutoShareWKS key was written to: " &vbcrlf _
          &regAdminSPath & vbcrlf _
          &"Please restart the machine, and rerun this script"
else
WriteLog "Admin$ shares found, and working.", 1
end if
Set AdminCheck = GetObject("WinNT://" & oNet.ComputerName _
							& "/Administrators,group")
if AdminCheck.IsMember("WinNT://"& strAdmin) then
WriteLog "User "&strAdmin&" found in Local Adminstrators group", 1
        else
WriteLog "Warning: User "&strAdmin&" not found in Local Administrators group",2
        on Error Resume Next
           AdminCheck.Add("WinNT://"&strAdmin)
        if Err.Number <> 0 then
WriteLog "ERROR: Unable to add "&strAdmin&" to the Local Administrators group",3
           Wscript.Echo "Unable to add "&strAdmin&" to local Admins." & vbcrlf _
           &"Please do so manually and rerun this script."
           Wscript.Quit(0)
        end if
end if

End Function
'*******************************************************************************
Function CCMSetup
'Client detection and installation
Set WMIServices = oWMIsvc.ExecQuery _
("Select * from Win32_Service where name ='ccmexec'")
boorun = true
   for each objservice in WMIServices
       strservice = lcase(objservice.name)
           if strservice = "ccmexec" then boorun = "False"
   next
if not boorun then
For Each service in WMIServices
  if lcase(service.state) <> "running" then
     svcStart "ccmexec" 'Attempt to start service if stopped
WriteLog "Warning: CcmExec service found, but was not running, " _
		& "attempting to start...", 2
  end if
next
WriteLog "SCCM client service, CcmExec found, closing script", 1
	exit function
end if
if boorun then
oWShell.run strCcmsetup&"ccmsetup.exe " & strArguement
        WriteLog "CcmSetup has begun using: ccmsetup.exe"& strArguement,1
end if

End Function
'******************************************************************************
Function Report
 If DEBUGMSG then msgbox "Complete" 'Set for popup on script complete
End Function
'End *************************************************************************||
'=============================================================================||

As has become my standard, the logs are written in a markup that works with trace32.

Ok, so I’ve not posted anything for a few days and I felt the need to throw a brief technical post up with a code snippet from a current project I’m working on. I’m building a health check logon script and as part of that framework I wanted to build a logging object. Since the target format has to be vbscript for what I’m doing, I’ve built it as such, and in a format that views nicely inside of trace32 and trace64.

The idea was to build an object that would perform a simple task, write a log file…. then write a log file, or an event, then write only error events, but could also buffer and dump a final error or success log as a split log to another remote location. All configurable via properties, but with only two exposed methods to control it all (write & writeremote). This object is instantiated by:

Dim Logging
Set Logging = New cls_Logging

Call Logging.Write("my message",1)

Wscript.Quit(0)

Simple enough, no? It’s also worth mentioning I wrote the code so that a constant could be set within the instantiating script of DEBUGMODE and if TRUE it will error, else suppresses all error output.

Here’s the code:

'=================================================================================
'Logging Class ===================================================================
'=================================================================================

'Not required for WSF, but is when in standard VBS
Const ForAppending = 8

'Log and Event writer object
Class cls_Logging
'Class for logging to file and event viewer

Private oWShell,oNet,oFSo,Filehandle,rFilehandle
Private fPath,strRFPath,fMaxSize,fLogname,strRemoteErr,BoolEvent,BoolRemote,oDict

Private Sub Class_Initialize()
    'Object Init subroutine
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
    Set oWShell 	= CreateObject("Wscript.Shell")
	Set oNet 		= CreateObject("Wscript.Network")
	Set oFSo 		= CreateObject("Scripting.FileSystemObject")
	Set oDict		= CreateObject("Scripting.Dictionary")
		LogEvent 	= False
		RemoteLog	= False
		Path 		= Left(WScript.ScriptFullName,(Len(WScript.ScriptFullName)_
						-Len(WScript.ScriptName)))
		File 		= LCase(oNet.ComputerName)
		MaxSize = 2
End Sub

'---------------------------------------------------------

Private Sub Class_Terminate()
	'Object Termination subroutine
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
	If bOpen Then
		Filehandle.close
	End If
		Set oWShell = Nothing
		Set oNet = Nothing
		Set oFSo = Nothing
		Set Filehandle = Nothing
		Set fPath = Nothing
		Set fMaxSize = Nothing
		Set fLogname = Nothing
		Set BoolEvent = Nothing
End Sub

'---------------------------------------------------------
'File name properties, for changing and retrieving the log file name
Public Property Let File(strFile)
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
	If (InStr(StrReverse(strFile),"gol.")) <> 0 Then
		fLogname = strFile
	Else
		fLogname = strFile & ".log"
	End If
End Property
		Public Property Get File()
		    If Debugmode Then On Error Goto 0 Else On Error Resume Next
				File = fLogname
		End Property

'---------------------------------------------------------
'Path name properties, for changing and retrieving the path to logs
Public Property Let Path(strPath)
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
		If (InStr(StrReverse(strpath),"\")) <> 1 Then
			fPath = strPath & "\"
		Else
			fPath = strPath
		End If
End Property
		Public Property Get Path()
		    If Debugmode Then On Error Goto 0 Else On Error Resume Next
			Path = fPath
		End Property
		
'---------------------------------------------------------
'Fully concatenated file name property for retrival.
		Public Property Get FullFileName()
			FullFileName = Path & File
		End Property

'---------------------------------------------------------
'Property for setting maximum file size of log file
Public Property Let MaxSize(strVal)
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
	fMaxSize = Cint(strVal) * 1048576
End Property
		Public Property Get MaxSize()
    		If Debugmode Then On Error Goto 0 Else On Error Resume Next
			MaxSize = fMaxSize
		End Property

'---------------------------------------------------------
'Boolean property to determine if the filehandle is in use
Private Property Get bOpen()
	If Debugmode Then On Error Goto 0 Else On Error Resume Next	
		If IsObject(Filehandle) Then
			bOpen = True
		Else
			bOpen = False
		End If
End Property

'---------------------------------------------------------
Public Property Let LogEvent( blValue)
'Bool property that dictates event viewer rights
	BoolEvent = blValue
End Property
	Private Property Get LogEvent()
		LogEvent = BoolEvent
	End Property
	
Public Property Let RemoteLog( blValue)
'Bool property that dictates if logging occurs to remote location
	BoolRemote = blValue
End Property
	Private Property Get RemoteLog()
		RemoteLog = BoolRemote
	End Property
Public Property Let RemotePath( strPath)
		If (InStr(StrReverse(strpath),"\")) <> 1 Then
			strRFPath = strPath & "\"
		Else
			strRFPath = strPath
		End If
End Property
	Public Property Get RemotePath()
		RemotePath = strRFPath
	End Property

'---------------------------------------------------------
Private Sub RemoteErrBuffer( strKey,  strItem)
'Method to concactenate new items under one key at the end of the string
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
		Dim concat

	If Not oDict.Exists(strKey) Then
		Call oDict.Add(strkey, stritem)
	Else
		concat = oDict.Item(strKey)
		concat = concat & "|:|" & strItem
			oDict.Remove(strKey)
		Call oDict.Add(strKey,concat)
	End If

End Sub

'---------------------------------------------------------
Public Function ErrBuffer()
'Method to return contents of the error buffer
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
    
    Dim ItemToSplit, ItemArray, item
	
	ItemToSplit = oDict.item("remotelog")
	
	ItemArray = Split(ItemToSplit, "|:|")
	
	ErrBuffer = ItemArray
	
End Function
'---------------------------------------------------------
Public Sub WriteRemote(strVal)
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
    	
	If Not CreateRemote Then
		Exit Sub
	End If

		rFilehandle.WriteLine strVal

End Sub

'---------------------------------------------------------
'Subroutine for creating the remote log file and instantiating the handle
Private Function CreateRemote()
    If Debugmode Then On Error Goto 0 Else On Error Resume Next

	Dim FileProperty,Logsize

	CreateRemote = False

	If Not oFso.FolderExists(RemotePath) Then 
		Call Write(RemotePath & " Does not exist, or is unreachable.",3)
			Exit Function
	End If 

	If Not oFSo.FileExists(RemotePath & File) Then
		oFso.CreateTextFile(RemotePath & File)
	Else
		oFSo.DeleteFile(RemotePath & File)
			oFso.CreateTextFile(RemotePath & File)
	End If    
		If Not IsObject(rFilehandle) Then
	       	Set rFileHandle = oFSo.OpenTextFile(RemotePath & File, _
	       	 ForAppending, True)
	    End If
	If oFSo.FolderExists(RemotePath) Then
		CreateRemote = True
	End If
End Function

'---------------------------------------------------------
'Subroutine for writing log entries
Public Function Write( msg,  mtype)
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
	Dim msgline, etype
		Call Create()

		If Not bOpen Then
			Call Create()
		End If
    msgline = "<![LOG["&msg&"]LOG]!><time="&""""&DatePart("h",Time) _
    &":"&DatePart("n",Time)&":"&DatePart("s",Time)&".000+0"""&" date=""" _
    &Replace(Date,"/","-")&""""&" component="""&Left(WScript.ScriptName, _
    Len(WScript.ScriptName)-Len(".vbs"))&""" context="""" type="""&mtype _
    &""" thread="""" file="""&Left(WScript.ScriptName,Len(WScript.ScriptName)_
    -Len(".vbs"))& """>"

	Filehandle.WriteLine msgline


		Select Case Mtype
			Case 1
				etype = 0
			Case 2
				etype = 2
						If LogEvent Then
					oWShell.LogEvent etype, msg
						End If 
					Call RemoteErrBuffer("remotelog", msg  & "," & "2")
			Case Else
				etype = 1
						If LogEvent Then
					oWShell.LogEvent etype, msg
						End If
					Call RemoteErrBuffer("remotelog", msg  & "," & "1")
		End Select
	
End Function

'---------------------------------------------------------
'Subroutine for rolling over log file at file size limit
Private Sub Rollover()
    If Debugmode Then On Error Goto 0 Else On Error Resume Next
	If bOpen Then
		Filehandle.Close
	End If
	oFso.CopyFile FullFileName, Left(FullFileName,(Len(FullFileName)-1)), True
		oFSo.DeleteFile FullFileName
	Set FileHandle = oFSo.OpenTextFile(FullFileName, ForAppending, True)			
End Sub

'---------------------------------------------------------
'Subroutine for creating the log file and instantiating the handle
Private Sub Create()
    If Debugmode Then On Error Goto 0 Else On Error Resume Next

	Dim FileProperty,Logsize

	If Not oFSo.FileExists(FullFileName) Then
		oFSo.CreateTextFile(FullFileName)
	End If    
		If Not bOpen Then
	       	Set FileHandle = oFSo.OpenTextFile(FullFileName, ForAppending, True)
	    End if
	Set FileProperty = oFSo.GetFile(FullFileName)
				Logsize = FileProperty.size
	If Logsize > MaxSize Then
			Filehandle.WriteLine "\\\\\\\\\\File Size Reached//////////"
		Call Rollover()
	End If
End Sub
End Class

I built a scripting dictionary wrapper as well which was inspired by work from Dan Thomson in his health check script. I’ll most likely post it next after I feel it’s complete.

If you found this object helpful, or otherwise, I would appreciate it if you rated it on script center.

My daughter Maggie, who I’ve mentioned on here before, has some major issues (undiagnosed as of yet) that have caused her considerable developmental delays.  This past Sunday was a major breakthrough for her, and my wife has done a fine job of summing it up for those interested in reading it.

I’m not really sure WHAT inspired me to come back and give it a try again…

When it first released and I bought it (being a long time fan of Turbine games) I really enjoyed it.  The game offered multiple layers to it’s (at the time only) PvE element for both customization and character development, it had a rich overarching story telling system (similar to the now defunct AC2), and great universe integration.  It had about everything I enjoy in an MMO, but for whatever reason, it didn’t grab me.  Perhaps it was due to the clunky skill systems, or the over all busy nature of the games complexity, but it just felt wrong after I got past my 20s/30s in the game.

Navigation was a headache, and often times, the little to no reward I started seeing from the quest lines began to leave me wanting more.  The amount of work required to do basic things, such as combat, also began to wear on me (I was a loremaster, which is a very inventive great class). 

However, several expansions, engines, and a pay model change later I’ve decided to give it a go again; here are my initial impressions upon returning (some of these I’ve experienced from recent jaunts back as well):

  • Graphics have improved
  • Character responsiveness still not what I’d like to see
  • Storylines have become richer
  • Customizations have branched further
  • PvP is a nice addition, but meh
  • Soloing feels easier
  • Free to Play model seems implemented fairly well
  • Long term game play feels more promising than before

So lets discuss these points a bit more.

DX11 has been integrated, aside from some differences in the shadows, soft particles, and some interactive water effects (not sure why this wasn’t done prior to DX11) I don’t see a huge difference from the previous DX10 and DX9 iteration.  Textures (although high res versions have been available for a while) still don’t offer anything new, no tessellation, I don’t suspect or expect them to redo all the in game textures and engine, especially since I already find the game quite beautiful but the tech in me always wants to see rendering boundaries pushed.  Mind you I’m on an SLI rig that is DX11 capable, I WANT to make use of that when I game.


 

Character responsiveness has always been a bit of a nagging point for me in Turbine games since their original Asheron’s Call.  I can only assume it’s something to do with their code that requires server response before the client will report movement.  Most online games allow the client to dictate to the server it’s position and the server simply analyzes that and agrees or disagrees with the client.  This is called client-side prediction and has been in use in multiplayer games since the days of Quake at least.  Wikipedia states that Duke Nukem 3D used it as well, but my first real taste of the difference came from shifting out of native Quake DM into QuakeWorld DM where it existed, but I digress. It might even be that Turbine engines USE a form of client-prediction that requires some sort of start receive message before engaging client control, I don’t know, but it causes the client side experience to feel delayed to some extent.

So what does that all mean in a practical scenario? It basically means, when I press forward, either I move forward, or I have to wait a split second or more before my character moves forward.  Both can create unfavorable gaming moments, but I feel that the method in place in Turbine games has been problematic since it’s inception, especially given the responsive nature of their game mechanics.  The thing of it is, in their current design, you are less likely to be called back to a previous position (rubber banding) but rubber banding generally only happens in the most extreme of latency/packet loss cases.  So to avoid rubber banding (I suppose) they verify client input prior to client output, and I see a delay before I can change my X Y or Z position in the game world. 

I would be remised though if I didn’t mention it’s game integrity benefits though as well.  It protects the server from being deceived by client-side “trainers” or utilities that can report illegal positions used to exploit game mechanics.  Anyone who’s played WoW and knows about people porting and speed hacking etc, these are fundamental vulnerabilities to that technical design.  Tolerances usually exist in client prediction to allow a client to get from point a to point b without having to report every step across the grid.  That tolerance NEEDS to be there or you would see a lot of rubber banding, this is what allows smooth game play on really bad connections.  However, it’s easy to exploit that relationship by passing bogus packets to the server.  Both require creativity to overcome their hurdles, and they both do so well enough.  For me personally I feel with client prediction you open yourself to more administrative headaches in an MMO, but a better customer experience.  So I can’t help but feel it’s non-use is a form of laziness at the cost of the gamer.  One could reason it improves customer experience by insuring a level playing field but lets just move on.


 

Storylines, customizations, soloing, and PVP aren’t NEW features per say, but they are features that didn’t exist or have become more robust since it’s release in 2007.  They’ve got 2 expansions and a 3rd on the way so there has been and will still be growth in this game.  It, like a fine wine, has matured rather nicely.  I’ve also been a fan of Turbine’s story telling, and LotRO is no different, and only continues to prove (to me) their mastery in the medium of engaging RPG content in an MMO.  They’ve revamped their starting story lines, and added more tie ins to their quest chains, they introduced new methods for questing/instancing/booking called skirmishes as well.  I could go on for a long time about all the methods they have taken to make their storylines relevant and effective to meet game play but it’s something one has to experience I think more than read about.  The addition of their PvP system (called PvM for Player Versus Monster) even insures story integration into their game by forcing players to spin off new evil characters strictly for PvP.

The other aspect of the game I’ve always found intriguing is their deeds, titles, outfitting, traits, and dying systems.  All offer a robust amount of options for character diversity in their own right, but can really offer diverse customizations when the sum of all parts come together.  Mind you, like all MMOs, people will MIN/MAX so some of the traits etc become pretty cookie cutter for what is used, but it’s not from a lack of options that’s for sure.  They’ve continued to add to these systems, and at times it leaves my head swimming trying to keep track of them all, I tend to be a meta gamer so information overload is definitely something I suffer from when reviewing the possibilities.

The solo aspect of the game also seems to be more realistic than it was at release.  Not so much that you can check out when you are playing, but enough that one can keep a decent rate of progression while going at it alone.  Also some of the earliest content that was at one time a guaranteed group event has been scaled back.  Some may complain it’s TOO easy now, and they may be right, but I feel it’s found a sweet spot.


 

They’ve also converted to a Free to Play gaming model which seem to be all the rage now with MMOs (Thanks Korea!).  For anyone not familiar with a standard F2P gaming model it’s simple, offer a game for free, gut some features out and reserve them for 1) subscribers 2) one time game purchase 3) micro transactions.  In a nutshell, like selling crack.  The first one’s always free!

But seriously, it’s a clever business model, for a guy like me it gives me a demo of what I’m going to invest in and leaves me the option of giving my monthly sub since I’ve no issue with that (and haven’t since I really started into MMOs back in 97/98).  There is also a warm fuzzy seeing an itemized list of things you are getting for your money.  Mind you, it’s all nonsense and I am aware of that, but “if I pay monthly I get…” definitely passes through my mind as a comfort statement when I’m looking at F2P games.  This one of course being no exception, I’m not a huge fan of micro transactions, I’d rather lease the service than commit to it.

At any rate, it’s flexible, and playable even if you want to go in on the cheap.  Kudos to them for making that transition, and congratulations on their success with it.


 

Overall I feel the long term playability of the game has vastly improved, but it’s definitely got it’s competition like all other games.  There appears to be plenty of high end content and customizations to chase after.  There’s also multiple facets to their storylines as well that are worth exploring that I think give incredible replay value to their game.  I suppose I’ll keep going with this for a while in my free time as my fill in game.

If you are interested, look for me, or mail me in game on either Lotekend or Jahosi.

Link!!11!1!

Johan Arwidmark just posted today a quick guide on finding/extracting the Trace64.exe log reader from CM 12.

Ok, why does that matter?

Because reading logs inside PE with notepad is horrible…..

notrace 

VS.

 

trace

 

Have a great day!

image

Yup…

My wife’s birthday post for me!  I’m a very blessed man indeed.