How do I stop IDA from turning function into variadic without a cleanup?

It’s my first day ever trying decomilation and IDA, so I don’t really understand what’s happening.

I have a following function in my disassembly:

IDA View
; =============== S U B R O U T I N E =======================================
CODE:004040FC
CODE:004040FC
CODE:004040FC ; int __usercall sub_4040FC@<eax>(int result@<eax>, char@<dl>, char)
CODE:004040FC sub_4040FC      proc near               ; CODE XREF: sub_403D94+7↑p
CODE:004040FC                                         ; sub_40D984+A↓p ...
CODE:004040FC
CODE:004040FC arg_0           = byte ptr  4
CODE:004040FC
CODE:004040FC                 push    edx
CODE:004040FD                 push    ecx
CODE:004040FE                 push    ebx
CODE:004040FF                 test    dl, dl
CODE:00404101                 jl      short loc_404106
CODE:00404103                 call    dword ptr [eax-0Ch]
CODE:00404106
CODE:00404106 loc_404106:                             ; CODE XREF: sub_4040FC+5↑j
CODE:00404106                 xor     edx, edx
CODE:00404108                 lea     ecx, [esp+0Ch+arg_0]
CODE:0040410C                 mov     ebx, fs:[edx]
CODE:0040410F                 mov     [ecx], ebx
CODE:00404111                 mov     [ecx+8], ebp
CODE:00404114                 mov     dword ptr [ecx+4], offset sub_404125
CODE:0040411B                 mov     [ecx+0Ch], eax
CODE:0040411E                 mov     fs:[edx], ecx
CODE:00404121                 pop     ebx
CODE:00404122                 pop     ecx
CODE:00404123                 pop     edx
CODE:00404124                 retn
CODE:00404124 sub_4040FC      endp
Pseudocode
int __usercall sub_4040FC@<eax>(int result@<eax>, char a2@<dl>, char a3)
{
  if ( a2 >= 0 )
    result = (*(int (**)(void))(result - 12))();
  __writefsdword(0, (unsigned int)&a3);
  return result;
}

Seeing that negative offset in result-12, I tried using __shifted

I created new struct

00000000 struct s20 // sizeof=0x10
00000000 {
00000000 void (*foo)(void);
00000004 _BYTE gap[8];
0000000C int i;
00000010 };

and then changed type of result to int *__shifted(s20,0xC)

My function turns into variadic
int *__shifted(s20,0xC) __usercall sub_4040FC@<eax>(int *__shifted(s20,0xC) result@<eax>, char a2@<dl>, ...)
{
  va_list va; // [esp+4h] [ebp+4h] BYREF

  va_start(va, a2);
  if ( a2 >= 0 )
    result = (int *__shifted(s20,0xC))((int (*)(void))ADJ(result)->foo)();
  __writefsdword(0, (unsigned int)va);
  return result;
}

Notice that it has va_list and va_begin - but no va_end.

Ctrl+Z reverts type, but not it turning into variadic
int __usercall sub_4040FC@<eax>(int result@<eax>, char a2@<dl>, ...)
{
  va_list va; // [esp+4h] [ebp+4h] BYREF

  va_start(va, a2);
  if ( a2 >= 0 )
    result = (*(int (**)(void))(result - 12))();
  __writefsdword(0, (unsigned int)va);
  return result;
}

I feel stuck

We’ll check what’s wrong with Undo, but in the meantime I would suggest applying Lumina or one of the Delphi signatures - this looks like a standard Delphi function, probably ClassCreate.

Yeah, that’s Delphi. It has class methods at negative offsets in the vftable.